http.py
changeset 46 54c5f5f340de
parent 42 5a72c00c4ae4
child 49 9b097385b463
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/http.py	Sun Feb 08 03:17:07 2009 +0200
@@ -0,0 +1,227 @@
+"""
+    WSGI HTTP utility code
+"""
+
+# for utility functions
+import cgi
+
+# for header handling
+import wsgiref.headers
+
+# for path handling
+import os.path
+
+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)
+ 
+    @property
+    def site_host (self) :
+        """
+            Returns the site's hostname (DNS name)
+        """
+        
+        return self.env['HTTP_HOST']
+  
+    @property
+    def site_root (self) :
+        """
+            Returns the URL path to the requested script's directory with no trailing slash, i.e.
+
+            /               -> 
+            /foo.cgi        -> 
+            /foo/bar.cgi    -> /foo
+        """
+
+        return os.path.dirname(self.env['SCRIPT_NAME']).rstrip('/')
+    
+    @property
+    def page_prefix (self) :
+        """
+            Returns the URL path root for page URLs, based on REQUEST_URI with PATH_INFO removed
+
+            /                   -> 
+            /foo.cgi            -> /foo.cgi
+            /foo.cgi/index      -> /foo.cgi
+            /foo.cgi/quux/bar   -> /foo.cgi
+            /quux/foo.cgi/bar   -> /quux/foo.cgi
+            /bar                -> 
+        """
+        
+        # XXX: request uri path without the query string
+        request_path = self.env.get('REQUEST_URI', '').split('?', 1)[0].rstrip('/')
+
+        # path info
+        page_name = self.get_page_name()
+
+        # special-case for empty page_name
+        if not page_name :
+            return request_path
+        
+        # sanity-check
+        assert request_path.endswith(page_name)
+        
+        # trim
+        return request_path[:-len(page_name)].rstrip('/')
+    
+    def get_page_name (self) :
+        """
+            Returns the requested page path with no leading slash, i.e.
+
+            /foo.cgi        -> 
+            /foo.cgi/       -> 
+            /foo.cgi/bar    -> bar
+            /foo.cgi/quux/  -> quux/
+        """
+        
+        # the raw PATH_INFO
+        path_info = self.env.get('PATH_INFO')
+        
+        # avoid nasty '.' paths
+        if path_info :
+            return os.path.normpath(path_info).lstrip('/')
+
+        else :
+            return ''
+    
+    def get_args (self) :
+        """
+            Iterate over all available (key, value) pairs from the query string
+        """
+
+        return cgi.parse_qsl(self.arg_str)
+
+class Response (object) :
+    """
+        HTTP Response with headers and data
+    """
+
+    def __init__ (self, data, content_type='text/html', status='200 OK', charset='UTF-8') :
+        """
+            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, details=None) :
+        """
+            Build a plain error message response with the given status/message
+
+            @param status HTTP status code
+            @param message short message to describe errors
+            @param details optional details, plaintext
+        """
+
+        data = """\
+<html><head><title>%(title)s</title></head><body>
+<h1>%(title)s</h1>
+<p>%(message)s</p>
+%(details)s
+</body></html>
+""" % dict(
+            title       = status, 
+            message     = message,
+            details     = '<pre>%s</pre>' % details if details else ''
+        )
+            
+        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', details=None) :
+        self.status = status
+        self.message = message
+        self.details = details
+
+        super(ResponseError, self).__init__(message)
+
+    def get_response (self) :
+        return ErrorResponse(self.status, self.message, self.details)
+
+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)
+
+