Skip to content

Instantly share code, notes, and snippets.

@mironside
Last active July 3, 2017 05:03
Show Gist options
  • Save mironside/09106e7725d45620cad427038903a2d9 to your computer and use it in GitHub Desktop.
Save mironside/09106e7725d45620cad427038903a2d9 to your computer and use it in GitHub Desktop.
minimal http server/router, intended for pasting into single file python scripts
#
# A minimal, zero dependency http server/router
#
# def root(req):
# return 'Hello, world'
# routes = (
# ('GET', '/', root),
# )
# http(routes, 'localhost', 8080)
#
# Routing with variable capture and default values
# def events(req, eventID=None):
# return 'eventID %s' % eventID
# ('GET', '/events', events),
# ('GET', '/events/<int:eventID>/view', events),
# ('GET', '/users/<username>', users)
#
# Response headers inferred from response type
# Route handlers return (response, content_type, status) where content_type and status are both optional and can be returned in any order
# Content-length is automatically set based on the response size
# Content-type is inferred by the response data type
# dict or list is encoded to json with Content-type: application/json
# string starting with '<html>' set Content-type: text/html
# strings set Content-type: text/plain
# any value that evaluates to False sends an empty response with Content-type: text/plain
#
# return '<html><body>Not Found</body></html>', 404
# return open('test.jpg','rb').read(), 'image/jpeg'
# return open('test.log','r').read()
# return {'a':'b', 'c':[1,2,3]}
#
# No template engine is provided but the builtin string.Template can be used to avoid additional dependencies
#
def http(routes, host, port):
import itertools, re, urllib.parse, http.server, socketserver, json
def route(self):
self.path,self.query = (lambda x: (x[0],urllib.parse.parse_qs(x[1])))((self.path.split('?') + [''])[:2])
for method,path,func in routes:
if method != self.command: continue
kwargs = {}
for r,p in itertools.zip_longest([x for x in path.split('/') if x], [x for x in self.path.split('/') if x], fillvalue=None):
if r and r[0] == '<' and r[-1] == '>': kwargs.update(dict([(lambda type,name: (name,{'int': lambda x: int(x) if x.isnumeric() else None}.get(type, lambda x: x)(p)))(*([None] + r[1:-1].split(':'))[-2:])]))
elif p != r: kwargs = None; break
if kwargs == None or None in kwargs.values(): continue
body,content_type,status = (lambda x: x+('text/plain',200) if len(x) == 1 else x+(200,) if len(x) == 2 and type(x[1]) == str else (x[0],'text/plain',x[1]) if len(x) == 2 and type(x[1]) == int else (x[0],x[2],x[1]) if type(x[1]) == int and type(x[2]) == str else x)((lambda x: x if type(x) == tuple else (x,))(func(self, **kwargs)))
body,content_type = (bytes(json.dumps(body),'utf-8'),'application/json') if type(body) == dict or type(body) == list else (bytes(body, 'utf-8'),'text/html') if type(body) == str and body.startswith('<html>') else (bytes(body, 'utf-8'),content_type) if type(body) == str else (bytes('', 'utf-8'),content_type) if not body else (body,content_type)
self.send_response(status),self.send_header('Content-type', content_type),self.send_header('Content-length', len(body)),self.end_headers(),self.wfile.write(body)
self.send_error(404)
class RequestHandler(http.server.BaseHTTPRequestHandler): pass
RequestHandler.do_GET = RequestHandler.do_PUT = RequestHandler.do_POST = RequestHandler.do_DELETE = route
class ThreadedHTTPServer(socketserver.ThreadingMixIn, http.server.HTTPServer): pass
server = ThreadedHTTPServer((host, port), RequestHandler)
server.serve_forever()
def root(req):
return '<html><b>root</b></html>'
def events(req, eventId):
return 'events'
def about(req):
return 'about'
routes = (
('GET', '/', root),
('GET', '/events/<eventId>', events),
('GET', '/events/<int:eventId>/view', events),
('GET', '/about', about),
)
if __name__ == '__main__':
http(routes, 'localhost', 8888)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment