Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Comparing changes

Choose two branches to see what's changed or to start a new pull request. If you need to, you can also compare across forks.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also compare across forks.
...
compare: 555ee61307
Checking mergeability… Don't worry, you can still create the pull request.
  • 17 commits
  • 18 files changed
  • 0 commit comments
  • 2 contributors
Commits on Dec 01, 2011
@ecdavis ecdavis Added an on_overflow_error() method to _Channel and modified Stream a…
…nd Datagram to make use of it.
c493b4f
@ecdavis ecdavis Forgot to stage this rather important line in the previous commit. 399bece
@ecdavis ecdavis Modified on_connect_error so that it takes an exception object rather…
… than two arguments. Added an on_error method to _Channel. Changed calls to on_overflow_error to use _safely_call.
c5499e5
Commits on Dec 02, 2011
@ecdavis ecdavis Refined the way that channels wait for events and fixed a serious iss…
…ue with writing not occuring until another event was raised.
6539da6
Commits on Jan 11, 2012
@ecdavis ecdavis A first attempt at baking SSL/TLS support into the Stream classes. In…
…complete so far.
4e86bd7
@ecdavis ecdavis Optimisation for protocols such as HTTP in which channels only modify…
… their own events from within their _handle_events() method.
79b45a1
Commits on Jan 12, 2012
@ecdavis ecdavis Added pants.simple - a convenience module that will replace both pant…
…s.network and pants.unix when the painless changes are merged in.
869c29e
@ecdavis ecdavis Fixed up the way the end() methods work on Datagram and Stream - they…
… now have special state rather than overriding on_write.
5234b65
@ecdavis ecdavis Further SSL stuff - see comments on #24 cbcee05
Commits on Jan 13, 2012
@ecdavis ecdavis A few PEP-8 violations cleared up. d9d6309
Commits on Jan 14, 2012
@ecdavis ecdavis Added errno import to pants.stream - required by the SSL code. f348476
Commits on Jan 15, 2012
@stendec stendec Rewrote the DNS Resolver to allow multiple query types. Seems to be w…
…orking, but I haven't tested heavily yet.
3a3b0a5
@ecdavis ecdavis Engine's timer methods now return Timer instances that can be called …
…to cancel the timer. This change was made because using functools.partial() was causing strange behaviour.
d347ad5
@stendec stendec Fixes for DNS. 4482232
@stendec stendec Split pants.contrib.web into its own package, as it was getting rathe…
…r large.
209e3e4
@stendec stendec Cleaning up some of pants.contrib after playing with code analysis. 732675d
@stendec stendec Altered web slightly to make the server name in the footer into a link. 555ee61
View
4 pants/__init__.py
@@ -44,7 +44,7 @@
__version__ = "0.10.1"
__all__ = [
- "engine", # Core
+ "engine", # Core
"callback", "loop", "cycle", "defer", # Scheduling
"Datagram", "Stream", "StreamServer", # Low-level networking
"Client", "Connection", "Server", # High-level networking
@@ -52,7 +52,7 @@
if "UnixClient" in globals():
__all__.extend([
- "UnixClient", "UnixConnection", "UnixServer", # High-level networking
+ "UnixClient", "UnixConnection", "UnixServer", # High-level networking
])
###############################################################################
View
124 pants/_channel.py
@@ -112,15 +112,13 @@ def __init__(self, **kwargs):
self._socket = None
self._socket_set(sock)
- # Socket state
- self._wait_for_read_event = True
- self._wait_for_write_event = True
-
# I/O attributes
self._recv_amount = 4096
- # Events
+ # Internal state
self._events = Engine.ALL_EVENTS
+ self._currently_active = False
+
Engine.instance().add_channel(self)
##### Control Methods #####################################################
@@ -134,6 +132,9 @@ def close(self):
Engine.instance().remove_channel(self)
self._socket_close()
+ self._events = Engine.ALL_EVENTS
+ self._currently_active = False
+
self._safely_call(self.on_close)
##### Public Event Handlers ###############################################
@@ -163,20 +164,6 @@ def on_connect(self):
"""
pass
- def on_connect_error(self, err, errstr):
- """
- Placeholder. Called when the channel has failed to connect to a
- remote socket.
-
- ========= ============
- Argument Description
- ========= ============
- err The error number that was raised.
- errstr The error message.
- ========= ============
- """
- pass
-
def on_listen(self):
"""
Placeholder. Called when the channel begins listening for new
@@ -204,6 +191,51 @@ def on_close(self):
"""
pass
+ def on_connect_error(self, exception):
+ """
+ Placeholder. Called when the channel has failed to connect to a
+ remote socket.
+
+ ========== ============
+ Argument Description
+ ========== ============
+ exception The exception that was raised.
+ ========== ============
+ """
+ pass
+
+ def on_overflow_error(self, exception):
+ """
+ Placeholder. Called when an internal buffer on the channel
+ exceeds its size limit.
+
+ By default, logs the exception and closes the channel.
+
+ ========== ============
+ Argument Description
+ ========== ============
+ exception The exception that was raised.
+ ========== ============
+ """
+ log.exception(exception)
+ self.close()
+
+ def on_error(self, exception):
+ """
+ Placeholder. Generic error handler for exceptions raised on the
+ channel. Called when no specific handler exists.
+
+ By default, logs the exception and closes the channel.
+
+ ========== ============
+ Argument Description
+ ========== ============
+ exception The exception that was raised.
+ ========== ============
+ """
+ log.exception(exception)
+ self.close()
+
##### Socket Method Wrappers ##############################################
def _socket_set(self, sock):
@@ -248,7 +280,7 @@ def _socket_connect(self, addr):
return True
if result in (errno.EAGAIN, errno.EWOULDBLOCK, errno.EINPROGRESS, errno.EALREADY):
- self._wait_for_write_event = True
+ self._start_waiting_for_write_event()
return False
raise socket.error(result, strerror(result))
@@ -280,7 +312,6 @@ def _socket_listen(self, backlog):
backlog = socket.SOMAXCONN
self._socket.listen(backlog)
- self._wait_for_read_event = True
def _socket_close(self):
"""
@@ -306,7 +337,6 @@ def _socket_accept(self):
return self._socket.accept()
except socket.error, err:
if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
- self._wait_for_read_event = True
return None, None
else:
raise
@@ -322,7 +352,6 @@ def _socket_recv(self):
data = self._socket.recv(self._recv_amount)
except socket.error, err:
if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
- self._wait_for_read_event = True
return ''
elif err[0] == errno.ECONNRESET:
return None
@@ -346,7 +375,6 @@ def _socket_recvfrom(self):
data, addr = self._socket.recvfrom(self._recv_amount)
except socket.error, err:
if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK, errno.ECONNRESET):
- self._wait_for_read_event = True
return '', None
else:
raise
@@ -372,7 +400,7 @@ def _socket_send(self, data):
return self._socket.send(data)
except Exception, err:
if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
- self._wait_for_write_event = True
+ self._start_waiting_for_write_event()
return 0
elif err[0] == errno.EPIPE:
self.close()
@@ -398,7 +426,7 @@ def _socket_sendto(self, data, addr, flags=0):
return self._socket.sendto(data, flags, addr)
except Exception, err:
if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
- self._wait_for_write_event = True
+ self._start_waiting_for_write_event()
return 0
elif err[0] == errno.EPIPE:
self.close()
@@ -408,6 +436,10 @@ def _socket_sendto(self, data, addr, flags=0):
def _socket_sendfile(self, sfile, offset, nbytes):
"""
+ Send data from a file to a remote socket.
+
+ Returns the number of bytes that were sent to the socket.
+
========= ============
Argument Description
========= ============
@@ -420,7 +452,7 @@ def _socket_sendfile(self, sfile, offset, nbytes):
return sendfile(sfile, self, offset, nbytes)
except Exception, err:
if err[0] in (errno.EAGAIN, errno.EWOULDBLOCK):
- self._wait_for_write_event = True
+ self._start_waiting_for_write_event()
return 0
elif err[0] == errno.EPIPE:
self.close()
@@ -430,6 +462,26 @@ def _socket_sendfile(self, sfile, offset, nbytes):
##### Internal Methods ####################################################
+ def _start_waiting_for_write_event(self):
+ """
+ Start waiting for a write event on the channel, and update the
+ engine accordingly.
+ """
+ if self._events != self._events | Engine.WRITE:
+ self._events = self._events | Engine.WRITE
+ if not self._currently_active:
+ Engine.instance().modify_channel(self)
+
+ def _stop_waiting_for_write_event(self):
+ """
+ Stop waiting for a write event on the channel, and update the
+ engine accordingly.
+ """
+ if self._events == self._events | Engine.WRITE:
+ self._events = self._events & (self._events ^ Engine.WRITE)
+ if not self._currently_active:
+ Engine.instance().modify_channel(self)
+
def _safely_call(self, thing_to_call, *args, **kwargs):
"""
Safely execute a callable.
@@ -482,14 +534,18 @@ def _handle_events(self, events):
(self.__class__.__name__, self.fileno))
return
+ self._currently_active = True
+
+ previous_events = self._events
+ if self._events != Engine.BASE_EVENTS:
+ self._events = Engine.BASE_EVENTS
+
if events & Engine.READ:
- self._wait_for_read_event = False
self._handle_read_event()
if self._socket is None:
return
if events & Engine.WRITE:
- self._wait_for_write_event = False
self._handle_write_event()
if self._socket is None:
return
@@ -508,15 +564,11 @@ def _handle_events(self, events):
self.close()
return
- events = Engine.ERROR | Engine.HANGUP
- if self._wait_for_read_event:
- events |= Engine.READ
- if self._wait_for_write_event:
- events |= Engine.WRITE
- if events != self._events:
- self._events = events
+ if self._events != previous_events:
Engine.instance().modify_channel(self)
+ self._currently_active = False
+
def _handle_read_event(self):
"""
Handle a read event raised on the channel.
View
2  pants/contrib/http/client.py
@@ -483,7 +483,7 @@ def _read_chunk_head(self, data):
length = int(data.strip(), 16)
- if length == 0:
+ if not length:
resp = self.current_response
if resp._decompressor:
resp.body += resp._decompressor.flush()
View
10 pants/contrib/http/server.py
@@ -319,7 +319,7 @@ def __html__(self):
for i in ('headers','get','post'):
if getattr(self,i):
out += u' %-8s = {\n %s\n }\n\n' % (
- i, pprint.pformat(getattr(self, i), 8, 80)[1:-1])
+ i, pprint.pformat(getattr(self, i), 8)[1:-1])
else:
out += u' %-8s = {}\n\n' % i
@@ -332,7 +332,7 @@ def __html__(self):
out += u' cookies = {}\n\n'
out += u' files = %s\n)</pre>' % \
- pprint.pformat(self.files.keys(), 0, 80)
+ pprint.pformat(self.files.keys(), 0)
return out
##### Properties ##########################################################
@@ -397,7 +397,7 @@ def set_secure_cookie(self, name, value, expires=30*86400, **kwargs):
Additional arguments, such as ``path`` and ``httponly`` may be set by
providing them as keyword arguments.
"""
- ts = str(int(curtime()))
+ ts = str(int(time()))
v = base64.b64encode(str(value))
signature = generate_signature(
self.connection.server.cookie_secret, expires, ts, v)
@@ -428,7 +428,7 @@ def get_secure_cookie(self, name):
v = base64.b64encode(str(value))
sig = generate_signature(self.connection.server.cookie_secret, expires, ts, v)
- if signature != sig or ts < curtime() - expires or ts > curtime() + expires:
+ if signature != sig or ts < time() - expires or ts > time() + expires:
return None
return value
@@ -522,7 +522,7 @@ def send_headers(self, headers, end_headers=True, cookies=True):
append('Server: %s' % SERVER)
if cookies and hasattr(self, '_cookies'):
- self.send_cookies(end_headers=False)
+ self.send_cookies()
if end_headers:
append(CRLF)
View
2  pants/contrib/qt.py
@@ -20,8 +20,6 @@
# Imports
###############################################################################
-import functools
-
from pants.engine import Engine
try:
View
4 pants/contrib/socks.py
@@ -74,7 +74,7 @@ def do_socks_handshake(self, addr, callback, error_callback=None, auth=None):
# Build our on_read.
def on_read(data):
- if self._socks_state == 0:
+ if not self._socks_state:
if data[0] != SOCKS_VERSION:
if error_callback:
self._safely_call(error_callback,
@@ -96,7 +96,7 @@ def on_read(data):
else:
self._socks_state = 1
- self.on_read("%s\x00" % (SOCKS_VERSION))
+ self.on_read("%s\x00" % SOCKS_VERSION)
elif self._socks_state == 1:
if data[0] != SOCKS_VERSION:
View
1,421 pants/contrib/web.py
@@ -1,1421 +0,0 @@
-###############################################################################
-#
-# Copyright 2011 Pants Developers (see AUTHORS.txt)
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-# http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-#
-###############################################################################
-
-###############################################################################
-# Imports
-###############################################################################
-
-import base64
-import inspect
-import logging
-import mimetypes
-import os
-import re
-import time
-import traceback
-import urllib
-
-from datetime import datetime, timedelta
-from pants import __version__ as pants_version
-from http import CRLF, HTTP, HTTPServer, HTTPRequest, SERVER, SERVER_URL, date
-
-try:
- import simplejson as json
-except ImportError:
- import json
-
-__all__ = ('Application', 'HTTPException', 'HTTPTransparentRedirect', 'abort',
- 'all_or_404', 'error', 'redirect', 'url_for', 'HTTPServer', 'FileServer')
-
-###############################################################################
-# Cross Platform Hidden File Detection
-###############################################################################
-
-def _is_hidden(file, path):
- return file.startswith(u'.')
-
-if os.name == 'nt':
- try:
- import win32api, win32con
- def _is_hidden(file, path):
- if file.startswith(u'.'):
- return True
- file = os.path.join(path, file)
- try:
- if win32api.GetFileAttributes(file) & win32con.FILE_ATTRIBUTE_HIDDEN:
- return True
- except Exception:
- return True
- return False
- except ImportError:
- pass
-
-###############################################################################
-# Logging
-###############################################################################
-
-log = logging.getLogger(__name__)
-
-###############################################################################
-# Constants
-###############################################################################
-
-HAIKUS = {
- 400: u'Something you entered<br>'
- u'transcended parameters.<br>'
- u'So much is unknown.',
-
- 401: u'To access this page,<br>'
- u'one must know oneself; but then:<br>'
- u'inform the server.',
-
- 403: u'Unfortunately,<br>'
- u'permissions insufficient.<br>'
- u'This, you cannot see.',
-
- 404: u'You step in the stream,<br>'
- u'But the water has moved on.<br>'
- u'This page is not here.',
-
- 410: u'A file that big?<br>'
- u'It might be very useful.<br>'
- u'But now it is Gone.',
-
- 413: u'Out of memory.<br>'
- u'We wish to hold the whole sky,<br>'
- u'But we never will.',
-
- 418: u'You requested coffee,<br>'
- u'it is neither short nor stout.<br>'
- u'I am a teapot.',
-
- 500: u'Chaos reigns within.<br>'
- u'Reflect, repent, and reboot.<br>'
- u'Order shall return.'
-}
-
-if os.name == 'nt':
- HAIKUS[500] = (u'Yesterday it worked.<br>'
- u'Today, it is not working.<br>'
- u'Windows is like that.')
-
-HTTP_MESSAGES = {
- 401: u'You must sign in to access this page.',
- 403: u'You do not have permission to view this page.',
- 404: u'The page at <code>%(uri)s</code> cannot be found.',
- 500: u'The server encountered an internal error and cannot display '
- u'this page.'
-}
-
-IMAGES = {
- 'audio' : u"iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAZpJREFUeNqsU01Lw0AQbdI0aVIVUiSlWKiV/gARiveC2ptehf4Rz+JJ0aM38ebJgzfvvQiWUhooqAeLYCkRUYRa8tHEN7IpIRS3ogsvuzs782Yz81YIgiDxH0P6ha/QbrdLvu/XgC1cIFWpVLZhd7lEpmlmXdetImgTwRuYl2Muc8DbT0SpZrN5huD65DqCkBBFkTAE3pFgCWaNR5RB9joFS5L0Ksvyg6qq97qum0CPJTrHLPJqlKFPPp8/KRQKDSw/IvgEsqw2AY/oOxNILjE9sWCbwSOCINZuXtf6wDPg81oqcs69WUhmIfq7IGMlEFut1u54PN6HvYROXpMiphEJnU5n1bbtUziuwbER41VBcowzgzZY1yANZ9qvKSC5gOM6acTzvCppKDI00hLZQruiKDfR+oVEmWQyqYWOBOz7EZ14xWLxMJ1Od6FqV9O023K5fAD7aKJ8VovFwWCwY1nWnuM4K8h2l8vljgzDuMLZCyCTPoESsMCexSNgAU6USAXo7dCjnGcK7jEdjVhhZaZ4mQlzGJLQ+BJgAITfplvWq5n7AAAAAElFTkSuQmCC",
- 'document' : u"iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAS1JREFUeNpi/P//PwM1AAsan+306dPngYZr4dPEyMh4zdTU1BDI/IXLIF4g1mJiYgIpBgsguxjEhvK1oGrf4jKIE0QoKCjUi4iI3AaxX79+rfXw4cMaqEvAGGoYJz6vgZ1x//79RiCGuwpmCFp4MuIzCAykpaXLpaSkjkNdZAh00URSAxts65MnTzqBGEUcFG4kGQQLB2RvoVtElEEgoKKiUiEgIPAIpA/dnhcvXug/fvy4nCiDbt++3UFpggQDCQmJBllZ2X1A5j80KeZnz55ZP336tI0og4DObwBhil0kIyNTJikpeRLI/IsmxfTy5UvjR48e9RMV/cDA7AJiksIIPXH8Y2dnvwBKM/gwSA16+DGipQshINYAYilc3gaCP0D8DIhvAPE7mCBAgAEAx0h2pQytmCsAAAAASUVORK5CYII=",
- 'folder' : u"iVBORw0KGgoAAAANSUhEUgAAABIAAAAKCAYAAAC5Sw6hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQ9JREFUeNqsUT2LwkAQvd3EaDDIESxiaQqLgCDk59z9Gu9XWN8/sUzE0i4iNloEES3EZJP1jWRExK/Chbezszvzdt6M0Fp/fWKZN349iqItbIMcwzD+wjAc4qheEUnakiRpxXH8n6ZpVwjRAM6PqPYHxn63IlkURR9Jv5ZljeiSicqy9FHh7kn+1nGcQRAES6pI+L6/llIeIKXJJPBJ2hl0vgXFAd+e51ExgrbSNM0MCbM8z+v3vmXySu7lDti4rkv901QRvRxBNFVKCQ58Z5rIWVWD0Dy1I/ozQbJiEvrxERnfQ8kSJr8ef4amjaG9A2QItK7lPFq2bcdMxFKIsAa0gV5lXzHtgTmwAA4nAQYAHA9ij4jhqJgAAAAASUVORK5CYII=",
- 'icon' : u"iVBORw0KGgoAAAANSUhEUgAAACgAAAAoCAYAAACM/rhtAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQ5JREFUeNpiYWBg+M8wiAELiGhI9RuUjmuYvYmBiWGQA8YhEcVT995EETx96TrDgsIAhoT+DUQbBFJvnNEL5+uqK5OkF2SXqZ4mini2szrEgeiOQwaXb94ly+fE6kP2CMhudEeyYHMUqaFHKQDZBbMT3S1Y0yDMcaSE3tkZxShRTAqAhSLIkVjTILbQIjdqyU0OIEeiuwPkYJaBcBAxaRYWqoO+HByZDgRlGKoW1NR2GLm5maYOpKajhlQapEoI4qp3qVF0US2K0WsBalWVVI3i////M4LwaDk46sBRB446cNSBow4cdeCoA0cdOOrAUQeOOpDUThMpI6KU9vZIAVQdo4Z1mBgZGalmJkCAAQB+2V2B4VtJPwAAAABJRU5ErkJggg==",
- 'image' : u"iVBORw0KGgoAAAANSUhEUgAAABUAAAATCAYAAAB/TkaLAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAphJREFUeNqsVE1rE1EU7UxmEjMl0QYxIU4SRi2KH9CkBhWKC3FbRfAH+AfcuBH/hZsiCC66U1y4URcKFnQjk5As1G6UhsQmJCkGZpIxyeRjPDe8J9OhaSL44DBvHu/dc9+55z7BcZyF/z0kQM7n80/H4/E9WhAEYd8GTor1r9lsdhVTe56gi8DdWCz2IJlM5jEfe/YItVrtYrVafVKv11Xs25kVVASCdDASiXzBdxeoeLAbj8eL+FqdTudCoVBI0/5ZmfpwRWcwGOyxICNvpqQCyWLb9rXhcPgol8tVRVF8ibU3mUzmg/d2kks7CnZQ1RxOlEqlPrZarQoyvtzr9dZAcB8ELQR/C5LnIHhHBNK8FaXbjEajdiKR2MIvaX+k3+8r0PtSu92+CpLXxWLxYTqdfiz9i1UoKJNIpqCBQEDRNI3q8BkBz/l8vmXMAyI/0Gw2M1MKIJRKpTNIVAFIuz5g0hHgJ1CyLMsEoRYOh3UqPs9UME1zU9f1zRnJeklJa7tcLl8hdVRVpaADiRl73+ZpDTDlJgG44o7f79clSTIoqDiPlkRCaDQaN9F9z6DfDebxBbhhCXa8HgwGP+H3N6/+SFGU991udx0Zid4s+ZBleRu2OUHtTICVKijMC+zXSO9oNEqu6E2SwMJRfKlqKlXU3W2wywpsEyU7IVDXMIx1FOQkEbsfIhB+g5VuMWcMKVML+A7UqLtcQRfR7xs4fOwgKdyBcXWdxRnyjqKJweAex7F5C6a+zfWbNmAlatXuX+JD3tOJLGjF0/DwKrx4HgRnUelTpD13BXT9hfZcw+8OfxYP6yi6zg/YZA+v1DYlRICmS3DBCpGguMuhUOgVu+Vgnkzdz6Of/MigACFGIrHOqrAkJuOPAAMATZ5MP7rfmUUAAAAASUVORK5CYII=",
- 'pants' : u"iVBORw0KGgoAAAANSUhEUgAAAKAAAACgCAYAAACLz2ctAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAhxJREFUeNrs3U1OwkAYgGEHJyndsSKkm3I9E6/jNbgLN+gJum9Cp8q2CsbYH1qeZwmE6PBm5kuLMZxOp/cQwgtM7XA4pLjb7br9fp8sB1PLsixsLANzEiCziv0HjsejVWE0VVXZAXEEgwARIAIEASJAmN6364D96zRgB0SAIEDWPwMOfS+4P1Mu7V7z0n/+R/t93QvGEQwCRIAgQASIAGEWceg3fLbrZmvT/7zG/jztgDiCESA8xgz43+8Dmvmeaya0A+IIBgGyjhnQDMeYM77vA+IIBgEiQBAgAkSAIEAECAJEgCBABAgCRIAwimgJhuXvou2ACBAEiBlw/TNen5nPDogAQYCYAZfPTGcHRIAgQAQIAkSAIEAECAJEgCBAFsi94F/4vyl2QAQIjuDZbbfbEGMMVz893zRNulwuXUrJYglwhONiswllWX7ce835fH77itRiOYKH17Ztd+/568ZY17U1FeD0bpzKOIJFZwcEASJAECACBAEiQARoCRAgAgQBIkAQIAIEASJAECACBAEiQBAgAgQBIkAQIAIEASJAECACBAEiQBAgAgQBIkAQIAIEASJABAgCRIAgQAQIAkSAIEAECAJEgCBABAgCRIAgQAQIAkSAIEAECAJEgCBABAgCRIAgQAQIAkSAIEAEiABBgAgQBIgAQYAIEMYV+w9UVWVVBmQ97YAIEG4cwUVRpDzPXy3FMKzn33wKMACVd1AkmFTspgAAAABJRU5ErkJggg==",
- 'video' : u"iVBORw0KGgoAAAANSUhEUgAAABIAAAAPCAYAAADphp8SAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAe9JREFUeNqsU0tLAlEUdmZ8Tki5qHxvtV3gLgmC6C+06me06CG5KFBo0b6lEEI7qWVB5CJCXKuLFsngeyEjk4yOY98d7pWrGW0aOJxz73z3O+d+51xhOp3a/uOzUy+Wy+VDj8dTHI/H27quPzKAIAiW5xO6XK59OMUwjK1EIvGA2GRE8mQyuYU/crvd66PRyOb3+48jkcg74WDJ6vV6st1uZ2RZ3kCyTeCusf8E0yyibrcbRGavpmkFlhUkb3BNmMGqj0ajRRDZ+v1+nrvVCiMSJElykp1QKJQ2TdPVbDbPKMknKZtVxE4SHG7gbLVa51g6mUYoRhCJBtBHB5FJ9TA4EhuNjSU4gRfbErXT6WTZulqt7sXj8Tu+okqlckAChmONmCMiHwTOiqJoNBqN1GAwyJVKpdxim8lhgoM3IEHqBxEpE+3UoJdO1sFgMA0tXhBOKETCwR1FUTIMx4/E3NV6vd4lJ+gzXIPvWiAQeAXRDPfr1cLh8AWyyejGCSVZ2rUF3NKrDWDCX11bwM2Ipj6fT4XIQ2S4YT9qtVoyFovd8xWhk7skYDjo1GKTb6fBCGNf8Hq9Hw6HYwgdrlRVzaNr+cWuISGZ+lM8ERnzZJ219KLlrZJGwdx0UtdoJUP+rcE8dAD7sC/yNGBt4r8FGADC3BrRMDVuEAAAAABJRU5ErkJggg==",
- 'zip' : u"iVBORw0KGgoAAAANSUhEUgAAABIAAAAQCAYAAAAbBi9cAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAVdJREFUeNpi/P//PwM1AAsan+306dPngYZrIQtyc3Pr/fnzR+Lnz5+7QHxGRsZrpqamhkDmL1wG8QKxFlQxg5ycXMbfv3+/Pn369BJMDOoDLajat7gM4oRpAIHHjx/PANGysrKZQAMEnzx50gaTg6nFZRBcFUiDoqJi7u/fv/8ADZiO5iIUtdgMYkB20f379yeDaAUFhYR///6JPHr0qAfNMPwGgRSCNMjLyxcCDfj64MGDBaTGGgp4+PBhP4iWkZHJBFLgMMKllgmfQVJSUrUmJiZ2P378YAUGfBvQdQzkGrQPSD0ChtEekFeZmJjIMwgI3p06derSx48feQmFEU6DQN64c+eOvZmZmcHr169N8XmLYGC/e/duBtBFGDFKkkGwtISUkhnwZXB0r/1jZ2e/BNIMC1wYGxmD1IDUoliMZosQEGuAIgyPa/8A8TMgvgHyPUwQIMAA22WMeFl8he8AAAAASUVORK5CYII="
-}
-
-IMAGES['icon'] = base64.b64decode(IMAGES['icon'])
-
-PAGE_CSS = u"""html, body { margin: 0; padding: 0; min-height: 100%%; }
-body {
- font-family: Calibri,"Arial","Helvetica",sans-serif;
- background: #EEE;
- background-image: -webkit-gradient( linear, left bottom, left top,
- color-stop(0, #ccc), color-stop(0.5, #eee) );
- background-image: -moz-linear-gradient( center bottom, #ccc 0%%, #eee 50%% );
-}
-
-table.dir td,a { color: #666; }
-h1, a:hover { color: #444; }
-
-a { text-decoration: none; }
-a:hover { text-decoration: underline; }
-
-div.document,.left,pre,table.dir th:first-child,table.dir td:first-child {
- text-align: left; }
-.thingy,.center,.footer { text-align: center; }
-table.dir td,table.dir th,.right { text-align: right; }
-
-table.dir td,table.dir th,.thingy > h1 { margin: 0; }
-p { margin-bottom: 0; }
-table.dir a,pre { display: block; }
-pre {
- background: #ddd;
- background-color: rgba(199,199,199,0.5);
- text-align: left;
- border-radius: 5px;
- -moz-border-radius: 5px;
- padding: 5px;
-}
-
-table.dir { width:100%%; border-spacing: 0; }
-table.dir td,table.dir th { padding: 2px 5px; }
-table.dir td { border-top: 1px solid transparent; border-bottom: 1px solid transparent; }
-table.dir tr:first-child td { border-top: none; }
-table.dir tr:hover td { border-color: #ccc; }
-table.dir td.noborder { border-color: transparent !important; }
-table.dir th { border-bottom: 1px solid #ccc; }
-
-.footer,.faint { color: #aaa; }
-.footer .debug { font-size: 0.9em; font-family: Consolas,monospace; }
-.haiku { margin-top: 20px; }
-.haiku + p { color: #777; }
-.spacer { padding-top: 60px; }
-.column { max-width: 960px; min-width: 600px; margin: 0px auto; }
-.footer { padding-top: 10px; }
-
-a.icon { padding-left: 23px; background-position: left; }
-a.icon,.thingy { background-repeat: no-repeat; }
-
-a.folder { background-image: url("data:image/png;base64,%s"); }
-a.document { background-image: url("data:image/png;base64,%s"); }
-a.image { background-image: url("data:image/png;base64,%s"); }
-a.zip { background-image: url("data:image/png;base64,%s"); }
-a.audio { background-image: url("data:image/png;base64,%s"); }
-a.video { background-image: url("data:image/png;base64,%s"); }
-
-.thingy { background-color: #FFF; background-position: center; color: #000;
- border: 5px #ddd solid;
- -moz-border-radius: 25px;
- border-radius: 25px;
- padding: 50px;
- margin: 0 50px;
-}""" % (IMAGES['folder'], IMAGES['document'], IMAGES['image'], IMAGES['zip'],
- IMAGES['audio'], IMAGES['video'])
-PAGE_CSS = PAGE_CSS.replace('%','%%%%')
-
-PAGE = u"""<!DOCTYPE html>
-<html><head><title>%%s</title><style>%s</style></head><body>
-<div class="column"><div class="spacer"></div><div class="thingy">
-%%s
-</div><div class="footer"><i><a href="%s">%s</a><br>%%%%s</i>
-<div class="debug">%%%%s</div></div>
-<div class="spacer"></div></div></body></html>""".replace('\n','') % (
- PAGE_CSS, SERVER_URL, SERVER)
-
-DIRECTORY_PAGE = PAGE % (
- u'Index of %s',
- u"""<h1>Index of %s</h1>%s<table class="dir"><thead><tr>
-<th style="width:50%%">Name</th><th>Size</th>
-<th class="center" colspan="2">Last Modified</th></tr></thead>%s
-</table>"""
- )
-
-ERROR_PAGE = PAGE % (
- u'%d %s',
- u'<h1>%d<br>%s</h1>%s%s'
- )
-
-# Regular expressions used for various types.
-REGEXES = {
- int : r'(-?\d+)',
- float : r'(-?\d+(?:\.\d+)?)',
-}
-
-# Formats for _parse_date to use.
-DATE_FORMATS = (
- "%a, %d %b %Y %H:%M:%S %Z",
- "%A, %d-%b-%y %H:%M:%S %Z",
- "%a %b %d %H:%M:%S %Y",
- )
-
-###############################################################################
-# Special Exceptions
-###############################################################################
-
-class HTTPException(Exception):
- """
- Raising an instance of HTTPException will cause the Application to render
- an error page out to the client with the given
- `HTTP status code <http://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_,
- message, and any provided headers.
-
- This is, generally, preferable to allowing an exception of a different
- type to bubble up to the Application, which would result in a
- ``500 Internal Server Error`` page.
-
- The :func:`abort` helper function makes it easy to raise instances of
- this exception.
-
- ========= ============
- Argument Description
- ========= ============
- status *Optional.* The `HTTP status code <http://en.wikipedia.org/wiki/List_of_HTTP_status_codes>`_ to generate an error page for. If this isn't specified, a ``404 Not Found`` page will be generated.
- message *Optional.* A text message to display on the error page.
- headers *Optional.* A dict of extra HTTP headers to return with the rendered page.
- ========= ============
- """
- def __init__(self, status=404, message=None, headers=None):
- self.status = status
- self.message = message
- self.headers = headers
-
-class HTTPTransparentRedirect(Exception):
- """
- Raising an instance of HTTPTransparentRedirect will cause the Application
- to silently redirect a request to a new URI.
- """
- def __init__(self, uri):
- self.uri = uri
-
-###############################################################################
-# Application Class
-###############################################################################
-
-class Application(object):
- """
- The Application class builds upon :class:`pants.contrib.http.HTTPServer`,
- adding support for request routing, additional error handling, and a
- degree of convenience that makes writing dynamic pages easier.
-
- Instances of Application are callable, and may be used as a HTTPServer's
- request handler.
-
- =============== ============
- Argument Description
- =============== ============
- default_domain *Optional.* The default domain to search for a route for if the request's Host does not exist.
- debug *Optional.* If this is set to True, automatically generated ``500 Internal Server Error`` response pages will include information about the failed request, including a traceback of the exception that caused the page to be generated.
- =============== ============
- """
- current_app = None
-
- def __init__(self, default_domain=None, debug=False):
- # Internal Stuff
- self._routes = {}
- self._names = {}
-
- self._routes[None] = {}
-
- # External Stuff
- self.default_domain = None
- self.debug = debug
-
- def run(self, port=None, host='', ssl_options=None):
- """
- This function exists for convenience, and when called creates a
- :class:`~pants.contrib.http.HTTPServer` instance with its request
- handler set to this application instance, calls
- :func:`~pants.contrib.http.HTTPServer.listen` on that HTTPServer, and
- finally, starts the Pants engine to process requests.
-
- ============ ============
- Argument Description
- ============ ============
- port *Optional.* The port to listen on. If this isn't specified, it will be either 80 or 443, depending on whether or not SSL options for the server have been provided.
- host *Optional.* The host interface to listen on. If this isn't specified, listen on all interfaces.
- ssl_options *Optional.* A dict of SSL options for the server. See :class:`pants.contrib.ssl.SSLServer` for more information.
- ============ ============
- """
- from pants import engine
- HTTPServer(self, ssl_options=ssl_options).listen(port, host)
- engine.start()
-
- ##### Route Management Methods ############################################
-
- def basic_route(self, rule, name=None, methods=['GET','HEAD']):
- """
- The basic_route decorator registers a route with the Application without
- holding your hand over it.
-
- It functions almost the same as the :func:`Application.route` decorator,
- but doesn't wrap the provided function with any argument handling code.
- Instead, you're provided with the request object and the the regex
- match object.
-
- Example Usage::
-
- @app.basic_route("/char/<char>")
- def my_route(request):
- char, = request.match.groups()
- return 'The character is %s!' % char
-
- That is, essentially, equivilent to::
-
- @app.route("/char/<char>/")
- def my_route(char):
- return 'The character is %s!' % char
-
- ========= ============
- Argument Description
- ========= ============
- rule The route rule to match for a request to go to the decorated function. See :func:`Application.route` for more information.
- name *Optional.* The name of the decorated function, for use with the :func:`url_for` helper function.
- methods *Optional.* A list of HTTP methods to allow for this request handler. By default, only ``GET`` and ``HEAD`` requests are allowed, and all others will result in a ``405 Method Not Allowed`` error.
- ========= ============
- """
- def decorator(func):
- if rule[0] != '/':
- domain, _, _rule = rule.partition('/')
- _rule = '/' + rule
- else:
- domain = None
- _rule = rule
-
- regex, arguments, names, namegen = _route_to_regex(_rule)
- _regex = re.compile(regex)
-
- _name = name
- if _name is None:
- _name = "%s.%s" % (func.__module__, func.__name__)
-
- if not hasattr(func, 'content_type'):
- func.content_type = None
-
- self._insert_route(_regex, func, domain, _name, methods, names, namegen)
- return func
- return decorator
-
- def route(self, rule, name=None, methods=['GET','HEAD'], auto404=False,
- content_type=None):
- """
- The route decorator is used to register a new request handler with the
- Application instance. Example::
-
- @app.route("/")
- def hello_world():
- return "Hiya, Everyone!"
-
- Variables may be specified in the route *rule* by wrapping them with
- inequality signs (for example: ``<variable_name>``). By default, a
- variable part accepts any character except a slash (``/``) and returns
- a string value. However, you may specify a specific type to be returned
- by using the format ``<type:name>``, where type is the name of a
- callable in the pants.contrib.web namespace that accepts a single
- string as its argument, and returns a value. Built-in types, such as
- int and float, work well for this. Example::
-
- @app.route("/user/<int:id>/")
- def user(id):
- return "Hi, user %d!" % id
-
- The ``id`` is automatically converted into an integer for you, and as
- an added bonus, your function is never even called if the provided
- value for ``id`` isn't a valid number.
-
- Request handlers are easy to write and can send their output to the
- client simply by returning a value, such as a string::
-
- @app.route("/")
- def hello_world():
- return "Hiya, Everyone!"
-
- The previous code would result in a `200 OK`` response, with a
- ``Content-Type`` header of ``text/plain``, and a ``Content-Length``
- header of ``15``. With, of course, the body ``Hiya, Everyone!``.
-
- If the returned string begins with ``<!DOCTYPE`` or ``<html``, it will
- be assumed that the response is of ``Content-Type: text/html``.
-
- If a unicode object is returned, rather than a simple string, it will
- be automatically encoded and an encoding argument will be added to the
- ``Content-Type`` header.
-
- If a dictionary is returned, it will be automatically converted to a
- string of `JSON <http://en.wikipedia.org/wiki/JSON>`_ and the
- ``Content-Type`` header will be set to ``application/json``.
-
- If any other object is returned, it will be converted to a string
- via ``str()`` before any content headers are set. The exception to this
- is that, if the object has a ``__html__`` method, that method will be
- called rather than ``str()``, and the ``Content-Type`` will be
- automatically assumed to be ``text/html``, regardless of the actual
- content of the string.
-
- A tuple of ``(body, status)`` or ``(body, status, headers)`` may be
- returned, rather than simply a body, to set the HTTP status code of
- the result and additional response headers. If provided, ``status``
- must be an integer, and ``headers`` must be a dict.
-
- The following example returns a page with the status code ``404 Not Found``::
-
- @app.route("/nowhere")
- def nowhere():
- return "This does not exist.", 404
-
- ============= ============
- Argument Description
- ============= ============
- rule The route rule to be matched for the decorated function to be used for handling a request.
- name *Optional.* The name of the decorated function, for use with the :func:`url_for` helper function.
- methods *Optional.* A list of HTTP methods to allow for this request handler. By default, only ``GET`` and ``HEAD`` requests are allowed, and all others will result in a ``405 Method Not Allowed`` error.
- auto404 *Optional.* If this is set to True, all response handler arguments will be checked for truthiness (True, non-empty strings, etc.) and, if any fail, a ``404 Not Found`` page will be rendered automatically.
- content_type *Optional.* If set, the ``Content-Type`` header will default to this unless returned as part of a header dict from the view function.
- ============= ============
- """
- if callable(name):
- self._add_route(rule, name, None, methods, auto404, content_type)
- return
-
- def decorator(func):
- self._add_route(rule, func, name, methods, auto404, content_type)
- return func
- return decorator
-
- ##### Error Handlers ######################################################
-
- def handle_404(self, request, exception):
- if isinstance(exception, HTTPException):
- return error(exception.message, 404)
- return error(404)
-
- def handle_500(self, request, exception):
- log.exception('Error handling HTTP request: %s %%s' % request.method,
- request.uri)
- if not self.debug:
- return error(500)
-
- resp = u''.join([
- u"<h2>Traceback</h2>\n",
- u"<pre>%s</pre>\n" % traceback.format_exc(),
- u"<h2>Route</h2>\n<pre>",
- u"route name = %r\n" % request.route_name,
- u"match groups = %r" % (request.match.groups(),),
- u"</pre>\n",
- u"<h2>HTTP Request</h2>\n",
- request.__html__(),
- ])
-
- return error(resp, 500)
-
- ##### The Request Handler #################################################
-
- def __call__(self, request):
- """
- This function is called when a new request is received, and calls both
- :func:`Application.handle_request` and :func:`Application.handle_output`
- to process the request.
- """
- Application.current_app = self
- self.request = request
-
- try:
- request.auto_finish = True
- self.handle_output(*self.handle_request(request))
- finally:
- request.route = None
- request.match = None
- request.route_name = None
-
- Application.current_app = None
- self.request = None
-
- def handle_output(self, result, content_type):
- """ Process the output of handle_request. """
- request = self.request
-
- if not request.auto_finish or result is None or \
- request._finish is not None:
- if request.auto_finish and request._finish is None:
- request.finish()
- return
-
- status = 200
- if type(result) is tuple:
- if len(result) == 3:
- body, status, headers = result
- else:
- body, status = result
- headers = {}
- else:
- body = result
- headers = {}
-
- # Set a Content-Type header if there isn't already one.
- if not 'Content-Type' in headers:
- if content_type != None:
- headers['Content-Type'] = content_type
- elif (isinstance(body, basestring) and
- body[:5].lower() in ('<html','<!doc')) or \
- hasattr(body, 'to_html'):
- headers['Content-Type'] = 'text/html'
- elif isinstance(body, dict):
- headers['Content-Type'] = 'application/json'
- else:
- headers['Content-Type'] = 'text/plain'
-
- # Convert the body to something sendable.
- try:
- body = body.to_html()
- except AttributeError:
- pass
-
- if isinstance(body, unicode):
- encoding = headers['Content-Type']
- if 'charset=' in encoding:
- before, sep, enc = encoding.partition('charset=')
- else:
- before = encoding
- sep = '; charset='
- enc = 'UTF-8'
-
- body = body.encode(enc)
- headers['Content-Type'] = '%s%s%s' % (before, sep, enc)
-
- elif isinstance(body, dict):
- try:
- body = json.dumps(body)
- except Exception, e:
- body, status, headers = self.handle_500(request, e)
- body = body.encode('utf-8')
- headers['Content-Type'] = 'text/html; charset=UTF-8'
-
- elif not isinstance(body, str):
- body = str(body)
-
- # More headers!
- headers['Content-Length'] = len(body)
- if not 'Date' in headers:
- headers['Date'] = date(datetime.utcnow())
- if not 'Server' in headers:
- headers['Server'] = SERVER
-
- # Send the response.
- request.send_status(status)
- request.send_headers(headers)
-
- if request.method == 'HEAD':
- request.finish()
- return
-
- request.write(body)
- request.finish()
-
- def handle_request(self, request):
- path = request.path
-
- # Domain Matching
- if len(self._routes) == 1:
- domain = None
- else:
- if request.host in self._routes:
- domain = request.host
- else:
- domain = '.' + request.host.partition('.')[2]
- if not domain in self._routes and ':' in request.host:
- domain = request.host.rpartition(':')[0]
- if not domain in self._routes:
- domain = '.' + domain.partition('.')[2]
- if not domain in self._routes:
- domain = self.default_domain
-
- for route in self._routes[domain]:
- match = route.match(path)
- if match is None:
- continue
-
- # Process this route.
- func, name, methods = self._routes[domain][route][:3]
-
- request.route = route
- request.match = match
- request.route_name = name
-
- if request.method not in methods:
- return error(
- 'The method %s is not allowed for %r.' % (
- request.method, path), 405, {
- 'Allow': ', '.join(methods)
- }), None
- else:
- try:
- return func(request), func.content_type
- except HTTPException, e:
- if hasattr(self, 'handle_%d' % e.status):
- return getattr(self, 'handle_%d' % e.status)(request, e), None
- else:
- return error(e.message, e.status, e.headers), None
- except HTTPTransparentRedirect, e:
- request.uri = e.uri
- request._parse_uri()
- return self.handle_request(request)
- except Exception, e:
- return self.handle_500(request, e), None
- break
- else:
- # No matching routes.
- if not path.endswith('/'):
- p = '%s/' % path
- for route in self._routes[domain]:
- if route.match(p):
- if request.query:
- return redirect('%s?%s' % (p,request.query)), None
- else:
- return redirect(p), None
-
- return self.handle_404(request, None), None
-
- ##### Internal Methods and Event Handlers #################################
-
- def _insert_route(self, route, handler, domain, name, methods, nms, namegen):
- if isinstance(route, basestring):
- route = re.compile(route)
- if not domain in self._routes:
- self._routes[domain] = {}
- self._routes[domain][route] = (handler, name, methods, nms, namegen)
- self._names[name] = route
-
- def _add_route(self, route, view, name=None, methods=['GET','HEAD'],
- auto404=False, content_type=None):
- """ See: Application.route """
- if name is None:
- if view is None:
- raise Exception('No name or view specified!')
- if hasattr(view, '__name__'):
- name = view.__name__
- elif hasattr(view, '__class__'):
- name = view.__class__.__name__
- else:
- raise NameError("Cannot find name for this route.")
-
- if not callable(view):
- raise Exception('View must be callable.')
-
- # Parse the route.
- if route[0] != '/':
- domain, _, route = route.partition('/')
- route = '/' + route
- else:
- domain = None
-
- regex, arguments, names, namegen = _route_to_regex(route)
- _regex = re.compile(regex)
-
- if not arguments:
- arguments = False
-
- try:
- args = inspect.getargspec(view).args
- except TypeError:
- args = inspect.getargspec(view.__call__).args[1:]
-
- if len(args) == 1 and args[0] == 'request':
- def view_runner(request):
- request.__viewmodule__ = view.__module__
- match = request.match
- try:
- if arguments is False:
- return view(request)
-
- out = []
- for val,type in zip(match.groups(), arguments):
- if type is not None:
- try:
- val = type(val)
- except Exception:
- return error('Unable to parse data %r.' % val, 400)
- out.append(val)
-
- if auto404 is True:
- all_or_404(*out)
-
- request.arguments = out
- return view(request)
- finally:
- request.arguments = None
-
- else:
- def view_runner(request):
- request.__viewmodule__ = view.__module__
- match = request.match
- try:
- try:
- view.func_globals['request'] = request
- except AttributeError:
- view.__call__.func_globals['request'] = request
- if arguments is False:
- return view()
-
- out = []
- for val,type in zip(match.groups(), arguments):
- if type is not None:
- try:
- val = type(val)
- except Exception:
- return error('Unable to parse data %r.' % val, 400)
- out.append(val)
-
- if auto404 is True:
- all_or_404(*out)
-
- return view(*out)
- finally:
- try:
- view.func_globals['request'] = None
- except AttributeError:
- view.__call__.func_globals['request'] = None
-
- view_runner.__name__ = name
- view_runner.content_type = content_type
- self._insert_route(_regex, view_runner, domain,
- "%s.%s" %(view.__module__,name), methods, names, namegen)
-
-###############################################################################
-# FileServer Class
-###############################################################################
-
-class FileServer(object):
- """
- The FileServer is a request handling class that, as it sounds, serves files
- to the client. It also supports the ``Content-Range`` header, HEAD requests,
- and last modified dates.
-
- ========== ============================== ============
- Argument Default Description
- ========== ============================== ============
- path The path to serve.
- blacklist ``.py`` and ``.pyc`` files *Optional.* A list of regular expressions to test filenames against. If a given file matches any of the provided patterns, it will not be downloadable and instead return a ``403 Unauthorized`` error.
- default ``index.html``, ``index.htm`` *Optional.* A list of default files to be displayed rather than a directory listing if they exist.
- renderers None *Optional.* A dictionary of methods for rendering files with a given extension into more suitable output, such as converting rST to HTML, or minifying CSS.
- ========== ============================== ============
-
- It attempts to serve the files as efficiently as possible, using the
- `sendfile <http://www.kernel.org/doc/man-pages/online/pages/man2/sendfile.2.html>`_
- system call when possible, and with proper use of ETags and other headers to
- minimize repetitive downloading.
-
- Using it is simple. It only requires a single argument: the path to serve
- files from. You can also supply a list of default files to check to serve
- rather than a file listing.
-
- When used with an Application, the FileServer is not created in the usual
- way with the route decorator, but rather with a method of the FileServer
- itself. Example::
-
- FileServer("/tmp/path").attach(app)
-
- If you wish to listen on a path other than ``/static/``, you can also use
- that when attaching::
-
- FileServer("/tmp/path").attach(app, "/files/")
- """
- def __init__(self, path, blacklist=[re.compile('.*\.py[co]?$')],
- defaults=['index.html','index.htm'],
- renderers=None):
- # Make sure our path is unicode.
- if not isinstance(path, unicode):
- path = _decode(path)
-
- self.path = os.path.normpath(os.path.realpath(path))
- self.defaults = defaults
- self.renderers = renderers or {}
-
- # Build the blacklist.
- self.blacklist = []
- for bl in blacklist:
- if isinstance(bl, str):
- bl = re.compile(bl)
- self.blacklist.append(bl)
-
- def attach(self, app, path='/static/', domain=None):
- """
- Attach this fileserver to an application, bypassing the usual route
- decorator to ensure things are done right.
-
- ========= =============== ============
- Argument Default Description
- ========= =============== ============
- app The :class:`~pants.contrib.web.Application` instance to attach to.
- path ``'/static/'`` *Optional.* The path to serve requests from.
- domain None *Optional.* The domain to serve requests upon.
- ========= =============== ============
- """
- path = re.compile("^%s(.*)$" % re.escape(path))
- app._insert_route(path, self, domain, "FileServer", ['HEAD','GET'], None, None)
-
- def check_blacklist(self, path):
- """
- Check the given path to make sure it isn't blacklisted. If it is
- blacklisted, then raise an :class:`~pants.contrib.web.HTTPException`
- via :func:`~pants.contrib.web.abort`.
-
- ========= ============
- Argument Description
- ========= ============
- path The path to check against the blacklist.
- ========= ============
- """
- for bl in self.blacklist:
- if isinstance(bl, unicode):
- if bl in path:
- abort(403)
- elif bl.match(path):
- abort(403)
-
- def __call__(self, request):
- """
- Serve a request.
- """
-
- try:
- path = request.match.group(1)
- except (AttributeError, IndexError):
- path = request.path
-
- # Conver the path to unicode.
- path = _decode(urllib.unquote(path))
-
- # Strip off a starting quote.
- if path.startswith('/') or path.startswith('\\'):
- path = path[1:]
-
- # Normalize the path.
- full_path = os.path.normpath(os.path.join(self.path, path))
-
- # Validate the request.
- if not full_path.startswith(self.path):
- abort(403)
- if not os.path.exists(full_path):
- abort(404)
-
- # Is this a directory?
- if os.path.isdir(full_path):
- # Check defaults.
- for f in self.defaults:
- full = os.path.join(full_path, f)
- if os.path.exists(full):
- request.path = urllib.quote(full.encode('utf8'))
- if hasattr(request, 'match'):
- del request.match
- return self.__call__(request)
-
- # Guess not. List it.
- if hasattr(request, 'match'):
- return self.list_directory(request, path)
- else:
- body, status, headers = self.list_directory(request, path)
- if isinstance(body, unicode):
- body = body.encode('utf-8')
- headers['Content-Length'] = len(body)
- request.send_status(status)
- request.send_headers(headers)
- request.send(body)
- request.finish()
- return
-
- # Blacklist Checking.
- self.check_blacklist(full_path)
-
- # Try rendering the content.
- ext = os.path.basename(full_path).rpartition('.')[-1]
- if ext in self.renderers:
- f, mtime, size, type = self.renderers[ext](request, full_path)
- else:
- # Get the information for the actual file.
- f = None
- stat = os.stat(full_path)
- mtime = stat.st_mtime
- size = stat.st_size
- type = mimetypes.guess_type(full_path)[0]
-
- # If we don't have a type, text/plain it.
- if type is None:
- type = 'text/plain'
-
- # Generate a bunch of data for headers.
- modified = datetime.fromtimestamp(mtime)
- expires = datetime.utcnow() + timedelta(days=7)
-
- etag = '"%x-%x"' % (size, int(mtime))
-
- headers = {
- 'Last-Modified' : date(modified),
- 'Expires' : date(expires),
- 'Cache-Control' : 'max-age=604800',
- 'Content-Type' : type,
- 'Date' : date(datetime.utcnow()),
- 'Server' : SERVER,
- 'Accept-Ranges' : 'bytes',
- 'ETag' : etag
- }
-
- do304 = False
-
- if 'If-Modified-Since' in request.headers:
- try:
- since = _parse_date(request.headers['If-Modified-Since'])
- except ValueError:
- since = None
- if since and since >= modified:
- do304 = True
-
- if 'If-None-Match' in request.headers:
- if etag == request.headers['If-None-Match']:
- do304 = True
-
- if do304:
- if f:
- f.close()
- request.auto_finish = False
- request.send_status(304)
- request.send_headers(headers)
- request.finish()
- return
-
- if 'If-Range' in request.headers:
- if etag != request.headers['If-Range'] and \
- 'Range' in request.headers:
- del request.headers['Range']
-
- last = size - 1
- range = 0, last
- status = 200
-
- if 'Range' in request.headers:
- if request.headers['Range'].startswith('bytes='):
- try:
- val = request.headers['Range'][6:].split(',')[0]
- start, end = val.split('-')
- except ValueError:
- if f:
- f.close()
- abort(416)
- try:
- if end and not start:
- end = last
- start = last - int(end)
- else:
- start = int(start or 0)
- end = int(end or last)
-
- if start < 0 or start > end or end > last:
- if f:
- f.close()
- abort(416)
- range = start, end
- except ValueError:
- pass
- if range[0] != 0 or range[1] != last:
- status = 206
- headers['Content-Range'] = 'bytes %d-%d/%d' % (
- range[0], range[1], size)
-
- # Set the content length header.
- if range[0] == range[1]:
- headers['Content-Length'] = 0
- else:
- headers['Content-Length'] = 1 + (range[1] - range[0])
-
- # Send the headers and status line.
- request.auto_finish = False
- request.send_status(status)
- request.send_headers(headers)
-
- # Don't send the body if this is head.
- if request.method == 'HEAD':
- if f:
- f.close()
- request.finish()
- return
-
- # Open the file and send it.
- if range[0] == range[1]:
- if f:
- f.close()
- request.finish()
- return
-
- if f is None:
- f = open(full_path, 'rb')
-
- if range[1] != last:
- length = 1 + (range[1] - range[0])
- else:
- length = 0
-
- def on_write():
- del request.connection.handle_write_file
- request.finish()
-
- request.connection.handle_write_file = on_write
- request.connection.write_file(f, nbytes=length, offset=range[0])
-
- def list_directory(self, request, path):
- """
- Generate a directory listing and return it.
- """
-
- # Normalize the path.
- full_path = os.path.normpath(os.path.join(self.path, path))
-
- # Get the URI, which is just request.path decoded.
- uri = _decode(urllib.unquote(request.path))
- if not uri.startswith(u'/'):
- uri = u'/%s' % uri
- if not uri.endswith(u'/'):
- return redirect(u'%s/' % uri)
-
- go_up = u''
- url = uri.strip(u'/')
- if url:
- go_up = u'<p><a href="..">Up to Higher Directory</a></p>'
-
- files = []
- dirs = []
-
- for p in sorted(os.listdir(full_path), key=unicode.lower):
- if _is_hidden(p, full_path):
- continue
-
- full = os.path.join(full_path, p)
- try:
- fp = full
- if os.path.isdir(full):
- fp += '/'
- self.check_blacklist(fp)
- except HTTPException:
- continue
-
- stat = os.stat(full)
- mtime = datetime.fromtimestamp(stat.st_mtime).strftime(
- u'<td class="right">%Y-%m-%d</td>'
- u'<td class="left">%I:%M:%S %p</td>'
- )
-
- if os.path.isdir(full):
- cls = u'folder'
- link = u'%s/' % p
- size = u'<span class="faint">Directory</span>'
- obj = dirs
-
- elif os.path.isfile(full):
- cls = 'document'
- ext = p[p.rfind('.')+1:]
- if ext in ('jpg','jpeg','png','gif','bmp'):
- cls = 'image'
- elif ext in ('zip','gz','tar','7z','tgz'):
- cls = 'zip'
- elif ext in ('mp3','mpa','wma','wav','flac','mid','midi','raw',
- 'mod','xm','aac','m4a','ogg','aiff','au','voc','m3u',
- 'pls','asx'):
- cls = 'audio'
- elif ext in ('mpg','mpeg','mkv','mp4','wmv','avi','mov'):
- cls = 'video'
- link = p
- size = _human_readable_size(stat.st_size)
- obj = files
-
- else:
- continue
-
- obj.append(
- u'<tr><td><a class="icon %s" href="%s%s">%s</a></td><td>%s'
- u'</td>%s</tr>' % (
- cls, uri, link, p, size, mtime))
-
- if files or dirs:
- files = u''.join(dirs) + u''.join(files)
- else:
- files = (u'<tr><td colspan="4" class="noborder">'
- u'<div class="footer center">'
- u'This directory is empty.</div></td></tr>')
-
- if Application.current_app and Application.current_app.debug:
- rtime = u'%0.3f ms' % (1000 * request.time)
- else:
- rtime = u''
-
- return DIRECTORY_PAGE % (uri, uri, go_up, files, request.host, rtime), \
- 200, {
- 'Content-Type':'text/html; charset=utf-8'
- }
-
-###############################################################################
-# Private Helper Functions
-###############################################################################
-
-def path(st):
- return st
-path.regex = "(.+?)"
-
-def _get_thing(thing):
- if thing in globals():
- return globals()[thing]
- elif type(__builtins__) is dict and thing in __builtins__:
- return __builtins__[thing]
- elif hasattr(__builtins__, thing):
- return getattr(__builtins__, thing)
- return None
-
-_route_parser = re.compile(r"<([^>]+)>([^<]*)")
-def _route_to_regex(route):
- """ Parse a Flask-style route and return a regular expression, as well as
- a tuple of things for conversion. """
- regex, values, names, namegen = "", [], [], ""
- if not route.startswith("^/"):
- if route.startswith("/"):
- route = "^%s$" % route
- else:
- route = "^/%s$" % route
-
- # Find up to the first < and add it to regex.
- ind = route.find('<')
- if ind is -1:
- return route, tuple(), tuple(), route[1:-1]
- elif ind > 0:
- regex += route[:ind]
- namegen += route[:ind]
- route = route[ind:]
-
- # If the parser doesn't match, return.
- if not _route_parser.match(route):
- return regex+route, tuple(), tuple(), (regex+route)[1:-1]
-
- for match in _route_parser.finditer(route):
- group = match.group(1)
- if ':' in group:
- type, var = group.split(':', 1)
- thing = _get_thing(type)
- if not thing:
- raise Exception, "Invalid type declaration, %s" % type
- if hasattr(thing, 'regex'):
- regex += thing.regex
- elif thing in REGEXES:
- regex += REGEXES[thing]
- else:
- regex += "([^/]+)"
- values.append(thing)
- names.append(var)
- else:
- regex += "([^/]+)"
- values.append(None)
- names.append(group)
- namegen += "%s" + match.group(2)
- regex += match.group(2)
-
- return regex, tuple(values), tuple(names), namegen[1:-1]
-
-_abbreviations = (
- (1<<50L, u' PB'),
- (1<<40L, u' TB'),
- (1<<30L, u' GB'),
- (1<<20L, u' MB'),
- (1<<10L, u' KB'),
- (1, u' B')
-)
-def _human_readable_size(size, precision=2):
- """ Convert a size to a human readable filesize. """
- if size == 0:
- return u'0 B'
-
- for f,s in _abbreviations:
- if size >= f:
- break
-
- ip, dp = `size/float(f)`.split('.')
- if int(dp[:precision]):
- return u'%s.%s%s' % (ip,dp[:precision],s)
- return u'%s%s' % (ip,s)
-
-_encodings = ('utf-8','iso-8859-1','cp1252','latin1')
-def _decode(text):
- for enc in _encodings:
- try:
- return text.decode(enc)
- except UnicodeDecodeError:
- continue
- else:
- return text.decode('utf-8','ignore')
-
-def _parse_date(text):
- for fmt in DATE_FORMATS:
- try:
- return datetime(*time.strptime(text, fmt)[:6])
- except ValueError:
- continue
- raise ValueError("Unable to parse time data %r." % text)
-
-###############################################################################
-# Public Helper Functions
-###############################################################################
-
-def abort(status=404, message=None, headers=None):
- """
- Raise a :class:`~pants.contrib.web.HTTPException` to display an error page.
- """
- raise HTTPException(status, message, headers)
-
-def all_or_404(*args):
- """
- If any of the provided arguments aren't truthy, raise a ``404 Not Found``
- exception. This is automatically called for you if you set ``auto404=True``
- when using the route decorator.
- """
- all(args) or abort()
-
-def error(message=None, status=None, headers=None, request=None, debug=None):
- """
- Return a very simple error page, defaulting to a ``404 Not Found`` error if
- no status code is supplied. Usually, you'll want to call :func:`~pants.contrib.web.abort`
- in your code, rather than error(), to streamline the process of abandoning
- your code. Usage::
-
- return error(404)
- return error("Some message.", 404)
- return error("Blah blah blah.", 403, {'Some-Header':'Fish'})
- """
- if request is None:
- request = Application.current_app.request
-
- if status is None:
- if type(message) is int:
- status = message
- message = None
- else:
- status = 404
-
- if not status in HTTP:
- status = 404
- title = HTTP[status]
- if not headers:
- headers = {}
-
- if message is None:
- if status in HTTP_MESSAGES:
- dict = request.__dict__.copy()
- dict['uri'] = _decode(urllib.unquote(dict['uri']))
- message = HTTP_MESSAGES[status] % dict
- else:
- message = u"An unspecified error has occured."
-
- haiku = u''
- if status in HAIKUS:
- haiku = u'<div class="haiku">%s</div>' % HAIKUS[status]
-
- if not message.startswith(u'<'):
- message = u'<p>%s</p>' % message
-
- if debug is None:
- debug = Application.current_app and Application.current_app.debug
-
- if debug:
- time = u'%0.3f ms' % (1000 * request.time)
- else:
- time = u''
-
- result = ERROR_PAGE % (status, title, status, title.replace(u' ',u'&nbsp;'),
- haiku, message, request.host, time)
-
- return result, status, headers
-
-def redirect(uri, status=302):
- """
- Construct a ``302 Found`` response to instruct the client's browser to
- redirect its request to a different URL. Other codes may be returned by
- specifying a status.
-
- ========= ======== ============
- Argument Default Description
- ========= ======== ============
- uri The URI to redirect the client's browser to.
- status ``302`` *Optional.* The status code to send with the response.
- ========= ======== ============
- """
- url = uri
- if isinstance(url, unicode):
- url = uri.encode('utf-8')
-
- return error(
- 'The document you have requested is located at <a href="%s">%s</a>.' % (
- uri, uri), status, {'Location':url})
-
-def url_for(name, **values):
- """
- Generates a URL to the route with the given name. The name is relative to
- the module of the route function. Examples:
-
- ============== ================ ================
- View's Module Target Endpoint Target Function
- ============== ================ ================
- ``test`` ``index`` ``test.index``
- ``test`` ``.who`` The first ``who`` function in *any* module.
- ``test`` ``admin.login`` ``admin.login``
- ============== ================ ================
-
- Any value provided to the function with an unknown key is appended to the
- generated URL as query arguments. For example, take the following route::
-
- @app.route("/user/<int:id>/")
- def user_page(id):
- pass
-
- Assuming ``url_for`` is used within the same module, the following examples
- will hold true::
-
- >>> url_for("user_page", id=12)
- '/user/12/'
-
- >>> url_for("user_page", id=12, section=3)
- '/user/12/?section=3'
-
- >>> url_for("user_page", id=12, _external=True)
- 'http://www.example.com/user/12/'
-
- As demonstrated above, the ``_external`` parameter is special, and will
- result in the generation of a full URL, using the scheme and host provided
- by the current request.
-
- *Note:* This function has not yet been updated to properly make use of
- domains.
- """
- app = Application.current_app
- request = app.request
-
- if name.startswith('.'):
- # Find it in the first possible place.
- name = name[1:]
- for n in app._names:
- module, nm = n.split('.',1)
- if nm == name:
- name = n
- break
- elif not '.' in name:
- # Find it in this module.
- name = "%s.%s" % (request.__viewmodule__, name)
-
- if not name in app._names:
- raise NameError("Cannot find route %r." % name)
-
- route = app._names[name]
- names, namegen = app._routes[route][-2:]
-
- out = []
- for n in names:
- out.append(str(values[n]))
- del values[n]
- out = tuple(out)
-
- if len(out) == 1:
- out = namegen % out[0]
- else:
- out = namegen % out
- out = urllib.quote(out)
-
- if '_external' in values:
- if values['_external']:
- out = '%s://%s%s' % (request.protocol, request.host, out)
- del values['_external']
-
- if values:
- out += '?%s' % urllib.urlencode(values)
-
- return out
View
24 pants/contrib/web/__init__.py
@@ -0,0 +1,24 @@
+###############################################################################
+#
+# Copyright 2011 Pants Developers (see AUTHORS.txt)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+###############################################################################
+
+###############################################################################
+# Imports
+###############################################################################
+
+from application import *
+from fileserver import *
View
764 pants/contrib/web/application.py
@@ -0,0 +1,764 @@
+###############################################################################
+#
+# Copyright 2011 Pants Developers (see AUTHORS.txt)
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+###############################################################################
+
+###############################################################################
+# Imports
+###############################################################################
+
+import inspect
+import traceback
+
+from pants.contrib.http import HTTPServer
+from pants.contrib.web.utils import *
+
+try:
+ import simplejson as json
+except ImportError:
+ import json
+
+###############################################################################
+# Application Class
+###############################################################################
+
+class Application(object):
+ """
+ The Application class builds upon :class:`pants.contrib.http.HTTPServer`,
+ adding support for request routing, additional error handling, and a
+ degree of convenience that makes writing dynamic pages easier.
+
+ Instances of Application are callable, and may be used as a HTTPServer's
+ request handler.
+
+ =============== ============
+ Argument Description
+ =============== ============
+ default_domain *Optional.* The default domain to search for a route for if the request's Host does not exist.
+ debug *Optional.* If this is set to True, automatically generated ``500 Internal Server Error`` response pages will include information about the failed request, including a traceback of the exception that caused the page to be generated.
+ =============== ============
+ """
+ current_app = None
+
+ def __init__(self, default_domain=None, debug=False):
+ # Internal Stuff
+ self._routes = {}
+ self._names = {}
+
+ self._routes[None] = {}
+
+ # External Stuff
+ self.default_domain = default_domain
+ self.debug = debug
+
+ def run(self, port=None, host='', ssl_options=None):
+ """
+ This function exists for convenience, and when called creates a
+ :class:`~pants.contrib.http.HTTPServer` instance with its request
+ handler set to this application instance, calls
+ :func:`~pants.contrib.http.HTTPServer.listen` on that HTTPServer, and
+ finally, starts the Pants engine to process requests.
+
+ ============ ============
+ Argument Description
+ ============ ============
+ port *Optional.* The port to listen on. If this isn't specified, it will be either 80 or 443, depending on whether or not SSL options for the server have been provided.
+ host *Optional.* The host interface to listen on. If this isn't specified, listen on all interfaces.
+ ssl_options *Optional.* A dict of SSL options for the server. See :class:`pants.contrib.ssl.SSLServer` for more information.
+ ============ ============
+ """
+ from pants import engine
+ HTTPServer(self, ssl_options=ssl_options).listen(port, host)
+ engine.start()
+
+ ##### Route Management Methods ############################################
+
+ def basic_route(self, rule, name=None, methods=('GET','HEAD')):
+ """
+ The basic_route decorator registers a route with the Application without
+ holding your hand over it.
+
+ It functions almost the same as the :func:`Application.route` decorator,
+ but doesn't wrap the provided function with any argument handling code.
+ Instead, you're provided with the request object and the the regex
+ match object.
+
+ Example Usage::
+
+ @app.basic_route("/char/<char>")
+ def my_route(request):
+ char, = request.match.groups()
+ return 'The character is %s!' % char
+
+ That is, essentially, equivilent to::
+
+ @app.route("/char/<char>/")
+ def my_route(char):
+ return 'The character is %s!' % char
+
+ ========= ============
+ Argument Description
+ ========= ============
+ rule The route rule to match for a request to go to the decorated function. See :func:`Application.route` for more information.
+ name *Optional.* The name of the decorated function, for use with the :func:`url_for` helper function.
+ methods *Optional.* A list of HTTP methods to allow for this request handler. By default, only ``GET`` and ``HEAD`` requests are allowed, and all others will result in a ``405 Method Not Allowed`` error.
+ ========= ============
+ """
+ def decorator(func):
+ if rule[0] != '/':
+ domain, _, _rule = rule.partition('/')
+ _rule = '/' + rule
+ else:
+ domain = None
+ _rule = rule
+
+ regex, arguments, names, namegen = _route_to_regex(_rule)
+ _regex = re.compile(regex)
+
+ _name = name
+ if _name is None:
+ _name = "%s.%s" % (func.__module__, func.__name__)
+
+ if not hasattr(func, 'content_type'):
+ func.content_type = None
+
+ self._insert_route(_regex, func, domain, _name, methods, names, namegen)
+ return func
+ return decorator
+
+ def route(self, rule, name=None, methods=('GET','HEAD'), auto404=False,
+ content_type=None):
+ """
+ The route decorator is used to register a new request handler with the
+ Application instance. Example::
+
+ @app.route("/")
+ def hello_world():
+ return "Hiya, Everyone!"
+
+ Variables may be specified in the route *rule* by wrapping them with
+ inequality signs (for example: ``<variable_name>``). By default, a
+ variable part accepts any character except a slash (``/``) and returns
+ a string value. However, you may specify a specific type to be returned
+ by using the format ``<type:name>``, where type is the name of a
+ callable in the pants.contrib.web namespace that accepts a single
+ string as its argument, and returns a value. Built-in types, such as
+ int and float, work well for this. Example::
+
+ @app.route("/user/<int:id>/")
+ def user(id):
+ return "Hi, user %d!" % id
+
+ The ``id`` is automatically converted into an integer for you, and as
+ an added bonus, your function is never even called if the provided
+ value for ``id`` isn't a valid number.
+
+ Request handlers are easy to write and can send their output to the
+ client simply by returning a value, such as a string::
+
+ @app.route("/")
+ def hello_world():
+ return "Hiya, Everyone!"
+
+ The previous code would result in a `200 OK`` response, with a
+ ``Content-Type`` header of ``text/plain``, and a ``Content-Length``
+ header of ``15``. With, of course, the body ``Hiya, Everyone!``.
+
+ If the returned string begins with ``<!DOCTYPE`` or ``<html``, it will
+ be assumed that the response is of ``Content-Type: text/html``.
+
+ If a unicode object is returned, rather than a simple string, it will
+ be automatically encoded and an encoding argument will be added to the
+ ``Content-Type`` header.
+
+ If a dictionary is returned, it will be automatically converted to a
+ string of `JSON <http://en.wikipedia.org/wiki/JSON>`_ and the
+ ``Content-Type`` header will be set to ``application/json``.
+
+ If any other object is returned, it will be converted to a string
+ via ``str()`` before any content headers are set. The exception to this
+ is that, if the object has a ``__html__`` method, that method will be
+ called rather than ``str()``, and the ``Content-Type`` will be
+ automatically assumed to be ``text/html``, regardless of the actual
+ content of the string.
+
+ A tuple of ``(body, status)`` or ``(body, status, headers)`` may be
+ returned, rather than simply a body, to set the HTTP status code of
+ the result and additional response headers. If provided, ``status``
+ must be an integer, and ``headers`` must be a dict.
+
+ The following example returns a page with the status code ``404 Not Found``::
+
+ @app.route("/nowhere")
+ def nowhere():
+ return "This does not exist.", 404
+
+ ============= ============
+ Argument Description
+ ============= ============
+ rule The route rule to be matched for the decorated function to be used for handling a request.
+ name *Optional.* The name of the decorated function, for use with the :func:`url_for` helper function.
+ methods *Optional.* A list of HTTP methods to allow for this request handler. By default, only ``GET`` and ``HEAD`` requests are allowed, and all others will result in a ``405 Method Not Allowed`` error.
+ auto404 *Optional.* If this is set to True, all response handler arguments will be checked for truthiness (True, non-empty strings, etc.) and, if any fail, a ``404 Not Found`` page will be rendered automatically.
+ content_type *Optional.* If set, the ``Content-Type`` header will default to this unless returned as part of a header dict from the view function.
+ ============= ============
+ """
+ if callable(name):
+ self._add_route(rule, name, None, methods, auto404, content_type)
+ return
+
+ def decorator(func):
+ self._add_route(rule, func, name, methods, auto404, content_type)
+ return func
+ return decorator
+
+ ##### Error Handlers ######################################################
+
+ def handle_404(self, request, exception):
+ if isinstance(exception, HTTPException):
+ return error(exception.message, 404, request=request)
+ return error(404, request=request)
+
+ def handle_500(self, request, exception):
+ log.exception('Error handling HTTP request: %s %%s' % request.method,
+ request.uri)
+ if not self.debug:
+ return error(500)
+
+ resp = u''.join([
+ u"<h2>Traceback</h2>\n",
+ u"<pre>%s</pre>\n" % traceback.format_exc(),
+ u"<h2>Route</h2>\n<pre>",
+ u"route name = %r\n" % request.route_name,
+ u"match groups = %r" % (request.match.groups(),),
+ u"</pre>\n",
+ u"<h2>HTTP Request</h2>\n",
+ request.__html__(),
+ ])
+
+ return error(resp, 500)
+
+ ##### The Request Handler #################################################
+
+ def __call__(self, request):
+ """
+ This function is called when a new request is received, and calls both
+ :func:`Application.handle_request` and :func:`Application.handle_output`
+ to process the request.
+ """
+ Application.current_app = self
+ self.request = request
+
+ try:
+ request.auto_finish = True
+ self.handle_output(*self.handle_request(request))
+ finally:
+ request.route = None
+ request.match = None
+ request.route_name = None
+
+ Application.current_app = None
+ self.request = None
+
+ def handle_output(self, result, content_type):
+ """ Process the output of handle_request. """
+ request = self.request
+
+ if not request.auto_finish or result is None or \
+ request._finish is not None:
+ if request.auto_finish and request._finish is None:
+ request.finish()
+ return
+
+ status = 200
+ if type(result) is tuple:
+ if len(result) == 3:
+ body, status, headers = result
+ else:
+ body, status = result
+ headers = {}
+ else:
+ body = result
+ headers = {}
+
+ # Set a Content-Type header if there isn't already one.
+ if not 'Content-Type' in headers:
+ if content_type is not None:
+ headers['Content-Type'] = content_type
+ elif (isinstance(body, basestring) and
+ body[:5].lower() in ('<html','<!doc')) or \
+ hasattr(body, 'to_html'):
+ headers['Content-Type'] = 'text/html'
+ elif isinstance(body, dict):
+ headers['Content-Type'] = 'application/json'
+ else:
+ headers['Content-Type'] = 'text/plain'
+
+ # Convert the body to something sendable.
+ try:
+ body = body.to_html()
+ except AttributeError:
+ pass
+
+ if isinstance(body, unicode):
+ encoding = headers['Content-Type']
+ if 'charset=' in encoding:
+ before, sep, enc = encoding.partition('charset=')
+ else:
+ before = encoding
+ sep = '; charset='
+ enc = 'UTF-8'
+
+ body = body.encode(enc)
+ headers['Content-Type'] = '%s%s%s' % (before, sep, enc)
+
+ elif isinstance(body, dict):
+ try:
+ body = json.dumps(body)
+ except Exception, e:
+ body, status, headers = self.handle_500(request, e)
+ body = body.encode('utf-8')
+ headers['Content-Type'] = 'text/html; charset=UTF-8'
+
+ elif not isinstance(body, str):
+ body = str(body)
+
+ # More headers!
+ headers['Content-Length'] = len(body)
+ if not 'Date' in headers:
+ headers['Date'] = date(datetime.utcnow())
+ if not 'Server' in headers:
+ headers['Server'] = SERVER
+
+ # Send the response.
+ request.send_status(status)
+ request.send_headers(headers)
+
+ if request.method == 'HEAD':
+ request.finish()
+ return
+
+ request.write(body)
+ request.finish()
+
+ def handle_request(self, request):
+ path = request.path
+
+ # Domain Matching
+ if len(self._routes) == 1:
+ domain = None
+ else:
+ if request.host in self._routes:
+ domain = request.host
+ else:
+ domain = '.' + request.host.partition('.')[2]
+ if not domain in self._routes and ':' in request.host:
+ domain = request.host.rpartition(':')[0]
+ if not domain in self._routes:
+ domain = '.' + domain.partition('.')[2]
+ if not domain in self._routes:
+ domain = self.default_domain
+
+ for route in self._routes[domain]:
+ match = route.match(path)
+ if match is None:
+ continue
+
+ # Process this route.
+ func, name, methods = self._routes[domain][route][:3]
+
+ request.route = route
+ request.match = match
+ request.route_name = name
+
+ if request.method not in methods:
+ return error(
+ 'The method %s is not allowed for %r.' % (
+ request.method, path), 405, {
+ 'Allow': ', '.join(methods)
+ }), None
+ else: