├── __init__.py ├── examples ├── __init__.py ├── media │ ├── default.css │ └── itty.png ├── sample_conf.py ├── hello_world.py ├── using_a_config.py ├── using_get_data.py ├── detail_on_exceptions.py ├── run_under_modwsgi.py ├── html │ ├── simple_post.html │ ├── upload.html │ └── complex_post.html ├── auto_environ_access.py ├── http_cookie_support.py ├── http_secure_cookie_support.py ├── web_service.py ├── alternate_servers.py ├── sending_json_or_xml.py ├── posting_data.py ├── uploading_data.py ├── http_header_support.py ├── error_handling.py └── static_files.py ├── setup.cfg ├── .gitignore ├── setup.py ├── AUTHORS ├── README.rst ├── benchmarks.rst ├── LICENSE └── itty.py /__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.pyc 3 | .DS_Store 4 | MANIFEST 5 | dist/ 6 | -------------------------------------------------------------------------------- /examples/media/default.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /examples/media/itty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/toastdriven/itty/HEAD/examples/media/itty.png -------------------------------------------------------------------------------- /examples/sample_conf.py: -------------------------------------------------------------------------------- 1 | # itty Config 2 | host = 'localhost' 3 | port = 8080 4 | server = 'wsgiref' -------------------------------------------------------------------------------- /examples/hello_world.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/') 4 | def index(request): 5 | return 'Hello World!' 6 | 7 | run_itty() 8 | -------------------------------------------------------------------------------- /examples/using_a_config.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/') 4 | def index(request): 5 | return 'Hello World!' 6 | 7 | run_itty(config='sample_conf') 8 | -------------------------------------------------------------------------------- /examples/using_get_data.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/') 4 | def test_get(request): 5 | return "'foo' is: %s" % request.GET.get('foo', 'not specified') 6 | 7 | run_itty() 8 | -------------------------------------------------------------------------------- /examples/detail_on_exceptions.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/test_500') 4 | def test_500(request): 5 | raise RuntimeError('Oops.') 6 | return 'This should never happen either.' 7 | 8 | run_itty() 9 | -------------------------------------------------------------------------------- /examples/run_under_modwsgi.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | # Your application code. 4 | @get('/') 5 | def hello(request): 6 | return 'Hello World!' 7 | 8 | # The hook to make it run in a mod_wsgi environment. 9 | def application(environ, start_response): 10 | return handle_request(environ, start_response) 11 | -------------------------------------------------------------------------------- /examples/html/simple_post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Simple Post 4 | 5 | 6 | 7 |
8 |
9 | 10 |
11 | 12 | -------------------------------------------------------------------------------- /examples/auto_environ_access.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/') 4 | def index(request): 5 | try: 6 | # Should raise an error. 7 | return 'What? Somehow found a remote user: %s' % request.REMOTE_USER 8 | except KeyError: 9 | pass 10 | 11 | return "Remote Addr: '%s' & GET name: '%s'" % (request.REMOTE_ADDR, request.GET.get('name', 'Not found')) 12 | 13 | run_itty() 14 | -------------------------------------------------------------------------------- /examples/html/upload.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Upload 4 | 5 | 6 | 7 |
8 |
9 |
12 | 13 |
14 | 15 | -------------------------------------------------------------------------------- /examples/http_cookie_support.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 *-* 2 | from itty import * 3 | 4 | @get('/receive') 5 | def receive_cookies(request): 6 | response = Response(repr(request.cookies), content_type='text/plain') 7 | return response 8 | 9 | @get('/send') 10 | def response_cookies(request): 11 | response = Response('Check your cookies.') 12 | response.set_cookie('foo', 'bar') 13 | response.set_cookie('session', 'asdfjlasdfjsdfkjgsdfogd') 14 | return response 15 | 16 | run_itty() 17 | -------------------------------------------------------------------------------- /examples/http_secure_cookie_support.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 *-* 2 | from itty import * 3 | 4 | @get('/receive') 5 | def receive_cookies(request): 6 | response = Response(str(request.get_secure_cookie('foo')), 7 | content_type='text/plain') 8 | return response 9 | 10 | @get('/send') 11 | def response_cookies(request): 12 | response = Response('Check your cookies.') 13 | response.set_secure_cookie('foo', 'bar') 14 | return response 15 | 16 | run_itty(cookie_secret='MySeCrEtCoOkIe') 17 | -------------------------------------------------------------------------------- /examples/web_service.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/get/(?P\w+)') 4 | def test_get(request, name=', world'): 5 | return 'Hello %s!' % name 6 | 7 | @post('/post') 8 | def test_post(request): 9 | return "'foo' is: %s" % request.POST.get('foo', 'not specified') 10 | 11 | @put('/put') 12 | def test_put(request): 13 | return "'foo' is: %s" % request.PUT.get('foo', 'not specified') 14 | 15 | @delete('/delete') 16 | def test_delete(request): 17 | return 'Method received was %s.' % request.method 18 | 19 | run_itty() 20 | -------------------------------------------------------------------------------- /examples/alternate_servers.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/') 4 | def index(request): 5 | return 'Hello World!' 6 | 7 | run_itty() 8 | # Same as above: run_itty(server='wsgiref') 9 | 10 | # Other options: 11 | # run_itty(server='tornado') 12 | # run_itty(server='diesel') 13 | # run_itty(server='twisted') 14 | # run_itty(server='appengine') 15 | # run_itty(server='cherrypy') 16 | # run_itty(server='flup') 17 | # run_itty(server='paste') 18 | # run_itty(server='gunicorn') 19 | # run_itty(server='gevent') 20 | # run_itty(server='eventlet') 21 | -------------------------------------------------------------------------------- /examples/sending_json_or_xml.py: -------------------------------------------------------------------------------- 1 | import json # or ``import simplejson as json`` for < Python 2.6 2 | import xml.etree.ElementTree as etree # or other ElementTree variants for < Python 2.5 3 | from itty import * 4 | 5 | @get('/json') 6 | def send_json(request): 7 | return Response(json.dumps({'foo': 'bar', 'moof': 123}), content_type='application/json') 8 | 9 | @get('/xml') 10 | def send_xml(request): 11 | xml = etree.Element('doc') 12 | foo = etree.SubElement(xml, 'foo', value='bar') 13 | foo = etree.SubElement(xml, 'moof', value='123') 14 | return Response(etree.tostring(xml), content_type='application/xml') 15 | 16 | run_itty() 17 | -------------------------------------------------------------------------------- /examples/html/complex_post.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Complex Post 4 | 5 | 6 | 7 |
8 |
9 |
16 | 17 |
18 | 19 | -------------------------------------------------------------------------------- /examples/posting_data.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/simple_post') 4 | def simple_post(request): 5 | return open('examples/html/simple_post.html', 'r').read() 6 | 7 | @post('/test_post') 8 | def test_post(request): 9 | return "'foo' is: %s" % request.POST.get('foo', 'not specified') 10 | 11 | @get('/complex_post') 12 | def complex_post(request): 13 | return open('examples/html/complex_post.html', 'r').read() 14 | 15 | @post('/test_complex_post') 16 | def test_complex_post(request): 17 | html = """ 18 | 'foo' is: %s
19 | 'bar' is: %s 20 | """ % (request.POST.get('foo', 'not specified'), request.POST.get('bar', 'not specified')) 21 | return html 22 | 23 | run_itty() 24 | -------------------------------------------------------------------------------- /examples/uploading_data.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @get('/upload') 4 | def upload(request): 5 | return open('examples/html/upload.html', 'r').read() 6 | 7 | @post('/test_upload') 8 | def test_upload(request): 9 | myfilename = '' 10 | 11 | if request.POST['myfile'].filename: 12 | myfilename = request.POST['myfile'].filename 13 | myfile_contents = request.POST['myfile'].file.read() 14 | uploaded_file = open(myfilename, 'w') 15 | uploaded_file.write(myfile_contents) 16 | uploaded_file.close() 17 | 18 | html = """ 19 | 'foo' is: %s
20 | 'bar' is: %s 21 | """ % (request.POST.get('foo', 'not specified'), myfilename) 22 | return html 23 | 24 | run_itty() 25 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import os 3 | 4 | try: 5 | from setuptools import setup 6 | except ImportError: 7 | from distutils.core import setup 8 | 9 | long_desc = '' 10 | 11 | try: 12 | long_desc = os.path.join(os.path.dirname(__file__), 'README.rst').read() 13 | except: 14 | # The description isn't worth a failed install... 15 | pass 16 | 17 | setup( 18 | name='itty', 19 | version='0.8.2', 20 | description='The itty-bitty Python web framework.', 21 | long_description=long_desc, 22 | author='Daniel Lindsley', 23 | author_email='daniel@toastdriven.com', 24 | url='http://github.com/toastdriven/itty/', 25 | py_modules=['itty'], 26 | license='BSD', 27 | classifiers=[ 28 | 'License :: OSI Approved :: BSD License' 29 | ], 30 | ) 31 | -------------------------------------------------------------------------------- /examples/http_header_support.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | from itty import * 3 | 4 | @get('/ct') 5 | def ct(request): 6 | response = Response('Check your Content-Type headers.', content_type='text/plain') 7 | return response 8 | 9 | @get('/headers') 10 | def test_headers(request): 11 | headers = [ 12 | ('X-Powered-By', 'itty'), 13 | ('Set-Cookie', 'username=daniel') 14 | ] 15 | response = Response('Check your headers.', headers=headers) 16 | return response 17 | 18 | @get('/redirected') 19 | def index(request): 20 | return 'You got redirected!' 21 | 22 | @get('/test_redirect') 23 | def test_redirect(request): 24 | raise Redirect('/redirected') 25 | 26 | @get('/unicode') 27 | def unicode(request): 28 | return u'Works with Unîcødé too!' 29 | 30 | run_itty() 31 | -------------------------------------------------------------------------------- /AUTHORS: -------------------------------------------------------------------------------- 1 | Primary authors: 2 | 3 | * Daniel Lindsley (toastdriven) 4 | 5 | 6 | Contributors: 7 | 8 | * Matt Croydon (mcroydon) for various patches and suggestions. 9 | * Christian Metts (mintchaos) for various patches and suggestions. 10 | * Alex Kritikos (akrito) for the patch for the twisted server. 11 | * marly for a patch to an exception's message. 12 | * tstromberg for a patch related to printing tracebacks on errors. 13 | * mronge for a patch to the static files example. 14 | * Chris Wanstrath (defunkt) for static files patches & suggestions. 15 | * Kevin Fricovsky (montylounge) for the Tornado adapter. 16 | * Matt G (binarydud) for the Gunicorn adapter. 17 | * DrMegahertz for updating the Gunicorn adapter to 0.9. 18 | * thejustinwalsh for a patch related to serving binary files. 19 | * apgwoz for the Eventlet adapter & a query patch. 20 | -------------------------------------------------------------------------------- /examples/error_handling.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | @error(500) 4 | def my_great_500(request, exception): 5 | html_output = """ 6 | 7 | 8 | Application Error! OH NOES! 9 | 10 | 11 | 12 |

OH NOES!

13 | 14 |

Yep, you broke it.

15 | 16 |

Exception: %s

17 | 18 | 19 | """ % exception 20 | response = Response(html_output, status=500) 21 | return response.send(request._start_response) 22 | 23 | @get('/hello') 24 | def hello(request): 25 | return 'Hello errors!' 26 | 27 | @get('/test_404') 28 | def test_404(request): 29 | raise NotFound('Not here, sorry.') 30 | return 'This should never happen.' 31 | 32 | @get('/test_500') 33 | def test_500(request): 34 | raise AppError('Oops.') 35 | return 'This should never happen either.' 36 | 37 | @get('/test_other') 38 | def test_other(request): 39 | raise RuntimeError('Oops.') 40 | return 'This should never happen either either.' 41 | 42 | @get('/test_403') 43 | def test_403(request): 44 | raise Forbidden('No soup for you!') 45 | return 'This should never happen either either either.' 46 | 47 | @get('/test_redirect') 48 | def test_redirect(request): 49 | raise Redirect('/hello') 50 | 51 | run_itty() 52 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ======= 2 | itty.py 3 | ======= 4 | 5 | The itty-bitty Python web framework. 6 | 7 | ``itty.py`` is a little experiment, an attempt at a Sinatra_ influenced 8 | micro-framework that does just enough to be useful and nothing more. 9 | 10 | Currently supports: 11 | 12 | * Routing 13 | * Basic responses 14 | * Content-types 15 | * HTTP Status codes 16 | * URL Parameters 17 | * Basic GET/POST/PUT/DELETE support 18 | * User-definable error handlers 19 | * Redirect support 20 | * File uploads 21 | * Header support 22 | * Static media serving 23 | 24 | Beware! If you're looking for a proven, enterprise-ready framework, you're in 25 | the wrong place. But it sure is a lot of fun. 26 | 27 | .. _Sinatra: http://sinatrarb.com/ 28 | 29 | 30 | Example 31 | ======= 32 | 33 | :: 34 | 35 | from itty import get, run_itty 36 | 37 | @get('/') 38 | def index(request): 39 | return 'Hello World!' 40 | 41 | run_itty() 42 | 43 | See ``examples/`` for more usages. 44 | 45 | 46 | Other Sources 47 | ============= 48 | 49 | A couple of bits have been borrowed from other sources: 50 | 51 | * Django 52 | 53 | * HTTP_MAPPINGS 54 | 55 | * Armin Ronacher's blog (http://lucumr.pocoo.org/2007/5/21/getting-started-with-wsgi) 56 | 57 | * How to get started with WSGI 58 | 59 | 60 | Thanks 61 | ====== 62 | 63 | Thanks go out to Matt Croydon & Christian Metts for putting me up to this late 64 | at night. The joking around has become reality. :) -------------------------------------------------------------------------------- /benchmarks.rst: -------------------------------------------------------------------------------- 1 | ========== 2 | Benchmarks 3 | ========== 4 | 5 | There are lies, damned lies and statistics... and benchmarks. 6 | 7 | Run using ``siege`` on the ``examples/alternate_servers.py`` file. Performed on 8 | a 2008 MacBook Pro. 9 | 10 | * ``siege -c 1 -b http://localhost:8080`` 11 | * ``siege -c 10 -b http://localhost:8080`` 12 | * ``siege -c 100 -b http://localhost:8080`` 13 | 14 | +-----------+----------+------------+-------------+ 15 | | Server | 1 Client | 10 Clients | 100 Clients | 16 | +-----------+----------+------------+-------------+ 17 | | wsgiref | 2 | 17 | N/A | 18 | +-----------+----------+------------+-------------+ 19 | | twisted | 348 | 426 | 408 | 20 | +-----------+----------+------------+-------------+ 21 | | diesel | 901 | 1373 | 1295 | 22 | +-----------+----------+------------+-------------+ 23 | | tornado | 962 | 1427 | 1380 | 24 | +-----------+----------+------------+-------------+ 25 | | cherrypy | ??? | ??? | ??? | 26 | +-----------+----------+------------+-------------+ 27 | | flup | ??? | ??? | ??? | 28 | +-----------+----------+------------+-------------+ 29 | | paste | ??? | ??? | ??? | 30 | +-----------+----------+------------+-------------+ 31 | | appengine | ??? | ??? | ??? | 32 | +-----------+----------+------------+-------------+ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009, Daniel Lindsley. 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, 8 | this list of conditions and the following disclaimer. 9 | 10 | 2. Redistributions in binary form must reproduce the above copyright 11 | notice, this list of conditions and the following disclaimer in the 12 | documentation and/or other materials provided with the distribution. 13 | 14 | 3. Neither the name of itty nor the names of its contributors may be used 15 | to endorse or promote products derived from this software without 16 | specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /examples/static_files.py: -------------------------------------------------------------------------------- 1 | from itty import * 2 | 3 | MY_ROOT = os.path.join(os.path.dirname(__file__), 'media') 4 | 5 | 6 | @get('/') 7 | def index(request): 8 | return '' 9 | 10 | # To serve static files, simply setup a standard @get method. You should 11 | # capture the filename/path and get the content-type. If your media root is 12 | # different than where your ``itty.py`` lives, manually setup your root 13 | # directory as well. Finally, use the ``static_file`` helper to serve up the 14 | # file. 15 | @get('/media/(?P.+)') 16 | def my_media(request, filename): 17 | output = static_file(filename, root=MY_ROOT) 18 | return Response(output, content_type=content_type(filename)) 19 | 20 | 21 | # Alternative, if sane-ish defaults are good enough for you, you can use the 22 | # ``serve_static_file`` handler to do the heavy lifting for you. For example: 23 | @get('/simple/') 24 | def simple(request): 25 | return """ 26 | 27 | 28 | Simple CSS 29 | 30 | 31 | 32 |

Simple CSS is Simple!

33 |

Simple reset here.

34 | 35 | 36 | """ 37 | 38 | # By default, the ``serve_static_file`` will try to guess the correct content 39 | # type. If needed, you can enforce a content type by using the 40 | # ``force_content_type`` kwarg (i.e. ``force_content_type='image/jpg'`` on a 41 | # directory of user uploaded images). 42 | @get('/simple_media/(?P.+)') 43 | def simple_media(request, filename): 44 | return serve_static_file(request, filename, root=MY_ROOT) 45 | 46 | 47 | run_itty() 48 | -------------------------------------------------------------------------------- /itty.py: -------------------------------------------------------------------------------- 1 | """ 2 | The itty-bitty Python web framework. 3 | 4 | Totally ripping off Sintra, the Python way. Very useful for small applications, 5 | especially web services. Handles basic HTTP methods (PUT/DELETE too!). Errs on 6 | the side of fun and terse. 7 | 8 | 9 | Example Usage:: 10 | 11 | from itty import get, run_itty 12 | 13 | @get('/') 14 | def index(request): 15 | return 'Hello World!' 16 | 17 | run_itty() 18 | 19 | 20 | Thanks go out to Matt Croydon & Christian Metts for putting me up to this late 21 | at night. The joking around has become reality. :) 22 | """ 23 | import base64 24 | import cgi 25 | import datetime 26 | import hashlib 27 | import hmac 28 | import logging 29 | import mimetypes 30 | import numbers 31 | import os 32 | import re 33 | import StringIO 34 | import sys 35 | import time 36 | import traceback 37 | try: 38 | from urlparse import parse_qs 39 | except ImportError: 40 | from cgi import parse_qs 41 | try: 42 | import Cookie 43 | except ImportError: 44 | import http.cookies as Cookie 45 | 46 | __author__ = 'Daniel Lindsley' 47 | __version__ = ('0', '8', '2') 48 | __license__ = 'BSD' 49 | 50 | 51 | REQUEST_MAPPINGS = { 52 | 'GET': [], 53 | 'POST': [], 54 | 'PUT': [], 55 | 'DELETE': [], 56 | } 57 | 58 | ERROR_HANDLERS = {} 59 | 60 | MEDIA_ROOT = os.path.join(os.path.dirname(__file__), 'media') 61 | 62 | HTTP_MAPPINGS = { 63 | 100: 'CONTINUE', 64 | 101: 'SWITCHING PROTOCOLS', 65 | 200: 'OK', 66 | 201: 'CREATED', 67 | 202: 'ACCEPTED', 68 | 203: 'NON-AUTHORITATIVE INFORMATION', 69 | 204: 'NO CONTENT', 70 | 205: 'RESET CONTENT', 71 | 206: 'PARTIAL CONTENT', 72 | 300: 'MULTIPLE CHOICES', 73 | 301: 'MOVED PERMANENTLY', 74 | 302: 'FOUND', 75 | 303: 'SEE OTHER', 76 | 304: 'NOT MODIFIED', 77 | 305: 'USE PROXY', 78 | 306: 'RESERVED', 79 | 307: 'TEMPORARY REDIRECT', 80 | 400: 'BAD REQUEST', 81 | 401: 'UNAUTHORIZED', 82 | 402: 'PAYMENT REQUIRED', 83 | 403: 'FORBIDDEN', 84 | 404: 'NOT FOUND', 85 | 405: 'METHOD NOT ALLOWED', 86 | 406: 'NOT ACCEPTABLE', 87 | 407: 'PROXY AUTHENTICATION REQUIRED', 88 | 408: 'REQUEST TIMEOUT', 89 | 409: 'CONFLICT', 90 | 410: 'GONE', 91 | 411: 'LENGTH REQUIRED', 92 | 412: 'PRECONDITION FAILED', 93 | 413: 'REQUEST ENTITY TOO LARGE', 94 | 414: 'REQUEST-URI TOO LONG', 95 | 415: 'UNSUPPORTED MEDIA TYPE', 96 | 416: 'REQUESTED RANGE NOT SATISFIABLE', 97 | 417: 'EXPECTATION FAILED', 98 | 500: 'INTERNAL SERVER ERROR', 99 | 501: 'NOT IMPLEMENTED', 100 | 502: 'BAD GATEWAY', 101 | 503: 'SERVICE UNAVAILABLE', 102 | 504: 'GATEWAY TIMEOUT', 103 | 505: 'HTTP VERSION NOT SUPPORTED', 104 | } 105 | 106 | 107 | class RequestError(Exception): 108 | """A base exception for HTTP errors to inherit from.""" 109 | status = 404 110 | 111 | def __init__(self, message, hide_traceback=False): 112 | super(RequestError, self).__init__(message) 113 | self.hide_traceback = hide_traceback 114 | 115 | 116 | class Forbidden(RequestError): 117 | status = 403 118 | 119 | 120 | class NotFound(RequestError): 121 | status = 404 122 | 123 | def __init__(self, message, hide_traceback=True): 124 | super(NotFound, self).__init__(message) 125 | self.hide_traceback = hide_traceback 126 | 127 | 128 | class AppError(RequestError): 129 | status = 500 130 | 131 | 132 | class Redirect(RequestError): 133 | """ 134 | Redirects the user to a different URL. 135 | 136 | Slightly different than the other HTTP errors, the Redirect is less 137 | 'OMG Error Occurred' and more 'let's do something exceptional'. When you 138 | redirect, you break out of normal processing anyhow, so it's a very similar 139 | case.""" 140 | status = 302 141 | url = '' 142 | 143 | def __init__(self, url): 144 | self.url = url 145 | self.args = ["Redirecting to '%s'..." % self.url] 146 | 147 | 148 | class lazyproperty(object): 149 | """A property whose value is computed only once. """ 150 | def __init__(self, function): 151 | self._function = function 152 | 153 | def __get__(self, obj, _=None): 154 | if obj is None: 155 | return self 156 | 157 | value = self._function(obj) 158 | setattr(obj, self._function.func_name, value) 159 | return value 160 | 161 | if hasattr(hmac, 'compare_digest'): # python 3.3 162 | _time_independent_equals = hmac.compare_digest 163 | else: 164 | def _time_independent_equals(a, b): 165 | if len(a) != len(b): 166 | return False 167 | result = 0 168 | if isinstance(a[0], int): # python3 byte strings 169 | for x, y in zip(a, b): 170 | result |= x ^ y 171 | else: # python2 172 | for x, y in zip(a, b): 173 | result |= ord(x) ^ ord(y) 174 | return result == 0 175 | 176 | if type('') is not type(b''): 177 | def u(s): 178 | return s 179 | bytes_type = bytes 180 | unicode_type = str 181 | basestring_type = str 182 | else: 183 | def u(s): 184 | return s.decode('unicode_escape') 185 | bytes_type = str 186 | unicode_type = unicode 187 | basestring_type = basestring 188 | 189 | _UTF8_TYPES = (bytes_type, type(None)) 190 | 191 | 192 | def utf8(value): 193 | """Converts a string argument to a byte string. 194 | """ 195 | if isinstance(value, _UTF8_TYPES): 196 | return value 197 | assert isinstance(value, unicode_type) 198 | return value.encode("utf-8") 199 | 200 | _TO_UNICODE_TYPES = (unicode_type, type(None)) 201 | 202 | 203 | def to_unicode(value): 204 | """Converts a string argument to a unicode string. 205 | """ 206 | if isinstance(value, _TO_UNICODE_TYPES): 207 | return value 208 | assert isinstance(value, bytes_type) 209 | return value.decode("utf-8") 210 | 211 | _unicode = to_unicode 212 | 213 | if str is unicode_type: 214 | native_str = to_unicode 215 | else: 216 | native_str = utf8 217 | 218 | 219 | def format_timestamp(ts): 220 | """Formats a timestamp in the format used by HTTP. 221 | """ 222 | if isinstance(ts, (tuple, time.struct_time)): 223 | pass 224 | elif isinstance(ts, datetime.datetime): 225 | ts = ts.utctimetuple() 226 | elif isinstance(ts, numbers.Real): 227 | ts = time.gmtime(ts) 228 | else: 229 | raise TypeError("unknown timestamp type: %r" % ts) 230 | return time.strftime("%a, %d %b %Y %H:%M:%S GMT", ts) 231 | 232 | 233 | def create_signed_value(secret, name, value): 234 | timestamp = utf8(str(int(time.time()))) 235 | value = base64.b64encode(utf8(value)) 236 | signature = _create_signature(secret, name, value, timestamp) 237 | value = b"|".join([value, timestamp, signature]) 238 | return value 239 | 240 | 241 | def decode_signed_value(secret, name, value, max_age_days=31): 242 | if not value: 243 | return None 244 | parts = utf8(value).split(b"|") 245 | if len(parts) != 3: 246 | return None 247 | signature = _create_signature(secret, name, parts[0], parts[1]) 248 | if not _time_independent_equals(parts[2], signature): 249 | logging.warning("Invalid cookie signature %r", value) 250 | return None 251 | timestamp = int(parts[1]) 252 | if timestamp < time.time() - max_age_days * 86400: 253 | logging.warning("Expired cookie %r", value) 254 | return None 255 | if timestamp > time.time() + 31 * 86400: 256 | # _cookie_signature does not hash a delimiter between the 257 | # parts of the cookie, so an attacker could transfer trailing 258 | # digits from the payload to the timestamp without altering the 259 | # signature. For backwards compatibility, sanity-check timestamp 260 | # here instead of modifying _cookie_signature. 261 | logging.warning("Cookie timestamp in future; possible tampering %r", value) 262 | return None 263 | if parts[1].startswith(b"0"): 264 | logging.warning("Tampered cookie %r", value) 265 | return None 266 | try: 267 | return base64.b64decode(parts[0]) 268 | except Exception: 269 | return None 270 | 271 | 272 | def _create_signature(secret, *parts): 273 | hash = hmac.new(utf8(secret), digestmod=hashlib.sha1) 274 | for part in parts: 275 | hash.update(utf8(part)) 276 | return utf8(hash.hexdigest()) 277 | 278 | 279 | class HTTPHeaders(dict): 280 | """A dictionary that maintains Http-Header-Case for all keys. 281 | """ 282 | def __init__(self, *args, **kwargs): 283 | dict.__init__(self) 284 | self._as_list = {} 285 | self._last_key = None 286 | if (len(args) == 1 and len(kwargs) == 0 and 287 | isinstance(args[0], HTTPHeaders)): 288 | for k, v in args[0].get_all(): 289 | self.add(k, v) 290 | else: 291 | self.update(*args, **kwargs) 292 | 293 | def add(self, name, value): 294 | """Adds a new value for the given key.""" 295 | norm_name = HTTPHeaders._normalize_name(name) 296 | self._last_key = norm_name 297 | if norm_name in self: 298 | dict.__setitem__(self, norm_name, 299 | native_str(self[norm_name]) + ',' + 300 | native_str(value)) 301 | self._as_list[norm_name].append(value) 302 | else: 303 | self[norm_name] = value 304 | 305 | def get_list(self, name): 306 | """Returns all values for the given header as a list.""" 307 | norm_name = HTTPHeaders._normalize_name(name) 308 | return self._as_list.get(norm_name, []) 309 | 310 | def get_all(self): 311 | """Returns an iterable of all (name, value) pairs. 312 | 313 | If a header has multiple values, multiple pairs will be 314 | returned with the same name. 315 | """ 316 | for name, list in self._as_list.items(): 317 | for value in list: 318 | yield (name, value) 319 | 320 | def parse_line(self, line): 321 | """Updates the dictionary with a single header line. 322 | """ 323 | if line[0].isspace(): 324 | # continuation of a multi-line header 325 | new_part = ' ' + line.lstrip() 326 | self._as_list[self._last_key][-1] += new_part 327 | dict.__setitem__(self, self._last_key, 328 | self[self._last_key] + new_part) 329 | else: 330 | name, value = line.split(":", 1) 331 | self.add(name, value.strip()) 332 | 333 | @classmethod 334 | def parse(cls, headers): 335 | """Returns a dictionary from HTTP header text. 336 | """ 337 | h = cls() 338 | for line in headers.splitlines(): 339 | if line: 340 | h.parse_line(line) 341 | return h 342 | 343 | def __setitem__(self, name, value): 344 | norm_name = HTTPHeaders._normalize_name(name) 345 | dict.__setitem__(self, norm_name, value) 346 | self._as_list[norm_name] = [value] 347 | 348 | def __getitem__(self, name): 349 | return dict.__getitem__(self, HTTPHeaders._normalize_name(name)) 350 | 351 | def __delitem__(self, name): 352 | norm_name = HTTPHeaders._normalize_name(name) 353 | dict.__delitem__(self, norm_name) 354 | del self._as_list[norm_name] 355 | 356 | def __contains__(self, name): 357 | norm_name = HTTPHeaders._normalize_name(name) 358 | return dict.__contains__(self, norm_name) 359 | 360 | def get(self, name, default=None): 361 | return dict.get(self, HTTPHeaders._normalize_name(name), default) 362 | 363 | def update(self, *args, **kwargs): 364 | for k, v in dict(*args, **kwargs).items(): 365 | self[k] = v 366 | 367 | def copy(self): 368 | return HTTPHeaders(self) 369 | 370 | _NORMALIZED_HEADER_RE = re.compile( 371 | r'^[A-Z0-9][a-z0-9]*(-[A-Z0-9][a-z0-9]*)*$') 372 | _normalized_headers = {} 373 | 374 | @staticmethod 375 | def _normalize_name(name): 376 | """Converts a name to Http-Header-Case. 377 | """ 378 | try: 379 | return HTTPHeaders._normalized_headers[name] 380 | except KeyError: 381 | if HTTPHeaders._NORMALIZED_HEADER_RE.match(name): 382 | normalized = name 383 | else: 384 | normalized = "-".join( 385 | [w.capitalize() for w in name.split("-")]) 386 | HTTPHeaders._normalized_headers[name] = normalized 387 | return normalized 388 | 389 | 390 | class Request(object): 391 | """An object to wrap the environ bits in a friendlier way.""" 392 | GET = {} 393 | 394 | def __init__(self, environ, start_response): 395 | self._environ = environ 396 | self._start_response = start_response 397 | self.setup_self() 398 | 399 | def setup_self(self): 400 | self.path = add_slash(self._environ.get('PATH_INFO', '')) 401 | self.method = self._environ.get('REQUEST_METHOD', 'GET').upper() 402 | self.query = self._environ.get('QUERY_STRING', '') 403 | self.content_length = 0 404 | self.headers = HTTPHeaders() 405 | if self._environ.get("CONTENT_TYPE"): 406 | self.headers["Content-Type"] = self._environ["CONTENT_TYPE"] 407 | if self._environ.get("CONTENT_LENGTH"): 408 | self.headers["Content-Length"] = self._environ["CONTENT_LENGTH"] 409 | for key in self._environ: 410 | if key.startswith("HTTP_"): 411 | self.headers[key[5:].replace("_", "-")] = self._environ[key] 412 | try: 413 | self.content_length = int(self._environ.get('CONTENT_LENGTH', '0')) 414 | except ValueError: 415 | pass 416 | 417 | self.GET = self.build_get_dict() 418 | 419 | def __getattr__(self, name): 420 | """ 421 | Allow accesses of the environment if we don't already have an attribute 422 | for. This lets you do things like:: 423 | 424 | script_name = request.SCRIPT_NAME 425 | """ 426 | return self._environ[name] 427 | 428 | @lazyproperty 429 | def POST(self): 430 | return self.build_complex_dict() 431 | 432 | @lazyproperty 433 | def PUT(self): 434 | return self.build_complex_dict() 435 | 436 | @lazyproperty 437 | def body(self): 438 | """Content of the request.""" 439 | return self._environ['wsgi.input'].read(self.content_length) 440 | 441 | @property 442 | def cookies(self): 443 | """A dictionary of Cookie.Morsel objects.""" 444 | if not hasattr(self, "_cookies"): 445 | self._cookies = Cookie.SimpleCookie() 446 | if "Cookie" in self.headers: 447 | try: 448 | self._cookies.load( 449 | native_str(self.headers["Cookie"])) 450 | except Exception: 451 | self._cookies = None 452 | return self._cookies 453 | 454 | def get_cookie(self, name, default=None): 455 | """Gets the value of the cookie with the given name, else default.""" 456 | if self.cookies is not None and name in self.cookies: 457 | return self.cookies[name].value 458 | return default 459 | 460 | def get_secure_cookie(self, name, value=None, max_age_days=31): 461 | """Returns the given signed cookie if it validates, or None. 462 | """ 463 | if value is None: 464 | value = self.get_cookie(name) 465 | return decode_signed_value(COOKIE_SECRET, name, value, 466 | max_age_days=max_age_days) 467 | 468 | def build_get_dict(self): 469 | """Takes GET data and rips it apart into a dict.""" 470 | raw_query_dict = parse_qs(self.query, keep_blank_values=1) 471 | query_dict = {} 472 | 473 | for key, value in raw_query_dict.items(): 474 | if len(value) <= 1: 475 | query_dict[key] = value[0] 476 | else: 477 | # Since it's a list of multiple items, we must have seen more than 478 | # one item of the same name come in. Store all of them. 479 | query_dict[key] = value 480 | 481 | return query_dict 482 | 483 | def build_complex_dict(self): 484 | """Takes POST/PUT data and rips it apart into a dict.""" 485 | raw_data = cgi.FieldStorage(fp=StringIO.StringIO(self.body), environ=self._environ) 486 | query_dict = {} 487 | 488 | for field in raw_data: 489 | if isinstance(raw_data[field], list): 490 | # Since it's a list of multiple items, we must have seen more than 491 | # one item of the same name come in. Store all of them. 492 | query_dict[field] = [fs.value for fs in raw_data[field]] 493 | elif raw_data[field].filename: 494 | # We've got a file. 495 | query_dict[field] = raw_data[field] 496 | else: 497 | query_dict[field] = raw_data[field].value 498 | 499 | return query_dict 500 | 501 | 502 | class Response(object): 503 | 504 | def __init__(self, output, headers=None, status=200, content_type='text/html'): 505 | self.output = output 506 | self.content_type = content_type 507 | self.status = status 508 | self.headers = HTTPHeaders() 509 | 510 | if headers and isinstance(headers, HTTPHeaders): 511 | self.headers = headers 512 | if headers and isinstance(headers, list): 513 | for (key, value) in headers: 514 | self.headers.add(key, value) 515 | 516 | def add_header(self, key, value): 517 | self.headers.add(key, value) 518 | 519 | def set_cookie(self, name, value, domain=None, expires=None, path="/", 520 | expires_days=None, **kwargs): 521 | """Sets the given cookie name/value with the given options. 522 | """ 523 | name = native_str(name) 524 | value = native_str(value) 525 | if re.search(r"[\x00-\x20]", name + value): 526 | raise ValueError("Invalid cookie %r: %r" % (name, value)) 527 | if not hasattr(self, "_new_cookie"): 528 | self._new_cookie = Cookie.SimpleCookie() 529 | if name in self._new_cookie: 530 | del self._new_cookie[name] 531 | self._new_cookie[name] = value 532 | morsel = self._new_cookie[name] 533 | if domain: 534 | morsel["domain"] = domain 535 | if expires_days is not None and not expires: 536 | expires = datetime.datetime.utcnow() + datetime.timedelta( 537 | days=expires_days) 538 | if expires: 539 | morsel["expires"] = format_timestamp(expires) 540 | if path: 541 | morsel["path"] = path 542 | for k, v in kwargs.items(): 543 | if k == 'max_age': 544 | k = 'max-age' 545 | morsel[k] = v 546 | 547 | def clear_cookie(self, name, path="/", domain=None): 548 | """Deletes the cookie with the given name.""" 549 | expires = datetime.datetime.utcnow() - datetime.timedelta(days=365) 550 | self.set_cookie(name, value="", path=path, expires=expires, 551 | domain=domain) 552 | 553 | def clear_all_cookies(self): 554 | """Deletes all the cookies the user sent with this request.""" 555 | for name in self.request.cookies: 556 | self.clear_cookie(name) 557 | 558 | def set_secure_cookie(self, name, value, expires_days=30, **kwargs): 559 | """Signs and timestamps a cookie so it cannot be forged.""" 560 | self.set_cookie(name, self.create_signed_value(name, value), 561 | expires_days=expires_days, **kwargs) 562 | 563 | def create_signed_value(self, name, value): 564 | """Signs and timestamps a string so it cannot be forged. 565 | """ 566 | return create_signed_value(COOKIE_SECRET, name, value) 567 | 568 | def send(self, start_response): 569 | status = "%d %s" % (self.status, HTTP_MAPPINGS.get(self.status)) 570 | headers = ([('Content-Type', "%s; charset=utf-8" % self.content_type)] + 571 | [(k, v) for k, v in self.headers.iteritems()]) 572 | 573 | if hasattr(self, "_new_cookie"): 574 | for cookie in self._new_cookie.values(): 575 | headers.append(("Set-Cookie", utf8(cookie.OutputString(None)))) 576 | 577 | start_response(status, headers) 578 | 579 | if isinstance(self.output, unicode): 580 | return self.output.encode('utf-8') 581 | else: 582 | return self.output 583 | 584 | def convert_to_ascii(self, data): 585 | if isinstance(data, unicode): 586 | try: 587 | return data.encode('us-ascii') 588 | except UnicodeError, e: 589 | raise 590 | else: 591 | return str(data) 592 | 593 | 594 | def handle_request(environ, start_response): 595 | """The main handler. Dispatches to the user's code.""" 596 | try: 597 | request = Request(environ, start_response) 598 | except Exception, e: 599 | return handle_error(e) 600 | 601 | try: 602 | (re_url, url, callback), kwargs = find_matching_url(request) 603 | response = callback(request, **kwargs) 604 | except Exception, e: 605 | return handle_error(e, request) 606 | 607 | if not isinstance(response, Response): 608 | response = Response(response) 609 | 610 | return response.send(start_response) 611 | 612 | 613 | def handle_error(exception, request=None): 614 | """If an exception is thrown, deal with it and present an error page.""" 615 | if request is None: 616 | request = {'_environ': {'PATH_INFO': ''}} 617 | 618 | if not getattr(exception, 'hide_traceback', False): 619 | (e_type, e_value, e_tb) = sys.exc_info() 620 | message = "%s occurred on '%s': %s\nTraceback: %s" % ( 621 | exception.__class__, 622 | request._environ['PATH_INFO'], 623 | exception, 624 | ''.join(traceback.format_exception(e_type, e_value, e_tb)) 625 | ) 626 | request._environ['wsgi.errors'].write(message) 627 | 628 | if isinstance(exception, RequestError): 629 | status = getattr(exception, 'status', 404) 630 | else: 631 | status = 500 632 | 633 | if status in ERROR_HANDLERS: 634 | return ERROR_HANDLERS[status](request, exception) 635 | 636 | return not_found(request, exception) 637 | 638 | 639 | def find_matching_url(request): 640 | """Searches through the methods who've registed themselves with the HTTP decorators.""" 641 | if not request.method in REQUEST_MAPPINGS: 642 | raise NotFound("The HTTP request method '%s' is not supported." % request.method) 643 | 644 | for url_set in REQUEST_MAPPINGS[request.method]: 645 | match = url_set[0].search(request.path) 646 | 647 | if match is not None: 648 | return (url_set, match.groupdict()) 649 | 650 | raise NotFound("Sorry, nothing here.") 651 | 652 | 653 | def add_slash(url): 654 | """Adds a trailing slash for consistency in urls.""" 655 | if not url.endswith('/'): 656 | url = url + '/' 657 | return url 658 | 659 | 660 | def content_type(filename): 661 | """ 662 | Takes a guess at what the desired mime type might be for the requested file. 663 | 664 | Mostly only useful for static media files. 665 | """ 666 | ct = 'text/plain' 667 | ct_guess = mimetypes.guess_type(filename) 668 | 669 | if ct_guess[0] is not None: 670 | ct = ct_guess[0] 671 | 672 | return ct 673 | 674 | 675 | def static_file(filename, root=MEDIA_ROOT): 676 | """ 677 | Fetches a static file from the filesystem, relative to either the given 678 | MEDIA_ROOT or from the provided root directory. 679 | """ 680 | if filename is None: 681 | raise Forbidden("You must specify a file you'd like to access.") 682 | 683 | # Strip the '/' from the beginning/end. 684 | valid_path = filename.strip('/') 685 | 686 | # Kill off any character trying to work their way up the filesystem. 687 | valid_path = valid_path.replace('//', '/').replace('/./', '/').replace('/../', '/') 688 | 689 | desired_path = os.path.join(root, valid_path) 690 | 691 | if not os.path.exists(desired_path): 692 | raise NotFound("File does not exist.") 693 | 694 | if not os.access(desired_path, os.R_OK): 695 | raise Forbidden("You do not have permission to access this file.") 696 | 697 | ct = str(content_type(desired_path)) 698 | 699 | # Do the text types as a non-binary read. 700 | if ct.startswith('text') or ct.endswith('xml') or ct.endswith('json'): 701 | return open(desired_path, 'r').read() 702 | 703 | # Fall back to binary for everything else. 704 | return open(desired_path, 'rb').read() 705 | 706 | 707 | # Static file handler 708 | 709 | def serve_static_file(request, filename, root=MEDIA_ROOT, force_content_type=None): 710 | """ 711 | Basic handler for serving up static media files. 712 | 713 | Accepts an optional ``root`` (filepath string, defaults to ``MEDIA_ROOT``) parameter. 714 | Accepts an optional ``force_content_type`` (string, guesses if ``None``) parameter. 715 | """ 716 | file_contents = static_file(filename, root) 717 | 718 | if force_content_type is None: 719 | ct = content_type(filename) 720 | else: 721 | ct = force_content_type 722 | 723 | return Response(file_contents, content_type=ct) 724 | 725 | 726 | # Decorators 727 | 728 | def get(url): 729 | """Registers a method as capable of processing GET requests.""" 730 | def wrapped(method): 731 | # Register. 732 | re_url = re.compile("^%s$" % add_slash(url)) 733 | REQUEST_MAPPINGS['GET'].append((re_url, url, method)) 734 | return method 735 | return wrapped 736 | 737 | 738 | def post(url): 739 | """Registers a method as capable of processing POST requests.""" 740 | def wrapped(method): 741 | # Register. 742 | re_url = re.compile("^%s$" % add_slash(url)) 743 | REQUEST_MAPPINGS['POST'].append((re_url, url, method)) 744 | return method 745 | return wrapped 746 | 747 | 748 | def put(url): 749 | """Registers a method as capable of processing PUT requests.""" 750 | def wrapped(method): 751 | # Register. 752 | re_url = re.compile("^%s$" % add_slash(url)) 753 | REQUEST_MAPPINGS['PUT'].append((re_url, url, method)) 754 | new.status = 201 755 | return method 756 | return wrapped 757 | 758 | 759 | def delete(url): 760 | """Registers a method as capable of processing DELETE requests.""" 761 | def wrapped(method): 762 | # Register. 763 | re_url = re.compile("^%s$" % add_slash(url)) 764 | REQUEST_MAPPINGS['DELETE'].append((re_url, url, method)) 765 | return method 766 | return wrapped 767 | 768 | 769 | def error(code): 770 | """Registers a method for processing errors of a certain HTTP code.""" 771 | def wrapped(method): 772 | # Register. 773 | ERROR_HANDLERS[code] = method 774 | return method 775 | return wrapped 776 | 777 | 778 | # Error handlers 779 | 780 | @error(403) 781 | def forbidden(request, exception): 782 | response = Response('Forbidden', status=403, content_type='text/plain') 783 | return response.send(request._start_response) 784 | 785 | 786 | @error(404) 787 | def not_found(request, exception): 788 | response = Response('Not Found', status=404, content_type='text/plain') 789 | return response.send(request._start_response) 790 | 791 | 792 | @error(500) 793 | def app_error(request, exception): 794 | response = Response('Application Error', status=500, content_type='text/plain') 795 | return response.send(request._start_response) 796 | 797 | 798 | @error(302) 799 | def redirect(request, exception): 800 | response = Response('', status=302, content_type='text/plain', headers=[('Location', exception.url)]) 801 | return response.send(request._start_response) 802 | 803 | 804 | # Servers Adapters 805 | 806 | def wsgiref_adapter(host, port): 807 | from wsgiref.simple_server import make_server 808 | srv = make_server(host, port, handle_request) 809 | srv.serve_forever() 810 | 811 | 812 | def appengine_adapter(host, port): 813 | from google.appengine.ext.webapp import util 814 | util.run_wsgi_app(handle_request) 815 | 816 | 817 | def cherrypy_adapter(host, port): 818 | # Experimental (Untested). 819 | from cherrypy import wsgiserver 820 | server = wsgiserver.CherryPyWSGIServer((host, port), handle_request) 821 | server.start() 822 | 823 | 824 | def flup_adapter(host, port): 825 | # Experimental (Untested). 826 | from flup.server.fcgi import WSGIServer 827 | WSGIServer(handle_request, bindAddress=(host, port)).run() 828 | 829 | 830 | def paste_adapter(host, port): 831 | # Experimental (Untested). 832 | from paste import httpserver 833 | httpserver.serve(handle_request, host=host, port=str(port)) 834 | 835 | 836 | def twisted_adapter(host, port): 837 | from twisted.web import server, wsgi 838 | from twisted.python.threadpool import ThreadPool 839 | from twisted.internet import reactor 840 | 841 | thread_pool = ThreadPool() 842 | thread_pool.start() 843 | reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) 844 | 845 | ittyResource = wsgi.WSGIResource(reactor, thread_pool, handle_request) 846 | site = server.Site(ittyResource) 847 | reactor.listenTCP(port, site) 848 | reactor.run() 849 | 850 | 851 | def diesel_adapter(host, port): 852 | # Experimental (Mostly untested). 853 | from diesel.protocols.wsgi import WSGIApplication 854 | app = WSGIApplication(handle_request, port=int(port)) 855 | app.run() 856 | 857 | 858 | def tornado_adapter(host, port): 859 | # Experimental (Mostly untested). 860 | from tornado.wsgi import WSGIContainer 861 | from tornado.httpserver import HTTPServer 862 | from tornado.ioloop import IOLoop 863 | 864 | container = WSGIContainer(handle_request) 865 | http_server = HTTPServer(container) 866 | http_server.listen(port) 867 | IOLoop.instance().start() 868 | 869 | 870 | def gunicorn_adapter(host, port): 871 | from gunicorn import version_info 872 | 873 | if version_info < (0, 9, 0): 874 | from gunicorn.arbiter import Arbiter 875 | from gunicorn.config import Config 876 | arbiter = Arbiter(Config({'bind': "%s:%d" % (host, int(port)), 'workers': 4}), handle_request) 877 | arbiter.run() 878 | else: 879 | from gunicorn.app.base import Application 880 | 881 | class IttyApplication(Application): 882 | def init(self, parser, opts, args): 883 | return { 884 | 'bind': '{0}:{1}'.format(host, port), 885 | 'workers': 4 886 | } 887 | 888 | def load(self): 889 | return handle_request 890 | 891 | IttyApplication().run() 892 | 893 | 894 | def gevent_adapter(host, port): 895 | from gevent import pywsgi 896 | pywsgi.WSGIServer((host, int(port)), handle_request).serve_forever() 897 | 898 | 899 | def eventlet_adapter(host, port): 900 | from eventlet import wsgi, listen 901 | wsgi.server(listen((host, int(port))), handle_request) 902 | 903 | 904 | WSGI_ADAPTERS = { 905 | 'wsgiref': wsgiref_adapter, 906 | 'appengine': appengine_adapter, 907 | 'cherrypy': cherrypy_adapter, 908 | 'flup': flup_adapter, 909 | 'paste': paste_adapter, 910 | 'twisted': twisted_adapter, 911 | 'diesel': diesel_adapter, 912 | 'tornado': tornado_adapter, 913 | 'gunicorn': gunicorn_adapter, 914 | 'gevent': gevent_adapter, 915 | 'eventlet': eventlet_adapter, 916 | } 917 | 918 | 919 | COOKIE_SECRET = None 920 | 921 | # Server 922 | 923 | 924 | def run_itty(server='wsgiref', host='localhost', port=8080, config=None, 925 | cookie_secret=None): 926 | """ 927 | Runs the itty web server. 928 | 929 | Accepts an optional host (string), port (integer), server (string) and 930 | config (python module name/path as a string) parameters. 931 | 932 | By default, uses Python's built-in wsgiref implementation. Specify a server 933 | name from WSGI_ADAPTERS to use an alternate WSGI server. 934 | """ 935 | if not server in WSGI_ADAPTERS: 936 | raise RuntimeError("Server '%s' is not a valid server. Please choose a different server." % server) 937 | 938 | if config is not None: 939 | # We'll let ImportErrors bubble up. 940 | config_options = __import__(config) 941 | host = getattr(config_options, 'host', host) 942 | port = getattr(config_options, 'port', port) 943 | server = getattr(config_options, 'server', server) 944 | 945 | # AppEngine seems to echo everything, even though it shouldn't. Accomodate. 946 | if server != 'appengine': 947 | print 'itty starting up (using %s)...' % server 948 | print 'Listening on http://%s:%s...' % (host, port) 949 | print 'Use Ctrl-C to quit.' 950 | print 951 | 952 | global COOKIE_SECRET 953 | COOKIE_SECRET = cookie_secret or base64.b64encode(os.urandom(32)) 954 | 955 | try: 956 | WSGI_ADAPTERS[server](host, port) 957 | except KeyboardInterrupt: 958 | print 'Shutting down. Have a nice day!' 959 | --------------------------------------------------------------------------------