Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 348 lines (262 sloc) 9.438 kB
e8ec060 @toastdriven Initial commit.
authored
1 """
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
2 The itty-bitty Python web framework.
e8ec060 @toastdriven Initial commit.
authored
3
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
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():
15 return 'Hello World!'
16
17 run_itty()
18
19
20 A couple of bits have been borrowed from other sources:
21
22 * Django
23 * HTTP_MAPPINGS
24 * Armin Ronacher's blog (http://lucumr.pocoo.org/2007/5/21/getting-started-with-wsgi)
25 * How to get started with WSGI
26
27
28 Thanks go out to Matt Croydon & Christian Metts for putting me up to this late
29 at night. The joking around has become reality. :)
e8ec060 @toastdriven Initial commit.
authored
30 """
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
31 import re
7d82835 @toastdriven Organization & documentation.
authored
32
e8ec060 @toastdriven Initial commit.
authored
33
34 __author__ = 'Daniel Lindsley'
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
35 __version__ = ('0', '1', '1')
e8ec060 @toastdriven Initial commit.
authored
36 __license__ = 'MIT'
37
38
39 REQUEST_MAPPINGS = {
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
40 'GET': [],
41 'POST': [],
42 'PUT': [],
43 'DELETE': [],
e8ec060 @toastdriven Initial commit.
authored
44 }
45
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
46 ERROR_HANDLERS = {}
47
f1dd532 @toastdriven You can now set content_type and status.
authored
48 HTTP_MAPPINGS = {
cccb1ac @toastdriven Now with GET parameters.
authored
49 100: '100 CONTINUE',
50 101: '101 SWITCHING PROTOCOLS',
f1dd532 @toastdriven You can now set content_type and status.
authored
51 200: '200 OK',
cccb1ac @toastdriven Now with GET parameters.
authored
52 201: '201 CREATED',
53 202: '202 ACCEPTED',
54 203: '203 NON-AUTHORITATIVE INFORMATION',
55 204: '204 NO CONTENT',
56 205: '205 RESET CONTENT',
57 206: '206 PARTIAL CONTENT',
58 300: '300 MULTIPLE CHOICES',
59 301: '301 MOVED PERMANENTLY',
60 302: '302 FOUND',
61 303: '303 SEE OTHER',
62 304: '304 NOT MODIFIED',
63 305: '305 USE PROXY',
64 306: '306 RESERVED',
65 307: '307 TEMPORARY REDIRECT',
66 400: '400 BAD REQUEST',
67 401: '401 UNAUTHORIZED',
68 402: '402 PAYMENT REQUIRED',
69 403: '403 FORBIDDEN',
f1dd532 @toastdriven You can now set content_type and status.
authored
70 404: '404 NOT FOUND',
cccb1ac @toastdriven Now with GET parameters.
authored
71 405: '405 METHOD NOT ALLOWED',
72 406: '406 NOT ACCEPTABLE',
73 407: '407 PROXY AUTHENTICATION REQUIRED',
74 408: '408 REQUEST TIMEOUT',
75 409: '409 CONFLICT',
76 410: '410 GONE',
77 411: '411 LENGTH REQUIRED',
78 412: '412 PRECONDITION FAILED',
79 413: '413 REQUEST ENTITY TOO LARGE',
80 414: '414 REQUEST-URI TOO LONG',
81 415: '415 UNSUPPORTED MEDIA TYPE',
82 416: '416 REQUESTED RANGE NOT SATISFIABLE',
83 417: '417 EXPECTATION FAILED',
84 500: '500 INTERNAL SERVER ERROR',
85 501: '501 NOT IMPLEMENTED',
86 502: '502 BAD GATEWAY',
87 503: '503 SERVICE UNAVAILABLE',
88 504: '504 GATEWAY TIMEOUT',
89 505: '505 HTTP VERSION NOT SUPPORTED',
f1dd532 @toastdriven You can now set content_type and status.
authored
90 }
91
e8ec060 @toastdriven Initial commit.
authored
92
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
93 class RequestError(Exception):
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
94 """A base exception for HTTP errors to inherit from."""
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
95 status = 404
96
97 class NotFound(RequestError):
98 status = 404
99
100 class AppError(RequestError):
101 status = 500
e8ec060 @toastdriven Initial commit.
authored
102
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
103 class Redirect(RequestError):
104 """
105 Redirects the user to a different URL.
106
107 Slightly different than the other HTTP errors, the Redirect is less
108 'OMG Error Occurred' and more 'let's do something exceptional'. When you
109 redirect, you break out of normal processing anyhow, so it's a very similar
110 case."""
111 status = 302
112 url = ''
113
114 def __init__(self, url):
115 self.url = url
116
e8ec060 @toastdriven Initial commit.
authored
117
f1dd532 @toastdriven You can now set content_type and status.
authored
118 class Request(object):
7d82835 @toastdriven Organization & documentation.
authored
119 """An object to wrap the environ bits in a friendlier way."""
cccb1ac @toastdriven Now with GET parameters.
authored
120 GET = {}
121 POST = {}
122 PUT = {}
123
f1dd532 @toastdriven You can now set content_type and status.
authored
124 def __init__(self, environ):
125 self._environ = environ
cccb1ac @toastdriven Now with GET parameters.
authored
126 self.setup_self()
127
128 def setup_self(self):
129 self.path = add_slash(self._environ.get('PATH_INFO', ''))
df960dd @toastdriven Basic POST support.
authored
130 self.method = self._environ.get('REQUEST_METHOD', 'GET').upper()
cccb1ac @toastdriven Now with GET parameters.
authored
131 self.query = self._environ.get('QUERY_STRING', '')
df960dd @toastdriven Basic POST support.
authored
132 self.content_length = 0
133
134 try:
135 self.content_length = int(self._environ.get('CONTENT_LENGTH', '0'))
136 except ValueError:
137 pass
cccb1ac @toastdriven Now with GET parameters.
authored
138
139 self.GET = build_query_dict(self.query)
df960dd @toastdriven Basic POST support.
authored
140
141 if self._environ.get('CONTENT_TYPE', '').startswith('multipart'):
142 raise Exception("Sorry, uploads are not supported.")
143
93e4039 @toastdriven Added PUT & DELETE support.
authored
144 raw_data = ''
145
146 if self.content_length != 0:
147 raw_data = self._environ['wsgi.input'].read(self.content_length)
148
df960dd @toastdriven Basic POST support.
authored
149 if self.method == 'POST':
93e4039 @toastdriven Added PUT & DELETE support.
authored
150 self.POST = build_query_dict(raw_data)
151 elif self.method == 'PUT':
152 self.PUT = build_query_dict(raw_data)
cccb1ac @toastdriven Now with GET parameters.
authored
153
154
155 def build_query_dict(query_string):
7d82835 @toastdriven Organization & documentation.
authored
156 """
157 Takes GET/POST data and rips it apart into a dict.
158
159 Expects a string of key/value pairs (i.e. foo=bar&moof=baz).
160 """
cccb1ac @toastdriven Now with GET parameters.
authored
161 pairs = query_string.split('&')
162 query_dict = {}
163 pair_re = re.compile('^(?P<key>[^=]*?)=(?P<value>.*)')
164
165 for pair in pairs:
166 match = pair_re.search(pair)
167
168 if match is not None:
169 match_data = match.groupdict()
170 query_dict[match_data['key']] = match_data['value']
171
172 return query_dict
f1dd532 @toastdriven You can now set content_type and status.
authored
173
174
e8ec060 @toastdriven Initial commit.
authored
175 def handle_request(environ, start_response):
176 """The main handler. Dispatches to the user's code."""
cccb1ac @toastdriven Now with GET parameters.
authored
177 request = Request(environ)
178
e8ec060 @toastdriven Initial commit.
authored
179 try:
cccb1ac @toastdriven Now with GET parameters.
authored
180 (re_url, url, callback), kwargs = find_matching_url(request)
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
181 output = callback(request, **kwargs)
182 except Exception, e:
183 return handle_error(e, environ, start_response)
e8ec060 @toastdriven Initial commit.
authored
184
f1dd532 @toastdriven You can now set content_type and status.
authored
185 ct = 'text/html'
186 status = 200
187
188 try:
189 ct = callback.content_type
190 except AttributeError:
191 pass
192
193 try:
194 status = callback.status
195 except AttributeError:
196 pass
197
198 start_response(HTTP_MAPPINGS.get(status), [('Content-Type', ct)])
199 return output
e8ec060 @toastdriven Initial commit.
authored
200
201
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
202 def handle_error(exception, environ, start_response):
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
203 """If an exception is thrown, deal with it and present an error page."""
204 environ['wsgi.errors'].write("Exception occurred on '%s': %s\n" % (environ['PATH_INFO'], exception[0]))
205
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
206 if isinstance(exception, RequestError):
207 status = getattr(exception, 'status', 404)
208 else:
209 status = 500
210
211 if status in ERROR_HANDLERS:
212 return ERROR_HANDLERS[status](exception, environ, start_response)
213
214 return not_found(exception, environ, start_response)
215
216
cccb1ac @toastdriven Now with GET parameters.
authored
217 def find_matching_url(request):
7d82835 @toastdriven Organization & documentation.
authored
218 """Searches through the methods who've registed themselves with the HTTP decorators."""
cccb1ac @toastdriven Now with GET parameters.
authored
219 if not request.method in REQUEST_MAPPINGS:
220 raise NotFound("The HTTP request method '%s' is not supported." % request.method)
e8ec060 @toastdriven Initial commit.
authored
221
cccb1ac @toastdriven Now with GET parameters.
authored
222 for url_set in REQUEST_MAPPINGS[request.method]:
223 match = url_set[0].search(request.path)
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
224
225 if match is not None:
226 return (url_set, match.groupdict())
e8ec060 @toastdriven Initial commit.
authored
227
228 raise NotFound("Sorry, nothing here.")
229
230
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
231 def add_slash(url):
7d82835 @toastdriven Organization & documentation.
authored
232 """Adds a trailing slash for consistency in urls."""
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
233 if not url.endswith('/'):
234 url = url + '/'
235 return url
236
237
e8ec060 @toastdriven Initial commit.
authored
238 # Decorators
239
240 def get(url):
7d82835 @toastdriven Organization & documentation.
authored
241 """Registers a method as capable of processing GET requests."""
e8ec060 @toastdriven Initial commit.
authored
242 def wrapped(method):
243 def new(*args, **kwargs):
244 return method(*args, **kwargs)
245 # Register.
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
246 re_url = re.compile("^%s$" % add_slash(url))
247 REQUEST_MAPPINGS['GET'].append((re_url, url, new))
e8ec060 @toastdriven Initial commit.
authored
248 return new
249 return wrapped
250
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
251
e8ec060 @toastdriven Initial commit.
authored
252 def post(url):
7d82835 @toastdriven Organization & documentation.
authored
253 """Registers a method as capable of processing POST requests."""
e8ec060 @toastdriven Initial commit.
authored
254 def wrapped(method):
255 def new(*args, **kwargs):
256 return method(*args, **kwargs)
257 # Register.
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
258 re_url = re.compile("^%s$" % add_slash(url))
259 REQUEST_MAPPINGS['POST'].append((re_url, url, new))
e8ec060 @toastdriven Initial commit.
authored
260 return new
261 return wrapped
262
263
93e4039 @toastdriven Added PUT & DELETE support.
authored
264 def put(url):
265 """Registers a method as capable of processing PUT requests."""
266 def wrapped(method):
267 def new(*args, **kwargs):
268 return method(*args, **kwargs)
269 # Register.
270 re_url = re.compile("^%s$" % add_slash(url))
271 REQUEST_MAPPINGS['PUT'].append((re_url, url, new))
272 new.status = 201
273 return new
274 return wrapped
275
276
277 def delete(url):
278 """Registers a method as capable of processing DELETE requests."""
279 def wrapped(method):
280 def new(*args, **kwargs):
281 return method(*args, **kwargs)
282 # Register.
283 re_url = re.compile("^%s$" % add_slash(url))
284 REQUEST_MAPPINGS['DELETE'].append((re_url, url, new))
285 return new
286 return wrapped
287
288
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
289 def error(code):
290 """Registers a method for processing errors of a certain HTTP code."""
291 def wrapped(method):
292 def new(*args, **kwargs):
293 return method(*args, **kwargs)
294 # Register.
295 ERROR_HANDLERS[code] = new
296 return new
297 return wrapped
298
299
300 # Error handlers
301
302 @error(404)
303 def not_found(exception, environ, start_response):
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
304 start_response(HTTP_MAPPINGS[404], [('Content-Type', 'text/plain')])
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
305 return ['Not Found']
306
307
308 @error(500)
309 def app_error(exception, environ, start_response):
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
310 start_response(HTTP_MAPPINGS[500], [('Content-Type', 'text/plain')])
e093fe8 @toastdriven Refactored error handling. Now allows for user-definable error handlers.
authored
311 return ['Application Error']
312
313
e73e76a @toastdriven Redirect support and 0.1.0 release!
authored
314 @error(302)
315 def redirect(exception, environ, start_response):
316 start_response(HTTP_MAPPINGS[302], [('Content-Type', 'text/plain'), ('Location', exception.url)])
317 return ['']
318
319
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
320 # Sample server
321
322 def run_itty(host='localhost', port=8080):
323 """
324 Runs the itty web server.
325
326 Accepts an optional host (string) and port (integer) parameters.
327
328 Uses Python's built-in wsgiref implementation. Easily replaced with other
329 WSGI server implementations.
330 """
331 print 'itty starting up...'
93e4039 @toastdriven Added PUT & DELETE support.
authored
332 print 'Listening on http://%s:%s...' % (host, port)
e13eab0 @toastdriven Better routing and now handles dynamic data in URLs.
authored
333 print 'Use Ctrl-C to quit.'
334 print
335
336 try:
337 from wsgiref.simple_server import make_server
338 srv = make_server(host, port, handle_request)
339 srv.serve_forever()
340 except KeyboardInterrupt:
341 print "Shuting down..."
342 import sys
343 sys.exit()
e8ec060 @toastdriven Initial commit.
authored
344
345
346 if __name__ == '__main__':
347 run_itty()
Something went wrong with that request. Please try again.