diff --git a/examples/example.py b/examples/example.py index fc11c78..dff3a35 100644 --- a/examples/example.py +++ b/examples/example.py @@ -4,16 +4,19 @@ from asyncio import coroutine, sleep import argparse + # simplest handler taking no arguments def hi(): return 'hi' + @coroutine def hi3(response): yield from response.send_headers(length=3) response.write('hi3') yield from response.close() + # coroutine that sleeps @coroutine def slow(response, t): @@ -32,6 +35,7 @@ def echo(request, response): yield from response.send_headers(length=len(body)) response.send(body) + # coroutine handler receiving arguments from re patterns in the route path @coroutine def groups(g1, g2): @@ -39,7 +43,7 @@ def groups(g1, g2): if __name__ == '__main__': logging.basicConfig(stream=sys.stdout, level=logging.ERROR, - format='%(asctime)s | %(levelname)s | %(message)s') + format='%(asctime)s | %(levelname)s | %(message)s') p = argparse.ArgumentParser() p.add_argument('-l', '--log-level', default='ERROR') @@ -53,10 +57,9 @@ def groups(g1, g2): (r'^/slow/(?P.*)', 'GET', slow), (r'^/groups/(?P.*)/(?P.*)$', 'GET', groups), (r'^/site/(?P.*)$', 'GET', Application.static, - {'doc_root':'/tmp/site'}), + {'doc_root': '/tmp/site'}), (r'^/hi3', 'GET', hi3), (r'^/.*$', 'GET', hi), - ]) + ]) a.serve('0.0.0.0', 2020, keep_alive=True) - diff --git a/microhttpd.py b/microhttpd.py index 2d27ac4..01ee136 100644 --- a/microhttpd.py +++ b/microhttpd.py @@ -14,11 +14,13 @@ log = logging.getLogger('microhttpd') + class HTTPException(Exception): def __init__(self, status_code, msg): self.status_code = status_code self.message = msg + class Request(object): TOTAL_TIME = 0 TOTAL_REQ = 0 @@ -33,7 +35,7 @@ def __init__(self, reader, can_keep_alive=False): self.start_time = time.time() self.body_consumed = False - @coroutine + @coroutine def _nextline(self): return (yield from self.reader.readline()).decode('latin-1').rstrip() @@ -68,12 +70,10 @@ def consume_headers(self): self.keep_alive = True if self.headers.get('connection') == 'close': self.keep_alive = False - + if self.keep_alive: log.debug('%s KEEP ALIVE REQUEST', self.http_version) - - @coroutine def body(self): if self.body_consumed: @@ -85,15 +85,15 @@ def body(self): b = yield from self.reader.readexactly(l) self.body_consumed = True return b - + def end(self): dur = time.time() - self.start_time Request.TOTAL_REQ += 1 Request.TOTAL_TIME += dur - log.info('%s %s %s %sms', self.status_code, self.method, - self.full_path, round(dur*1000, 2)) + log.info('%s %s %s %sms', self.status_code, self.method, + self.full_path, round(dur*1000, 2)) log.debug('avg req time %0.6f for %d requests', - Request.TOTAL_TIME / Request.TOTAL_REQ, Request.TOTAL_REQ) + Request.TOTAL_TIME / Request.TOTAL_REQ, Request.TOTAL_REQ) class Response(object): @@ -106,7 +106,7 @@ def __init__(self, writer, request, server): self.is_head_request = False self.headers_sent = False self.status_code = -1 - + @coroutine def send_headers(self, length=0, status=200, headers={}): log.debug('in send headers') @@ -119,7 +119,7 @@ def send_headers(self, length=0, status=200, headers={}): log.debug('headers written') self.headers_sent = True self.request.status_code = status - + @coroutine def send(self, d): self.write(d) @@ -132,13 +132,13 @@ def write(self, d): if isinstance(d, str): d = d.encode('utf-8') self.writer.write(d) - + @coroutine def close(self): yield from self.writer.drain() log.debug('drained') self.request.end() - + # we've completed this request self.is_sent = True @@ -149,6 +149,7 @@ def close(self): log.debug('ready for next connection') yield from self.server.recycle(self.request, self) + class HTTPServer(object): def __init__(self, application): self.c = 0 @@ -177,7 +178,7 @@ def cb_wrapper(cb, cb_kwargs, is_head=False): def f(req, res): res.is_head_request = is_head kwargs = cb_kwargs - + # figure out whether cb wants request and response objects cb_args = inspect.getargspec(cb).args log.debug('cb args=%s', cb_args) @@ -202,32 +203,33 @@ def f(req, res): log.debug('sending reply of length %d', len(r)) yield from res.send_headers(length=len(r)) yield from res.send(r) - + except HTTPException as e: - yield from self.send_error(res,e.status_code,msg=e.message) + yield from self.send_error(res, e.status_code, + msg=e.message) except ConnectionResetError as e: log.debug('connection reset by peer') except Exception as e: yield from self.send_error(res, 500, exception=e) - # callback is all wrapped up and ready to be scheduled! + # callback is all wrapped up and ready to be scheduled! return f cb = None for pattern, method, callback, kwargs in self.app.routes: m = pattern.match(path) - if not m: continue - + if not m: + continue cb_kwargs = m.groupdict() cb_kwargs.update(kwargs) - + if meth.lower() == method.lower(): cb = cb_wrapper(callback, cb_kwargs) elif meth.lower() == 'head': cb = cb_wrapper(callback, cb_kwargs, is_head=True) if cb: - break # we matched one + break # we matched one return cb @coroutine @@ -252,7 +254,7 @@ def handle(self, reader, writer, reused=False): def recycle(self, request, response): # ok, recycle this reader and writer for a new connection # used for keep alive requests - + # first make sure we consumed the body log.debug('recycling') if not request.body_consumed: @@ -261,14 +263,14 @@ def recycle(self, request, response): log.debug('done') Task(self.handle(request.reader, response.writer, True)) - + @coroutine def send_error(self, response, status=500, msg='', exception=None): log.debug('sending %s', status) - yield from response.send_headers(status=status,length=len(msg)) + yield from response.send_headers(status=status, length=len(msg)) yield from response.send(msg) - if exception: log.exception(exception) - + if exception: + log.exception(exception) def client_connected(self, client_reader, client_writer): # a new client connected, our reader _ReaderWrapper, will let @@ -278,14 +280,16 @@ def client_connected(self, client_reader, client_writer): Task(self.handle(client_reader, client_writer)) + class Application(object): def __init__(self, routes): self.routes = [] for route in routes: - pattern, meth, cb = route[:3] - if len(route) == 4: kwargs = route[3] - else: kwargs = {} - + pattern, meth, cb = route[:3] + if len(route) == 4: + kwargs = route[3] + else: + kwargs = {} self.routes.append((re.compile(pattern), meth, cb, kwargs)) @classmethod @@ -299,7 +303,7 @@ def static(cls, doc_root, response, path): L = os.stat(f).st_size yield from response.send_headers(length=L) log.debug('serving %s size %d', f, L) - + with open(f, 'rb') as the_file: while True: data = the_file.read(chunk_size) @@ -310,5 +314,3 @@ def static(cls, doc_root, response, path): def serve(self, host, port, **kwargs): HTTPServer(self).serve(host, port, **kwargs) - - diff --git a/setup.py b/setup.py index 7a62ee8..acd2a9f 100644 --- a/setup.py +++ b/setup.py @@ -10,8 +10,9 @@ author='Rob Tandy', author_email='rob.tandy@gmail.com', url='https://github.com/robtandy/microhttpd', + description="A small, fast HTTP server based around asyncio", long_description=""" A simple, small and fast web server based around asyncio. """, py_modules=['microhttpd'], -) + )