YES YES MOAR WSGI - Hello World
authorTero Marttila <terom@fixme.fi>
Fri, 06 Feb 2009 21:31:02 +0200
changeset 7 d6a8258bd90e
parent 6 5565d94da522
child 8 0ce1f471e9d7
YES YES MOAR WSGI - Hello World
index.cgi
lib/Makefile
lib/handler.py
lib/http.py
lib/loaders.py
lib/page.py
lib/request.py
lib/wsgi.py
pages/__init__.py
--- a/index.cgi	Fri Feb 06 20:49:29 2009 +0200
+++ b/index.cgi	Fri Feb 06 21:31:02 2009 +0200
@@ -1,3 +1,26 @@
 #!/usr/bin/python2.5
+# :set filetype=py encoding=utf8
 
+"""
+    CGI implementation
+"""
 
+# CGI handler for WSGI
+import wsgiref.handlers
+
+# our WSGI app
+from lib import wsgi
+
+def cgi_main () :
+    """
+        Run in CGI mode
+    """
+    
+    # create handler
+    cgi_handler = wsgiref.handlers.CGIHandler()
+    
+    # run once
+    cgi_handler.run(wsgi.app)
+
+if __name__ == '__main__' :
+    cgi_main()
--- a/lib/Makefile	Fri Feb 06 20:49:29 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-
-all: $(patsubst %.tmpl,%.py,$(wildcard templates/*.tmpl))
-
-templates/%.py: templates/%.tmpl
-	@cheetah compile --nobackup $<
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/handler.py	Fri Feb 06 21:31:02 2009 +0200
@@ -0,0 +1,13 @@
+"""
+    The actual application behaviour, i.e. generating a Response from a Request :)
+"""
+
+import http, page
+
+def handle_request (request) :
+    """
+        Take the Request, and return a Response
+    """
+
+    return http.Response("Hello World")
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/http.py	Fri Feb 06 21:31:02 2009 +0200
@@ -0,0 +1,141 @@
+"""
+    WSGI HTTP utility code
+"""
+
+# for utility functions
+import cgi
+
+# for header handling
+import wsgiref.headers
+
+class Request (object) :
+    """
+        HTTP Request with associated metadata
+    """
+
+    def __init__ (self, env) :
+        """
+            Parse env data
+        """
+
+        # store env
+        self.env = env
+
+        # get the querystring
+        self.arg_str = env.get('QUERY_STRING', '')
+
+        # parse query args
+        self.arg_dict = cgi.parse_qs(self.arg_str, True)
+
+class Response (object) :
+    """
+        HTTP Response with headers and data
+    """
+
+    def __init__ (self, data, content_type='text/html', status='200 OK', charset='utf8') :
+        """
+            Create the response. The Content-type header is built from the given values. The given \a data must be
+            either a str (which is sent plain), an unicode object (which is encoded with the relevant charset), or
+            None, whereupon an empty response body is sent. The content_type argument can also be forced to None to
+            not send a Content-type header (e.g. for redirects)
+        """
+
+        # store info
+        self.status = status
+        self.data = data
+        self.charset = charset
+
+        # headers
+        self.headers = wsgiref.headers.Headers([])
+        
+        # add Content-type header?
+        if content_type :
+            self.add_header('Content-type', content_type, charset=charset)
+
+    def add_header (self, name, value, **params) :
+        """
+            Add response header with the given name/value, plus option params
+
+            XXX: uses the wsgiref.headers code, not sure how that behaves re multiple headers with the same name, etc
+        """
+        
+        self.headers.add_header(name, value, **params)
+    
+    def get_status (self) :
+        """
+            Returns response status string (XXX Foo)
+        """
+
+        return self.status
+    
+    def get_headers (self) :
+        """
+            Returns the list of header (name, value) pairs
+        """
+
+        return self.headers.items()
+
+    def get_data (self) :
+        """
+            Returns the response data - as an encoded string
+        """
+
+        if self.data :
+            return self.data.encode(self.charset)
+
+        else :
+            return ''
+
+class ErrorResponse (Response) :
+    """
+        A response with an error code / message
+    """
+
+    def __init__ (self, status, message) :
+        """
+            Build a plain error message response with the given status/message
+        """
+
+        data = """\
+<html><head><title>%(title)s</title></head><body>
+<h1>%(title)s</h1>
+<p>%(message)s</p>
+</body></html>
+""" % dict(
+            title       = status, 
+            message     = message
+        )
+            
+        super(ErrorResponse, self).__init__(data, status=status)
+
+class ResponseError (Exception) :
+    """
+        An exception that results in a specfic 4xx ErrorResponse message to the client
+    """
+
+    def __init__ (self, message, status='400 Bad Request') :
+        self.status = status
+        self.message = message
+
+        super(ResponseError, self).__init__(message)
+
+    def get_response (self) :
+        return ErrorResponse(self.status, self.message)
+
+class Redirect (Response) :
+    """
+        Redirect response
+    """
+
+    def __init__ (self, url) :
+        """
+            Redirect to given *absolute* URL
+        """
+        
+        # no content-type or data
+        super(Redirect, self).__init__(None, content_type=None, status='302 Found')
+
+        # add Location: header
+        self.add_header("Location", url)
+
+
--- a/lib/loaders.py	Fri Feb 06 20:49:29 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,160 +0,0 @@
-
-from __future__ import with_statement
-
-# XXX: for _HTMLPage
-import page
-import templates
-
-from pages import error404
-
-import os.path
-import imp
-
-# the page dir
-PAGE_DIR_PATH = "pages/"
-
-def build_page_path (*bits) :
-    # XXX: fix directory traversal...
-    
-    return os.path.join(PAGE_DIR_PATH, *bits)
-
-class PageLoader (object) :
-    """
-        Load Page objects from files under pages/
-    """
-    
-    # file extension, e.g. '.html' or '.py'
-    suffix = None
-
-    def __init__ (self, suffix) :
-        self.suffix = suffix
-        
-    def _build_path (self, page_path) :
-        """
-            Builds a path from base_path + page_path + suffix. Returns None if the path does not exist
-        """
-        
-        path = build_page_path(page_path) + self.suffix
-
-        if os.path.exists(path) :
-            return path
-
-        else :
-            return None
-
-    def load (self, page_path) :
-        """
-            Attempts to load the page at the given path, returns the class on success, None on failure
-        """
-        
-        abstract
-
-class _HTMLPage (page.Page) :
-    # the path to the .html file
-    file_path = None
-    
-    # parent page
-    # parent = None
-
-    def __init__ (self, *args) :
-        super(_HTMLPage, self).__init__(*args)
-
-        # open the .html and read in the contents
-        with open(self.file_path, "r") as fh :
-            self.file_data = fh.read()
-
-    def render_template (self) :
-        tpl = self._build_template(templates.layout)
-        
-        tpl.page_content = self.file_data
-        
-        return tpl
-
-class HTMLLoader (PageLoader) :
-    """
-        Static .html files that are inserted into the layout template as-is
-
-        Sub-pages are not supported...
-    """
-
-    def __init__ (self) :
-        super(HTMLLoader, self).__init__(".html")
-
-    def get_title (self, page_path) :
-        head, tail = os.path.split(page_path)
-
-        return tail.title() if tail else "Index"
-
-    def load (self, _page_path) :
-        _file_path = self._build_path(_page_path)
-        
-        # ignore if it doesn't exist
-        if not _file_path :
-            return
-        
-        # get page title
-        _title = self.get_title(_page_path)
-
-        # create a new class and return it
-        class _html_page (_HTMLPage) :
-            file_path = _file_path
-            title = _title
-            name = _title
-            path = _page_path
-            
-        # return it
-        return _html_page
-
-class PythonLoader (PageLoader) :
-    """
-        Dynamic .py files that define a Page class
-    """
-
-    def __init__ (self) :
-        super(PythonLoader, self).__init__(".py")
-    
-    def load (self, page_path) :
-        path = self._build_path(page_path)
-
-        # ignore if not exists
-        if not path :
-            return
-
-        # load the module dynamically
-        module = imp.load_source("__dyn_%d" % id(path), path)
-
-        # return the Page object
-        return module.Page
-
-# our defined loaders
-loaders = [
-    HTMLLoader(),
-    PythonLoader(),
-]
-
-def load_page (req) :
-    """
-        Returns an instance of a Page object corresponding to the given req
-    """
-
-    # page path is given in req
-    page_path = req.page_path
-
-    # if it's a dir, then add 'index'
-    if os.path.isdir(build_page_path(page_path)) :
-        page_path = os.path.join(page_path, "index")
-
-    # try each loader in turn
-    for loader in loaders :
-        page = loader.load(req.page_path)
-        
-        # found?
-        if page :
-            break
-
-    # 404 error...
-    if not page :
-        page = error404.Error404
-        
-    return page(req, req.page_path)
-
--- a/lib/page.py	Fri Feb 06 20:49:29 2009 +0200
+++ b/lib/page.py	Fri Feb 06 21:31:02 2009 +0200
@@ -1,59 +1,21 @@
 
-class Page (object) :
+"""
+    Handling page requests
+"""
+
+def handle_html (env) :
+    return "A HTML page"
+
+
+# list of page handlers, by type
+type_handlers = [
+    ('.html',   handle_html),
+]
+
+def lookup_handler (path) :
     """
-        A page is kind of like a controller, I guess
+        Look up and return a handler for the given page, or raise an error
     """
 
-    # the page title, used in the HTML <title>
-    title = None
-
-    # the page name, used in the menu
-    name = None
-
-    # the page path, used in the URL
-    path = None
-
-    # parent page, can be self
-    parent = None
-
-    # menu of sub-pages, may be empty
-    menu = None
-
-    def __init__ (self, req, path_suffix) :
-        self.req = req
-        self.path_suffix = path_suffix
-
-        # default parent to root
-        if not self.parent :
-            from pages import index as index_page
+    return handle_html
 
-            self.parent = index_page.Page
-    
-    def _build_template (self, template_class) :
-        tpl = template_class(searchList=[self.req])
-        
-        tpl.page_title = self.title
-        tpl.page_name = self.name
-        tpl.page_path = self.path
-
-        tpl.page_menu = self.menu
-        tpl.page = self
-        
-        return tpl
-    
-    def get_response_code (self) :
-        """
-            Returns the HTTP response code to be used
-        """
-
-        return 200
-
-    def render_template (self) :
-        """
-            Returns an instance of Cheetah.Template, prepopulated with whatever variables it needs, ready to be rendered
-        """
-
-        abstract
-
-
-
--- a/lib/request.py	Fri Feb 06 20:49:29 2009 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,73 +0,0 @@
-
-import os.path
-
-def get_site_url (script_name) :
-    """
-        Get the URL that points to the site root (i.e. where style.css is) using the given value of SCRIPT_NAME
-
-        /foo/bar/quux.py                -> /foo
-        /~terom/qmsk.net/site/index.py  -> /~terom/qmsk.net
-        /                               -> ""
-        None                            -> ""
-    """
-    
-    if script_name :
-        return os.path.dirname(os.path.dirname(script_name)).rstrip("/")
-    else :
-        return ""
-
-def get_page_path (path_info, default) :
-    """
-        Get the path of the page that was requested, or the given default is empty/invalid.
-        The path will never begin with an /.
-
-        /quux       -> quux
-        /           -> <default>
-        None        -> <default>
-    """
-    
-    if path_info :
-        # remove prefixed slashes
-        path_info = path_info.lstrip('/')
-
-    if path_info :
-        return path_info
-    else :
-        return default
-
-
-class Request (object) :
-    # The name of the site itself, this can be used to reference e.g. style.css
-    site_url = None
-
-    # The page root url, for links to pages
-    page_root = None
-
-    # The full path to the requested page
-    page_path = None
-    
-    def __init__ (self, environ, default_page='main') :
-        self.site_url = get_site_url(environ.get("SCRIPT_NAME"))
-        self.page_root = environ.get("SCRIPT_NAME")
-        self.page_path = get_page_path(environ.get("PATH_INFO"), default_page)
-    
-    def page_name_parts (self) :
-        """
-            Returns a list of page name components
-        """
-
-        return self.page_path.split('/')
-
-    def page_name_prefixes (self) :
-        """
-            Iterate over the components of the page name, yielding (prefix, suffix) pairs
-        """
-        
-        prefix = self.page_name_parts()
-        suffix = []
-
-        while prefix :
-            yield ('/'.join(prefix), '/'.join(suffix))
-
-            suffix.insert(0, prefix.pop(-1))
-
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/lib/wsgi.py	Fri Feb 06 21:31:02 2009 +0200
@@ -0,0 +1,59 @@
+
+"""
+    WSGI application implementation
+"""
+
+# for error reporting
+import sys, traceback
+
+# for Request/Response
+import http
+
+# for the request -> response bit :)
+import handler
+
+def request_handler (env, start_response) :
+    """
+        The actual request handling code
+    """
+
+    # build Request object
+    request = http.Request(env)
+    
+    try :
+        # request -> response
+        response = handler.handle_request(request)
+
+    except http.ResponseError, err :
+        # just use the generated response
+        response = err.get_response()
+
+    # send response
+    assert response, "No response"
+    
+    # send response status/headers
+    start_response(response.get_status(), response.get_headers())
+
+    # send respones data
+    yield response.get_data()
+
+def app (env, start_response) :
+    """
+        Wraps request_handler to trap errors
+    """
+
+    try :
+        # passthrough request_handler
+        for chunk in request_handler(env, start_response) :
+            yield chunk
+
+    except :
+        # execption info
+        info = sys.exc_info()
+
+        # try and send 500 ISE to browser, if no headers yet...
+        start_response("500 Internal Server Error", [('Content-type', "text/plain; charset=utf8")], info)
+
+        # send traceback
+        yield traceback.format_exc()
+