From 2435f82361f6bc4dcd51e1305905ecbbb5757f50 Mon Sep 17 00:00:00 2001 From: Helmut Grohne Date: Sat, 14 Apr 2007 22:37:26 +0200 Subject: initial tree --- wsgitools/filters.py | 218 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 wsgitools/filters.py (limited to 'wsgitools/filters.py') diff --git a/wsgitools/filters.py b/wsgitools/filters.py new file mode 100644 index 0000000..1d6154f --- /dev/null +++ b/wsgitools/filters.py @@ -0,0 +1,218 @@ +__all__ = [] + +import sys +import time + +__all__.append("CloseableIterator") +class CloseableIterator: + """Concatenating iterator with close attribute.""" + def __init__(self, close_function, *iterators): + """If close_function is not None, it will be the close attribute of + the created iterator object. Further parameters specify iterators + that are to be concatenated.""" + if close_function is not None: + self.close = close_function + self.iterators = map(iter, iterators) + def __iter__(self): + """iterator interface""" + return self + def next(self): + """iterator interface""" + if not self.iterators: + raise StopIteration + try: + return self.iterators[0].next() + except StopIteration: + self.iterators.pop(0) + return self.next() + +__all__.append("CloseableList") +class CloseableList(list): + """A list with a close attribute.""" + def __init__(self, close_function, *args): + """If close_function is not None, it will be the close attribute of + the created list object. Other parameters are passed to the list + constructor.""" + if close_function is not None: + self.close = close_function + list.__init__(self, *args) + def __iter__(self): + return CloseableIterator(getattr(self, "close", None), + list.__iter__(self)) + +__all__.append("BaseWSGIFilter") +class BaseWSGIFilter: + """Generic WSGI filter class to be used with WSGIFilterMiddleware. + + For each request a filter object gets created. + The environment is then passed through filter_environ. + Possible exceptions are filtered by filter_exc_info. + After that for each (header, value) tuple filter_header is used. + The resulting list is filtered through filter_headers. + Any data is filtered through filter_data. + In order to possibly append data the append_data method is invoked. + When the request has finished handle_close is invoked. + + All methods do not modify the passed data by default. Passing the + BaseWSGIFilter class to a WSGIFilterMiddleware will result in not modifying + the request at all. + """ + def __init__(self): + """This constructor does nothing and can safely be overwritten. It is + only listed here to document that it must be callable without additional + parameters.""" + pass + def filter_environ(self, environ): + """Receives a dict with the environment passed to the wsgi application + and a dict must be returned. The default is to return the same dict.""" + return environ + def filter_exc_info(self, exc_info): + """Receives either None or a tuple passed as third argument to + start_response from the wrapped wsgi application. Either None or such a + tuple must be returned.""" + return exc_info + def filter_status(self, status): + """Receives a status string passed as first argument to start_response + from the wrapped wsgi application. A valid HTTP status string must be + returned.""" + return status + def filter_header(self, headername, headervalue): + """This function is invoked for each (headername, headervalue) tuple in + the second argument to the start_response from the wrapped wsgi + application. Such a value or None for discarding the header must be + returned.""" + return (headername, headervalue) + def filter_headers(self, headers): + """A list of headers passed as the second argument to the start_response + from the wrapped wsgi application is passed to this function and such a + list must also be returned.""" + return headers + def filter_data(self, data): + """For each string that is either written by the write callable or + returned from the wrapped wsgi application this method is invoked. It + must return a string.""" + return data + def append_data(self): + """This function can be used to append data to the response. A list of + strings or some kind of iterable yielding strings has to be returned. + The default is to return an empty list. + """ + return [] + def handle_close(self): + """This method is invoked after the request has finished.""" + pass + +__all__.append("WSGIFilterMiddleware") +class WSGIFilterMiddleware: + """This wsgi middleware can be used with specialized BaseWSGIFilters to + modify wsgi requests and/or reponses.""" + def __init__(self, app, filterclass): + """app is a wsgi application. + filterclass is a subclass of BaseWSGIFilter or some class that + implements the interface.""" + self.app = app + self.filterclass = filterclass + def __call__(self, environ, start_response): + """wsgi interface""" + filter = self.filterclass() + environ = filter.filter_environ(environ) + + def modified_start_response(status, headers, exc_info=None): + exc_info = filter.filter_exc_info(exc_info) + status = filter.filter_status(status) + headers = (filter.filter_header(h, v) for h, v in headers) + headers = [h for h in headers if h is not None] + headers = filter.filter_headers(headers) + write = start_response(status, headers, exc_info) + def modified_write(data): + write(filter.filter_data(data)) + return modified_write + + ret = self.app(environ, modified_start_response) + + def modified_close(): + filter.handle_close() + getattr(ret, "close", lambda:0)() + + if isinstance(ret, list): + return CloseableList(modified_close, + [filter.filter_data(data) for data in ret] + + list(filter.append_data())) + ret = iter(ret) + return CloseableIterator(modified_close, + (filter.filter_data(data) for data in ret), + filter.append_data()) + +__all__.append("RequestLogWSGIFilter") +class RequestLogWSGIFilter(BaseWSGIFilter): + """This filter logs all requests in the apache log file format.""" + @classmethod + def creator(cls, log): + return lambda:cls(log) + def __init__(self, log=sys.stdout): + self.log = log + self.time = time.strftime("%d/%b/%Y:%T %z") + self.length = 0 + def filter_environ(self, environ): + """BaseWSGIFilter interface""" + self.remote = environ.get("REMOTE_ADDR", "?") + self.reqmethod = environ["REQUEST_METHOD"] + self.path = environ["SCRIPT_NAME"] + environ["PATH_INFO"] + self.proto = environ.get("SERVER_PROTOCOL", None) + self.referrer = environ.get("HTTP_REFERER", "-") + self.useragent = environ.get("HTTP_USER_AGENT", "-") + return environ + def filter_status(self, status): + """BaseWSGIFilter interface""" + self.status = status.split()[0] + return status + def filter_data(self, data): + """BaseWSGIFilter interface""" + self.length += len(data) + return data + def handle_close(self): + """BaseWSGIFilter interface""" + line = '%s - - [%s] "%s' % (self.remote, self.time, self.reqmethod) + line = '%s %s' % (line, self.path) + if self.proto is not None: + line = "%s %s" % (line, self.proto) + line = '%s" %s %d' % (line, self.status, self.length) + if self.referrer is not None: + line = '%s "%s"' % (line, self.referrer) + else: + line += " -" + if self.useragent is not None: + line = '%s "%s"' % (line, self.useragent) + else: + line += " -" + print >> self.log, line + +__all__.append("TimerWSGIFilter") +class TimerWSGIFilter(BaseWSGIFilter): + @classmethod + def creator(cls, pattern): + return lambda:cls(pattern) + def __init__(self, pattern="?GenTime"): + self.pattern = pattern + self.start = time.time() + def filter_data(self, data): + """BaseWSGIFilter interface""" + if data == self.pattern: + return "%8.3g" % (time.time() - self.start) + return data + +__all__.append("EncodeWSGIFilter") +class EncodeWSGIFilter(BaseWSGIFilter): + """Encodes all body data (no headers) with given charset.""" + @classmethod + def creator(cls, charset): + return lambda:cls(charset) + def __init__(self, charset="utf-8"): + self.charset = charset + def filter_data(self, data): + """BaseWSGIFilter interface""" + return data.encode(self.charset) + def filter_header(self, header, value): + if header.lower() != "content-type": + return (header, value) + return (header, "%s; charset=%s" % (value, self.charset)) -- cgit v1.2.3