Commits

Jamie Turner  committed e48156a

Big cleanup, docstrings, added a few more examples, updated
the cherrypy and WSGI stuff.

  • Participants
  • Parent commits b7abedd

Comments (0)

Files changed (14)

File diesel/app.py

 # vim:ts=4:sw=4:expandtab
+'''The main Application and Service classes
+'''
 import socket
 import traceback
 import os
 current_app = None
 
 class Application(object):
+    '''The Application represents diesel's main loop--
+    the coordinating entity that runs all Services, Loops,
+    Client protocol work, etc.
+    '''
     def __init__(self, logger=None):
         global current_app
         current_app = self
         self._loops = []
 
     def run(self):
+        '''Start up an Application--blocks until the program ends
+        or .halt() is called.
+        '''
         self._run = True
         logmod.set_current_application(self)
         log.info('Starting diesel application')
         log.info('Ending diesel application')
 
     def add_service(self, service):
+        '''Add a Service instance to this Application.
+
+        The service will bind to the appropriate port and start
+        handling connections when the Application is run().
+        '''
         service.application = self
         if self._run:
             s.bind_and_listen()
             self._services.append(service)
 
     def add_loop(self, loop, front=False):
+        '''Add a Loop instance to this Application.
+
+        The loop will be started when the Application is run().
+        '''
         loop.application = self
         if self._run:
             loop.iterate()
                 self._loops.append(loop)
         
     def halt(self):    
+        '''Stop this application from running--the initial run() call
+        will return.
+        '''
         self.hub.run = False
         self._run = False
 
     def setup(self):
+        '''Do some initialization right before the main loop is entered.
+
+        Called by run().
+        '''
         pass
 
 class Service(object):
+    '''A TCP service listening on a certain port, with a protocol 
+    implemented by a passed connection handler.
+    '''
     LQUEUE_SIZ = 500
     def __init__(self, connection_handler, port, iface=''):
+        '''Given a generator definition `connection_handler`, handle
+        connections on port `port`.
+
+        Interface defaults to all interfaces, but overridable with `iface`.
+        '''
         self.port = port
         self.iface = iface
         self.sock = None
         sock.listen(self.LQUEUE_SIZ)
         self.sock = sock
 
-    def _get_listening(self):
+    @property
+    def listening(self):
         return self.sock is not None
 
-    listening = property(_get_listening)
-
     def accept_new_connection(self):
         sock, addr = self.sock.accept()
         Connection(sock, addr, self.connection_handler).iterate()

File diesel/buffer.py

 # vim:ts=4:sw=4:expandtab
 class Buffer(object):
+    '''An input buffer.
+
+    Allows socket data to be read immediately and buffered, but
+    fine-grained byte-counting or sentinel-searching to be
+    specified by consumers of incoming data.
+    '''
     def __init__(self):
         self._atinbuf = []
         self._atterm = None
         self._atmark = 0
         
     def set_term(self, term):
+        '''Set the current sentinel.
+
+        `term` is either an int, for a byte count, or
+        a string, for a sequence of characters that needs
+        to occur in the byte stream.
+        '''
         self._atterm = term
 
     def feed(self, data):
+        '''Feed some data into the buffer.
+
+        The buffer is appended, and the check() is run in case
+        this append causes the sentinel to be satisfied.
+        '''
         self._atinbuf.append(data)
         self._atmark += len(data)
         return self.check()
 
     def check(self):
-        '''Look for the message
+        '''Look for the next message in the data stream based on
+        the current sentinel.
         '''
         ind = None
         all = None

File diesel/client.py

 from collections import deque
 
 class call(object):
+    '''A decorator that indicates to diesel a separate
+    client and generator needs to do some protocol work
+    and return a response.
+    '''
     def __init__(self, f, inst=None):
         self.f = f
         self.client = inst
         self.client.conn.wake()
 
 class response(object):
+    '''A yield token that indicates a client method has finished
+    protocol work and has a return value for the @call-ing generator.
+    '''
     def __init__(self, value):
         self.value = value
 
 class Client(object):
+    '''An agent that connects to an external host and provides an API to
+    return data based on a protocol across that host.
+    '''
     def __init__(self, connection_handler=None):
         self.connection_handler = connection_handler or self.client_conn_handler
         self.jobs = deque()
         self.conn = None
      
     def connect(self, addr, port):  
+        '''Connect to a remote host.
+        '''
         remote_addr = (addr, port)
         sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
         sock.connect(remote_addr)
         self.conn.iterate()
 
     def close(self):
+        '''Close the socket to the remote host.
+        '''
         self.conn = None
 
     @property
         return self.conn is None
 
     def client_conn_handler(self, addr):
+        '''The default connection handler.  Handles @call-ing
+        behavior to client API methods.
+        '''
         from diesel.core import sleep, ConnectionClosed
         yield self.on_connect()
 
                 break
 
     def on_connect(self):
+        '''Hook to implement a handler to do any setup after the
+        connection has been established.
+        '''
         if 0: yield 0
 
     def on_close(self):
-        pass
+        '''Hook called when the remote side closes the connection,
+        for cleanup.
+        '''

File diesel/core.py

 # vim:ts=4:sw=4:expandtab
+'''Core implementation/handling of generators, including
+the various yield tokens.
+'''
 import socket
 from types import GeneratorType
 from collections import deque, defaultdict
 from diesel import buffer
 from diesel.client import call, response
 
-class ConnectionClosed(socket.error): pass
+class ConnectionClosed(socket.error): 
+    '''Raised if the client closes the connection.
+    '''
+    pass
 
 CRLF = '\r\n'
 BUFSIZ = 2 ** 14
 
 class until(object):
+    '''A yield token that indicates the generator wants
+    a string sent back from the socket stream when a 
+    certain `sentinel` is encountered.
+    '''
     def __init__(self, sentinel):
         self.sentinel = sentinel
 
 def until_eol():
+    '''Macro for `until("\r\n")`.
+    '''
     return until(CRLF)
 
 class bytes(object):
+    '''A yield token that indicates the generator wants
+    a string sent back from the socket stream when a
+    certain number of bytes are available.
+    '''
     def __init__(self, sentinel):
         self.sentinel = sentinel
 
 class sleep(object):
+    '''A yield token that indicates the generator wants
+    a callback in `duration` seconds.
+
+    If no argument is passed, the generator will be called
+    again during the next iteration of the main loop.  It
+    can act as a way to yield control to other loops, with
+    the intention of taking control back as soon as they've
+    had a pass.
+    '''
     def __init__(self, duration=0):
         self.duration = duration
 
 class up(object):
+    '''For nested generators, a yield token that indicates this value is 
+    being passed "up" the stack to the "calling" generator, and isn't intended
+    as a message for diesel itself.
+    '''
     def __init__(self, value):
         self.value = value
 
 class wait(object):
+    '''A yield token that indicates a generators desire to wait until a
+    certain event is `fire`d.
+    '''
     def __init__(self, event):
         self.event = event
 
 class fire(object):
+    '''A yield token that fires an event to any appropriate `wait`ers.
+    '''
     def __init__(self, event, value=None):
         self.event = event
         self.value = value
 
 class WaitPool(object):
+    '''A structure that manages all `wait`ers, makes sure fired events
+    get to the right places, and that all other waits are canceled when
+    a one event is passed back to a generator.
+    '''
     def __init__(self):
         self.waits = defaultdict(set)
         self.loop_refs = defaultdict(set)
 waits = WaitPool()
 
 class NoPipeline(object):
+    '''Fake pipeline for Loops that aren't managing a connection and have no
+    I/O stream.
+    '''
     def __getattr__(self, *args, **kw):
         return ValueError("Cannot write to the outgoing pipeline for socketless Loops (yield string, file)")
     empty = True
 
 class NoBuffer(object):
+    '''Fake buffer for loops that aren't managing a connection and have no
+    I/O stream.
+    '''
     def __getattr__(self, *args, **kw):
         return ValueError("Cannot check incoming buffer on socketless Loops (yield until, bytes, etc)")
 
 ids = id_gen()
 
 class Loop(object):
-    '''A cooperative generator.
+    '''A cooperative generator that represents an arbitrary piece of
+    logic.
     '''
     def __init__(self, loop_callable, *callable_args):
         self.g = self.cycle_all(loop_callable(*callable_args))
         return other.id == self.id
 
     def fire(self, what, value):
+        '''Fire an event back into this generator.
+        '''
         if what in self.fire_handlers:
             handler = self.fire_handlers.pop(what)
             self.fire_handlers = {}
             handler(value)
 
     def cycle_all(self, current, error=None):
-        '''Effectively flattens all iterators.
+        '''Effectively flattens all iterators, providing the
+        "generator stack" effect.
         '''
         last = None
         stack = []
                         error = (ConnectionClosed, str(e))
 
     def multi_callin(self, pos, tot, real_f=None):
+        '''Provide a callable that will pass `None` in all spots
+        that aren't the event that triggered the rescheduling of the
+        generator.  For yield groups.
+        '''
         real_f = real_f or self.wake
         if tot == 1:
             return real_f
         return f
 
     def iterate(self, n_val=None):
+        '''The algorithm that represents iterating over all items
+        in the nested generator that represents this Loop or
+        Connection.  Run whenever a generator is (re-)scheduled.
+        Handles all the `yield` tokens.
+        '''
+
         while True:
             try:
                 if n_val is not None:
             self.set_writable(True)
 
     def clear_pending_events(self):
+        '''When a loop is rescheduled, cancel any other timers or waits.
+        '''
         if self._wakeup_timer and self._wakeup_timer.pending:
             self._wakeup_timer.cancel()
         self.fire_handlers = {}
         waits.clear(self)
 
     def schedule(self, value=None):
+        '''Called by another Loop--reschedule this loop so the hub will run
+        it.  Used in `response` and `fire` situations.
+        '''
         self.clear_pending_events()
         self._wakeup_timer = self.hub.call_later(0, self.wake, value)
 
     def wake(self, value=None):
+        '''Wake up this loop.  Called by the main hub to resume a loop
+        when it is rescheduled.
+        '''
         self.clear_pending_events()
         self.iterate(value)
 
 class Connection(Loop):
-    '''A cooperative loop hooked up to a socket.
+    '''A `Loop` with an associated socket and I/O stream.
     '''
     def __init__(self, sock, addr, connection_handler):
         Loop.__init__(self, connection_handler, addr)
         self.closed = False
 
     def set_writable(self, val):
+        '''Set the associated socket writable.  Called when there is
+        data on the outgoing pipeline ready to be delivered to the 
+        remote host.
+        '''
         if self.closed:
             return
         if val and not self._writable:
             self._writable = False
 
     def shutdown(self, remote_closed=False):
+        '''Clean up after a client disconnects or after
+        the connection_handler ends (and we disconnect).
+        '''
         self.hub.unregister(self.sock)
         self.closed = True
         if not remote_closed:
         self.g = None
 
     def handle_write(self):
+        '''The low-level handler called by the event hub
+        when the socket is ready for writing.
+        '''
         if not self.pipeline.empty:
             try:
                 data = self.pipeline.read(BUFSIZ)
                         self.set_writable(False)
 
     def handle_read(self):
+        '''The low-level handler called by the event hub
+        when the socket is ready for reading.
+        '''
         disconnect_reason = None
         try:
             data = self.sock.recv(BUFSIZ)

File diesel/hub.py

 # vim:ts=4:sw=4:expandtab
+'''An event hub that supports sockets and timers, based
+on Python 2.6's epoll support.
+'''
 import select
 from select import EPOLLIN, EPOLLOUT, EPOLLPRI
 from collections import deque
 from time import time
 
 class Timer(object):
-    ALLOWANCE = 0.03
+    '''A timer is a promise to call some function at a future date.
+    '''
+    ALLOWANCE = 0.03 # If we're within 30ms, the timer is due
     def __init__(self, interval, f, *args, **kw):
         self.trigger_time = time() + interval
         self.f = f
         self.pending = False
 
     def callback(self):
+        '''When the external entity checks this timer and determines
+        it's due, this function is called, which calls the original 
+        callback.
+        '''
         self.pending = False
         return self.f(*self.args, **self.kw)
 
     @property
     def due(self):
+        '''Is it time to run this timer yet?
+
+        The allowance provides some give-and-take so that if a 
+        sleep() delay comes back a little early, we still go.
+        '''
         return (self.trigger_time - time()) < self.ALLOWANCE
 
 class EventHub(object):
+    '''A epoll-based hub.
+    '''
     SIZE_HINT = 50000
     def __init__(self):
         self.epoll = select.epoll(self.SIZE_HINT)
         self.events = {}
 
     def handle_events(self):
+        '''Run one pass of event handling.
+
+        epoll() is called, with a timeout equal to the next-scheduled
+        timer.  When epoll returns, all fd-related events (if any) are
+        handled, and timers are handled as well.
+        '''
         if self.new_timers:
             self.timers.extend(self.new_timers)
             self.timers = deque(sorted(self.timers))
             timeout = 0
         events = self.epoll.poll(timeout)
 
+        # Run timers first, to try to nail their timings
         while self.timers:
             if self.timers[0][1].due:
                 t = self.timers.popleft()[1]
             else:
                 break
         
+        # Handle all socket I/O
         for (fd, evtype) in events:
             if evtype == EPOLLIN or evtype == EPOLLPRI:
                 self.events[fd][0]()
             if not self.run:
                 return
 
+        # Run timers one last time, until no more timers are due
         runs = -1
         while runs != 0:
             runs = 0
                     break
 
     def call_later(self, interval, f, *args, **kw):
+        '''Schedule a timer on the hub.
+        '''
         t = Timer(interval, f, *args, **kw)
         self.new_timers.append((t.trigger_time, t))
         return t
 
     def register(self, fd, read_callback, write_callback):
+        '''Register a socket fd with the hub, providing callbacks
+        for read (data is ready to be recv'd) and write (buffers are
+        ready for send()).
+
+        By default, only the read behavior will be polled and the
+        read callback used until enable_write is invoked.
+        '''
         assert fd not in self.events
         self.events[fd.fileno()] = (read_callback, write_callback)
         self.epoll.register(fd, EPOLLIN | EPOLLPRI)
 
     def enable_write(self, fd):
+        '''Enable write polling and the write callback.
+        '''
         self.epoll.modify(fd, EPOLLIN | EPOLLPRI | EPOLLOUT)
 
     def disable_write(self, fd):
+        '''Disable write polling and the write callback.
+        '''
         self.epoll.modify(fd, EPOLLIN | EPOLLPRI)
 
     def unregister(self, fd):
+        '''Remove this socket from the list of sockets the
+        hub is polling on.
+        '''
         fn = fd.fileno()
         if fn in self.events:
             del self.events[fn]

File diesel/logmod.py

 # vim:ts=4:sw=4:expandtab
+'''A simple logging module that supports various verbosity
+levels and component-specific subloggers.
+'''
 import sys
 import time
 
     
 
 class Logger(object):
+    '''Create a logger, with either a provided file-like object
+    or a list of such objects.  If no argument is provided, sys.stdout
+    will be used.
+
+    Optionally, override the global verbosity to be more or less verbose
+    than LOGLVL_WARN.
+    '''
     def __init__(self, fd=None, verbosity=LOGLVL_WARN):
         if fd is None:
             fd = [sys.stdout]
         self.level = verbosity
         self.component = None
 
+    # The actual message logging functions
     def _writelogline(self, lvl, message):
         if lvl >= self.level:
             for fd in self.fdlist:
     critical = lambda s, m: s._writelogline(LOGLVL_CRITICAL, m)
 
     def get_sublogger(self, component, verbosity=None):
+        '''Clone this logger and create a sublogger within the context
+        of `component`, and with the provided `verbosity`.
+
+        The same file object list will be used as the logging
+        location.
+        '''
         copy = Logger(verbosity=verbosity or self.level)
         copy.fdlist = self.fdlist
         copy.component = component

File diesel/pipeline.py

 # vim:ts=4:sw=4:expandtab
+'''An outgoing pipeline that can handle
+strings or files.
+'''
 try:
     import cStringIO
 except ImportError:
     f.seek(m)
     return r
 
-class PipelineLimitReached(Exception): pass
 class PipelineCloseRequest(Exception): pass
 class PipelineClosed(Exception): pass
     
 class Pipeline(object):
-    def __init__(self, limit=0):
+    '''A pipeline that supports appending strings or
+    files and can read() transparently across object
+    boundaries in the outgoing buffer.
+    '''
+    def __init__(self):
         self.line = []
-        self.limit = limit
-        self.used = 0
-        self.callbacks = []
         self.want_close = False
 
     def add(self, d):
+        '''Add object `d` to the pipeline.
+        '''
         if self.want_close:
             raise PipelineClosed
 
-        if self.limit > 0 and self.used >= self.limit:
-            raise PipelineLimitReached
-
         if type(d) is str:
-            if self.line and type(self.line[-1][0]) is _type_SIO and \
-            (self.limit == 0 or self.line[-1][1] < (self.limit / 2)):
+            if self.line and type(self.line[-1][0]) is _type_SIO:
                 fd, l = self.line[-1]
                 m = fd.tell()
                 fd.seek(0, 2)
                 self.line[-1] = [fd, l + len(d)]
             else:
                 self.line.append([make_SIO(d), len(d)])
-            self.used += len(d)
         else:
             self.line.append([d, get_file_length(d)])
 
     def close_request(self):
+        '''Add a close request to the outgoing pipeline.
+
+        No more data will be allowed in the pipeline, and, when
+        it is emptied, PipelineCloseRequest will be raised.
+        '''
         self.want_close = True
 
     def read(self, amt):
+        '''Read up to `amt` bytes off the pipeline.
+
+        May raise PipelineCloseRequest if the pipeline is
+        empty and the connected stream should be closed.
+        '''
         if self.line == [] and self.want_close:
             raise PipelineCloseRequest
 
         while self.line and read < amt:
             data = self.line[0][0].read(amt - read)
             if data == '':
-                if type(self.line[0][0]) is _type_SIO:
-                    self.used -= self.line[0][1]
                 del self.line[0]
             else:
                 rbuf.append(data)
                 read += len(data)
 
         while self.line and self.line[0][1] == self.line[0][0].tell():
-            self.used -= self.line[0][1]
             del self.line[0]
 
-        while self.callbacks and self.used < self.limit:
-            self.callbacks.pop(0).callback(self)
-
         return ''.join(rbuf)
     
     def backup(self, d):
+        '''Pop object d back onto the front the pipeline.
+
+        Used in cases where not all data is sent() on the socket,
+        for example--the remainder will be placed back in the pipeline.
+        '''
         self.line.insert(0, [make_SIO(d), len(d)])
-        self.used += len(d)
 
-    def _get_empty(self):
+    @property
+    def empty(self):
+        '''Is the pipeline empty?
+
+        A close request is "data" that needs to be consumed,
+        too.
+        '''
         return self.want_close == False and self.line == []
-    empty = property(_get_empty)
-
-    def _get_full(self):
-        return self.used == 0 or self.used >= self.limit
-    full = property(_get_full)

File diesel/protocols/http.py

 # vim:ts=4:sw=4:expandtab
+'''HTTP/1.1 implementation of client and server.
+'''
 import sys, socket
 import urllib
 from collections import defaultdict
 from diesel import up, until, until_eol, bytes, ConnectionClosed
 
 status_strings = {
-100 : "Continue",
-101 : "Switching Protocols",
-200 : "OK",
-201 : "Created",
-202 : "Accepted",
-203 : "Non-Authoritative Information",
-204 : "No Content",
-205 : "Reset Content",
-206 : "Partial Content",
-300 : "Multiple Choices",
-301 : "Moved Permanently",
-302 : "Found",
-303 : "See Other",
-304 : "Not Modified",
-305 : "Use Proxy",
-307 : "Temporary Redirect",
-400 : "Bad Request",
-401 : "Unauthorized",
-402 : "Payment Required",
-403 : "Forbidden",
-404 : "Not Found",
-405 : "Method Not Allowed",
-406 : "Not Acceptable",
-407 : "Proxy Authentication Required",
-408 : "Request Time-out",
-409 : "Conflict",
-410 : "Gone",
-411 : "Length Required",
-412 : "Precondition Failed",
-413 : "Request Entity Too Large",
-414 : "Request-URI Too Large",
-415 : "Unsupported Media Type",
-416 : "Requested range not satisfiable",
-417 : "Expectation Failed",
-500 : "Internal Server Error",
-501 : "Not Implemented",
-502 : "Bad Gateway",
-503 : "Service Unavailable",
-504 : "Gateway Time-out",
-505 : "HTTP Version not supported",
+    100 : "Continue",
+    101 : "Switching Protocols",
+    200 : "OK",
+    201 : "Created",
+    202 : "Accepted",
+    203 : "Non-Authoritative Information",
+    204 : "No Content",
+    205 : "Reset Content",
+    206 : "Partial Content",
+    300 : "Multiple Choices",
+    301 : "Moved Permanently",
+    302 : "Found",
+    303 : "See Other",
+    304 : "Not Modified",
+    305 : "Use Proxy",
+    307 : "Temporary Redirect",
+    400 : "Bad Request",
+    401 : "Unauthorized",
+    402 : "Payment Required",
+    403 : "Forbidden",
+    404 : "Not Found",
+    405 : "Method Not Allowed",
+    406 : "Not Acceptable",
+    407 : "Proxy Authentication Required",
+    408 : "Request Time-out",
+    409 : "Conflict",
+    410 : "Gone",
+    411 : "Length Required",
+    412 : "Precondition Failed",
+    413 : "Request Entity Too Large",
+    414 : "Request-URI Too Large",
+    415 : "Unsupported Media Type",
+    416 : "Requested range not satisfiable",
+    417 : "Expectation Failed",
+    500 : "Internal Server Error",
+    501 : "Not Implemented",
+    502 : "Bad Gateway",
+    503 : "Service Unavailable",
+    504 : "Gateway Time-out",
+    505 : "HTTP Version not supported",
 }
 
 def parse_request_line(line):
+    '''Given a request line, split it into 
+    (method, url, protocol).
+    '''
     items = line.split(' ')
     items[0] = items[0].upper()
     if len(items) == 2:
     return tuple(items)
 
 class HttpHeaders(object):
+    '''Support common operations on HTTP headers.
+
+    Parsing, modifying, formatting, etc.
+    '''
     def __init__(self):
         self._headers = defaultdict(list)
         self.link()
     def get(self, k, d=None):
         return self._headers.get(k.lower(), d)
 
+    def get_one(self, k, d=None):
+        return self.get(k, [d])[0]
+
     def __iter__(self):
         return self._headers
 
         return self.format()
 
 class HttpRequest(object):
-    def __init__(self, cmd, url, version, id=None):
-        self.cmd = cmd
+    '''Structure representing an HTTP request.
+    '''
+    def __init__(self, method, url, version):
+        self.method = method
         self.url = url
         self.version = version
         self.headers = None
         self.body = None
-        self.id = id
         
     def format(self):    
-        return '%s %s HTTP/%s' % (self.cmd, self.url, self.version)
+        '''Format the request line for the wire.
+        '''
+        return '%s %s HTTP/%s' % (self.method, self.url, self.version)
         
-class HttpProtocolError(Exception): pass    
 class HttpClose(object): pass    
 
 class HttpServer(object):
+    '''An HTTP/1.1 implementation or a server.
+    '''
     def __init__(self, request_handler):
+        '''`request_handler` is a callable that takes
+        an HttpRequest object and generates a response.
+        '''
         self.request_handler = request_handler
 
     BODY_CHUNKED, BODY_CL, BODY_NONE = range(3)
 
     def check_for_http_body(self, heads):
-        if heads.get('Transfer-Encoding') == ['chunked']:
+        if heads.get_one('Transfer-Encoding') == 'chunked':
             return self.BODY_CHUNKED
         elif 'Content-Length' in heads:
             return self.BODY_CL
         return self.BODY_NONE
 
     def __call__(self, addr):
-        req_id = 1
+        '''Since an instance of HttpServer is passed to the Service
+        class (with appropriate request_handler established during
+        initialization), this __call__ method is what's actually
+        invoked by diesel.
+
+        This is our generator, this is our protocol handler.
+
+        It does protocol work, then calls the request_handler, 
+        looking for HttpClose if necessary.
+        '''
         while True:
             chunks = []
             try:
             except ConnectionClosed:
                 break
 
-            cmd, url, version = parse_request_line(header_line)    
-            req = HttpRequest(cmd, url, version, req_id)
-            req_id += 1
+            method, url, version = parse_request_line(header_line)    
+            req = HttpRequest(method, url, version)
 
             header_block = yield until('\r\n\r\n')
 
             heads.parse(header_block)
             req.headers = heads
 
-            if req.version >= '1.1' and heads.get('Expect') == ['100-continue']:
+            if req.version >= '1.1' and heads.get_one('Expect') == '100-continue':
                 yield 'HTTP/1.1 100 Continue\r\n\r\n'
 
             more_mode = self.check_for_http_body(heads)
                 break
 
 def http_response(req, code, heads, body):
-    if req.version <= '1.0' and req.headers.get('Connection') != 'Keep-Alive':
+    '''A "macro", which can be called by `request_handler` callables
+    that are passed to an HttpServer.  Takes care of the nasty business
+    of formatting a response for you, as well as connection handling
+    on Keep-Alive vs. not.
+    '''
+    if req.version <= '1.0' and req.headers.get_one('Connection', '') != 'keep-alive':
         close = True
-    elif req.headers.get('Connection') == ['close'] or  \
-        heads.get('Connection') == ['close']:
+    elif req.headers.get_one('Connection') == 'close' or  \
+        heads.get_one('Connection') == 'close':
         close = True
     else:
         close = False
 from diesel import Client, call, response
 
 def handle_chunks(headers):
+    '''Generic chunk handling code, used by both client
+    and server.
+
+    Modifies the passed-in HttpHeaders instance.
+    '''
     chunks = []
     while True:
         chunk_head = yield until_eol()
             break
 
 class HttpClient(Client):
+    '''An HttpClient instance that issues 1.1 requests,
+    including keep-alive behavior.
+
+    Does not support sending chunks, yet... body must
+    be a string.
+    '''
     @call
     def request(self, method, path, headers, body=None):
+        '''Issues a `method` request to `path` on the
+        connected server.  Sends along `headers`, and
+        body.
+
+        Very low level--you must set "host" yourself,
+        for example.  It will set Content-Length, 
+        however.
+        '''
         req = HttpRequest(method, path, '1.1')
         
         if body:
         heads = HttpHeaders()
         heads.parse(header_block)
 
-        if heads.get('Transfer-Encoding') == ['chunked']:
+        if heads.get_one('Transfer-Encoding') == 'chunked':
             body = yield handle_chunks(heads)
         else:
-            cl = int(heads.get('Content-Length', [0])[0])
+            cl = int(heads.get_one('Content-Length', 0))
             if cl:
                 body = yield bytes(cl)
             else:
                 body = None
 
-        if version < '1.0' or heads.get('Connection', ['keep-alive'])[0] == 'close':
+        if version < '1.0' or heads.get_one('Connection') == 'close':
             self.close()
         yield response((code, heads, body))

File diesel/protocols/wsgi.py

 # vim:ts=4:sw=4:expandtab
-"""A minimal WSGI container.
+"""A minimal WSGI implementation to hook into
+diesel's HTTP module.
+
+Note: not well-tested.  Contributions welcome.
 """
 import urlparse
 import os
         pass
 
 def build_wsgi_env(req, port):
+    '''Produce a godawful CGI-ish mess from a sensible
+    API.
+    '''
     url_info = urlparse.urlparse(req.url)
     env = {}
 
     # CGI bits
-    env['REQUEST_METHOD'] = req.cmd
+    env['REQUEST_METHOD'] = req.method
     env['SCRIPT_NAME'] = ''
     env['PATH_INFO'] = url_info[2]
     env['QUERY_STRING'] = url_info[4]
     return env
 
 class WSGIRequestHandler(object):
+    '''The request_handler for the HttpServer that
+    bootsraps the WSGI environemtn and hands it off to the
+    WSGI callable.  This is the key coupling.
+    '''
     def __init__(self, app):
         self.app = app
 
         for n, v in self.response_headers:
             heads.add(n, v)
         body = ''.join(self.outbuf)
+        if 'Content-Length' not in heads:
+            heads.set('Content-Length', len(body))
+        
         return http_response(req, code, heads, body)
 
 class WSGIApplication(Application):
+    '''A WSGI application that takes over both `Service`
+    setup, `request_handler` spec for the HTTPServer,
+    and the app startup itself.
+
+
+    Just pass it a wsgi_callable and port information, and
+    it should do the rest.
+    '''
     def __init__(self, wsgi_callable, port=80, iface=''):
         Application.__init__(self)
         self.port = port

File examples/chat.py

+# vim:ts=4:sw=4:expandtab
+'''Simple chat server.
+
+telnet, type your name, hit enter, then chat.  Invite
+a friend to do the same.
+'''
+from diesel import Application, Service, until_eol, fire, wait
+
+def chat_server(addr):
+    my_nick = (yield until_eol()).strip()
+    while True:
+        my_message, other_message = yield (until_eol(), wait('chat_message'))
+        if my_message:
+            yield fire('chat_message', (my_nick, my_message.strip()))
+        else:
+            nick, message = other_message
+            yield "<%s> %s\r\n"  % (nick, message)
+
+app = Application()
+app.add_service(Service(chat_server, 8000))
+app.run()

File examples/combined.py

+# vim:ts=4:sw=4:expandtab
+'''Combine Client, Server, and Loop, in one crazy app.
+
+Just give it a run and off it goes.
+'''
+
+import time
+from diesel import Application, Service, Client, Loop, until, call, response
+
+def handle_echo(remote_addr):
+    while True:
+        message = yield until('\r\n')
+        yield "you said: %s" % message
+
+class EchoClient(Client):
+    @call
+    def echo(self, message):
+        yield message + '\r\n'
+        back = yield until("\r\n")
+        yield response(back)
+
+app = Application()
+
+def do_echos():
+    client = EchoClient()
+    client.connect('localhost', 8000)
+    t = time.time()
+    for x in xrange(5000):
+        msg = "hello, world #%s!" % x
+        echo_result = yield client.echo(msg)
+        assert echo_result.strip() == "you said: %s" % msg
+    print '5000 loops in %.2fs' % (time.time() - t)
+    app.halt()
+
+app.add_service(Service(handle_echo, port=8000))
+app.add_loop(Loop(do_echos))
+app.run()

File examples/cp.py

 # vim:ts=4:sw=4:expandtab
 '''Test the WSGI binding, hook cherrypy up.
 
-Tested on CherryPy 3.0.3
+Tested on CherryPy 3.1.2
 '''
 from diesel.protocols.wsgi import WSGIApplication
 import cherrypy
 
-print "Note.. on CherryPy 3.0.3, KeyboardInterrupt doesn't work."
-print "You'll need to Ctl-Z and kill the job manually"
-
 class Root(object):
     @cherrypy.expose
     def index(self):
         return "Hello, World!"
 
+cherrypy.config.update({'environment' : 'production'})
+
 root = cherrypy.tree.mount(Root(), '/')
-cherrypy.engine.start(blocking=False)
-
 app = WSGIApplication(root, port=7080)
 app.run()

File examples/http_client.py

     heads.set('Host', 'www.jamwt.com')
     print (yield client.request('GET', '/Py-TOC/', heads))
     print (yield client.request('GET', '/', heads))
+    a.halt()
 
 a = Application()
 a.add_loop(Loop(req_loop))

File examples/http_static.py

 
 def static_http(req):
     ct = 'text/plain'
-    if req.cmd != 'GET':
+    if req.method != 'GET':
         content = 'Method unsupported'
         code = 501
     else:
 
     headers.add('Content-Type', ct)
     
-    log.info('%s %s %s' % (req.cmd, req.url, code))
+    log.info('%s %s %s' % (req.method, req.url, code))
     return http.http_response(req, code, headers, content)
 
 app = Application()