# HG changeset patch # User Tero Marttila # Date 1233948662 -7200 # Node ID d6a8258bd90ea11b927018fd0ea23e638a2118fa # Parent 5565d94da52268f972528409414dbd093e096b42 YES YES MOAR WSGI - Hello World diff -r 5565d94da522 -r d6a8258bd90e index.cgi --- 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() diff -r 5565d94da522 -r d6a8258bd90e lib/Makefile --- 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 $< - diff -r 5565d94da522 -r d6a8258bd90e lib/handler.py --- /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") + diff -r 5565d94da522 -r d6a8258bd90e lib/http.py --- /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 = """\ +%(title)s +

%(title)s

+

%(message)s

+ +""" % 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) + + diff -r 5565d94da522 -r d6a8258bd90e lib/loaders.py --- 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) - diff -r 5565d94da522 -r d6a8258bd90e lib/page.py --- 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 = 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 - - - diff -r 5565d94da522 -r d6a8258bd90e lib/request.py --- 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)) - diff -r 5565d94da522 -r d6a8258bd90e lib/wsgi.py --- /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() +