<?xml version="1.0" encoding="UTF-8"?>
<commit>
  <added type="array">
    <added>
      <filename>shrub/helpers/__init__.py</filename>
    </added>
    <added>
      <filename>shrub/response/__init__.py</filename>
    </added>
    <added>
      <filename>shrub/response/base.py</filename>
    </added>
    <added>
      <filename>shrub/response/sax/__init__.py</filename>
    </added>
    <added>
      <filename>shrub/response/sax/bucket.py</filename>
    </added>
    <added>
      <filename>shrub/response/sax/object.py</filename>
    </added>
    <added>
      <filename>shrub/views/rss.mako</filename>
    </added>
    <added>
      <filename>shrub/views/xspf.mako</filename>
    </added>
  </added>
  <modified type="array">
    <modified>
      <diff>@@ -11,103 +11,107 @@ from mako.lookup import TemplateLookup
 
 import simplejson
 
-from shrub import feeds
+import shrub
+
+
+class PrintEnvironmentHandler(webapp.RequestHandler):
+	def get(self):
+		for name in os.environ.keys():
+			self.response.out.write(&quot;%s = %s&lt;br /&gt;\n&quot; % (name, os.environ[name]))
 
-class ShrubException(Exception):
-  def __init__(self, code, message):
-    super(Exception, self).__init__()
-    self.code = code
-    self.message = message
 
 class BasePage(webapp.RequestHandler):
-  &quot;&quot;&quot;Base request handler to provide template lookup and rendering&quot;&quot;&quot;
-  
-  def __init__(self):
-    super(BasePage, self).__init__()
-    self._template_lookup = None
-  
-  def view_path(self):
-    return os.path.join(os.path.dirname(__file__), &quot;..&quot;, &quot;views&quot;)
-    
-  @property
-  def template_lookup(self):
-    if not self._template_lookup:
-      self._template_lookup = TemplateLookup(directories=[self.view_path(), feeds.view_path()], output_encoding='utf-8')
-    return self._template_lookup
-    
-  def get_template(self, name):
-    return self.template_lookup.get_template(name)
-  
-  def set_content_type(self, content_type):
-    self.response.headers['Content-Type'] = content_type
-    
-  def render(self, name, values, content_type=None, cache_key=None):
-    template = self.get_template(name)
-    if content_type:
-      self.set_content_type(content_type)
-      
-    try:
-      self.render_text(template.render(**values), cache_key=cache_key)
-    except:
-      self.render_text(exceptions.html_error_template().render())
-      
-  def render_text(self, text, cache_key=None):
-    if cache_key: 
-      # Cache for 5 minutes
-      memcache.add(cache_key, text, 5 * 60)
-      
-    self.response.out.write(text)
-    
-  def render_with_cache(self, cache_key, content_type=None):
-    data = memcache.get(cache_key)
-    if data is not None:
-      if content_type: self.set_content_type(content_type)
-      self.render_text(data)
-      return True    
-    return False
+	&quot;&quot;&quot;Base request handler to provide template lookup and rendering&quot;&quot;&quot;
+	
+	def __init__(self):
+		super(BasePage, self).__init__()
+		self._template_lookup = None
+		
+	def view_path(self):
+		return os.path.join(os.path.dirname(__file__), &quot;..&quot;, &quot;views&quot;)
+		
+	@property
+	def template_lookup(self):
+		if not self._template_lookup:
+			self._template_lookup = TemplateLookup(directories=[self.view_path()] + shrub.view_paths(), output_encoding='utf-8')
+		return self._template_lookup
+		
+	def get_template(self, name):
+		return self.template_lookup.get_template(name)
+		
+	def set_content_type(self, content_type):
+		self.response.headers['Content-Type'] = content_type
+		
+	def render(self, name, values, content_type=None, cache_key=None):
+		template = self.get_template(name)
+		if content_type:
+			self.set_content_type(content_type)
+			
+		try:
+			self.render_text(template.render(**values), cache_key=cache_key)
+		except:
+			self.render_text(exceptions.html_error_template().render())
+			
+	def render_text(self, text, cache_key=None):
+		if cache_key:
+		# Cache for 5 minutes
+			memcache.add(cache_key, text, 5 * 60)
+			
+		self.response.out.write(text)
+		
+	def render_with_cache(self, cache_key, content_type=None):
+		data = memcache.get(cache_key)
+		if data is not None:
+			if content_type: self.set_content_type(content_type)
+			self.render_text(data)
+			return True
+		return False
 
-class BaseResponse(object):
-  &quot;&quot;&quot;Base response when using a front controller&quot;&quot;&quot;
 
-  def __init__(self, request_handler):
-    self.request_handler = request_handler
-    self.request = request_handler.request
+class BaseResponse(object):
+	&quot;&quot;&quot;Base response when using a front controller&quot;&quot;&quot;
+	
+	def __init__(self, request_handler):
+		self.request_handler = request_handler
+		self.request = request_handler.request
+		
+	def render(self, name, values, content_type=None, cache_key=None):
+		self.request_handler.render(name, values, content_type=content_type, cache_key=cache_key)
 
-  def render(self, name, values, content_type=None, cache_key=None):
-    self.request_handler.render(name, values, content_type=content_type, cache_key=cache_key)
 
 class JSONResponse(BaseResponse):
-  
-  ContentType = &quot;text/javascript; charset=utf-8&quot;
-
-  def _wrap_in_callback(self, data, callback):
-    
-    # Callback function names may only use upper and lowercase alphabetic characters (A-Z, a-z), 
-    # numbers (0-9), the period (.), the underscore (_)
-    
-    if not re.match(&quot;^[a-zA-Z0-9._]+$&quot;, callback):
-      raise ShrubException(&quot;InvalidCallback&quot;, &quot;Callback contains invalid characters&quot;)
-    
-    return &quot;%s(%s)&quot; % (callback, data)
 
-  def render_json(self, value, cache_key=None, callback=None):
-    json = simplejson.dumps(value)
-    self.request_handler.set_content_type(self.ContentType)
-    if callback:
-      json = self._wrap_in_callback(json, callback)
-    self.request_handler.render_text(json, cache_key=cache_key)
+	ContentType = &quot;text/javascript; charset=utf-8&quot;
+	
+	def _wrap_in_callback(self, data, callback):
+	
+	# Callback function names may only use upper and lowercase alphabetic characters (A-Z, a-z),
+	# numbers (0-9), the period (.), the underscore (_)
+	
+		if not re.match(&quot;^[a-zA-Z0-9._]+$&quot;, callback):
+			raise shrub.ShrubException(&quot;InvalidCallback&quot;, &quot;Callback contains invalid characters&quot;)
+			
+		return &quot;%s(%s)&quot; % (callback, data)
+		
+	def render_json(self, value, cache_key=None, callback=None):
+		json = simplejson.dumps(value)
+		self.request_handler.set_content_type(self.ContentType)
+		if callback:
+			json = self._wrap_in_callback(json, callback)
+		self.request_handler.render_text(json, cache_key=cache_key)
+		
+	def render_json_from_cache(self, cache_key):
+		return self.request_handler.render_with_cache(cache_key, content_type=self.ContentType)
 
-  def render_json_from_cache(self, cache_key):
-    return self.request_handler.render_with_cache(cache_key, content_type=self.ContentType)
-  
-  def render_json_error(self, status_code, error):
-    self.request_handler.response.set_status(status_code)
-    self.render_json(dict(error=dict(code=error.code, message=error.message)))
+	def render_json_error(self, status_code, error):
+		self.request_handler.response.set_status(status_code)
+		self.render_json(dict(error=dict(code=error.code, message=error.message)))
+		
+	def handle(self, response):
+		callback = self.request.get(&quot;callback&quot;, None)
+		try:
+			self.render_json(response, callback=callback)
+		except shrub.ShrubException, e:
+			self.render_json_error(500, e)
+			
 
-  def handle(self, response):
-    callback = self.request.get(&quot;callback&quot;, None)
-    try:
-      self.render_json(response, callback=callback)
-    except ShrubException, e:
-      self.render_json_error(500, e)
-    </diff>
      <filename>app/controllers/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -19,184 +19,190 @@ from app.controllers import tape
 from shrub import feeds
 
 class DefaultPage(base.BasePage):
-  &quot;&quot;&quot;Home page&quot;&quot;&quot;
+	&quot;&quot;&quot;Home page&quot;&quot;&quot;
 
-  def get(self):
-    search = self.request.get('q')
-    if search:
-      self.redirect(&quot;/&quot; + search)
-      return
+	def get(self):
+		search = self.request.get('q')
+		if search:
+			self.redirect(&quot;/&quot; + search)
+			return
+			
+		self.render(&quot;index.mako&quot;, dict(title=&quot;Shrub / Amazon S3 Proxy&quot;))
 
-    self.render(&quot;index.mako&quot;, dict(title=&quot;Shrub / Amazon S3 Proxy&quot;))
 
 class S3Page(base.BasePage):
-  &quot;&quot;&quot;
+	&quot;&quot;&quot;
   Front controller for all S3 style requests (until I figure out how to do more advanced routing). 
   
   Request should be passed off based on their format or response type.
   &quot;&quot;&quot;
-  
-  def _get(self):  
-    max_keys = self.request.get('max-keys')
-    delimiter = self.request.get('delimiter', '/')
-    marker = self.request.get('marker', None)    
-    format = self.request.get('format', None)
-    
-    cache_key = self.request.url
-    
-    bucket_name, prefix = S3Utils.parse_request(self.request)    
-    
-    if not bucket_name:
-      handler = ErrorResponse(self)
-      handler.render_error(404)
-      return
-    
-    if format == 'id3-json':
-      url = 'http://%s/%s' % (S3.DefaultLocation, self.request.path)
-      tape.ID3Response(self).load_url(url, 'json', cache_key=cache_key)
-      return
-        
-    # Make S3 request
-    s3response = S3().list(bucket_name, max_keys, prefix, delimiter, marker)        
-      
-    # If not 2xx, show friendly error page
-    if not s3response.ok:
-      handler = ErrorResponse(self)
-      handler.handle(s3response)
-      return
-        
-    # If no format use HTML response
-    if not format:
-      handler = HTMLResponse(self)          
-      handler.handle(s3response)
-      return
-
-    # If truncated with a request format; return 501
-    if s3response.is_truncated:
-      handler = ErrorResponse(self)
-      handler.render_error(501, &quot;There were too many items ( &amp;gt; %s ) in the current bucket to sort and display.&quot; % s3response.max_keys)
-      return
-    
-    # Get handler for format
-    if format == 'rss': handler = RSSResponse(self)
-    elif format == 'xspf': handler = tape.XSPFResponse(self)
-    elif format == 'tape': handler = tape.TapeResponse(self)
-    elif format == 'json': handler = JSONResponse(self)
-    elif format == 'error': handler = ErrorResponse(self)
-    else:
-      # If no handler for a format return error page
-      handler = ErrorResponse(self)
-      handler.render_error(404, &quot;The requested format parameter is unknown.&quot;, title=&quot;Not found&quot;)
-      return
-      
-    # Render response with handler
-    handler.handle(s3response)
-
-  def get(self):
-    try:
-      self._get()
-    except DeadlineExceededError:
-      self.response.clear()
-      ErrorResponse(self).render_error(500, &quot;The request couldn't be completed in time. Please try again.&quot;)
-
+	
+	def _get(self):
+		max_keys = self.request.get('max-keys')
+		delimiter = self.request.get('delimiter', '/')
+		marker = self.request.get('marker', None)
+		format = self.request.get('format', None)
+		
+		cache_key = self.request.url
+		
+		bucket_name, prefix = S3Utils.parse_gae_request(self.request)
+		
+		if not bucket_name:
+			handler = ErrorResponse(self)
+			handler.render_error(404)
+			return
+			
+		if format == 'id3-json':
+			url = 'http://%s/%s' % (S3.DefaultLocation, self.request.path)
+			tape.ID3Response(self).load_url(url, 'json', cache_key=cache_key)
+			return
+			
+			# Make S3 request
+		s3response = S3().list(bucket_name, max_keys, prefix, delimiter, marker)
+		
+		# If not 2xx, show friendly error page
+		if not s3response.ok:
+			handler = ErrorResponse(self)
+			handler.handle(s3response)
+			return
+			
+			# If no format use HTML response
+		if not format:
+			handler = HTMLResponse(self)
+			handler.handle(s3response)
+			return
+			
+			# If truncated with a request format; return 501
+		if s3response.is_truncated:
+			handler = ErrorResponse(self)
+			handler.render_error(501, &quot;There were too many items ( &amp;gt; %s ) in the current bucket to sort and display.&quot; % s3response.max_keys)
+			return
+			
+			# Get handler for format
+		if format == 'rss': handler = RSSResponse(self)
+		elif format == 'xspf': handler = tape.XSPFResponse(self)
+		elif format == 'tape': handler = tape.TapeResponse(self)
+		elif format == 'json': handler = JSONResponse(self)
+		elif format == 'error': handler = ErrorResponse(self)
+		else:
+		# If no handler for a format return error page
+			handler = ErrorResponse(self)
+			handler.render_error(404, &quot;The requested format parameter is unknown.&quot;, title=&quot;Not found&quot;)
+			return
+			
+			# Render response with handler
+		handler.handle(s3response)
+		
+	def get(self):
+		try:
+			self._get()
+		except DeadlineExceededError:
+			self.response.clear()
+			ErrorResponse(self).render_error(500, &quot;The request couldn't be completed in time. Please try again.&quot;)
+			
+			
 class HTMLResponse(base.BaseResponse):
-  
-  def handle(self, s3response):        
-    files = s3response.files
-    path_components = s3response.path_components
-    path = s3response.path
-    warning_message = None
-
-    if s3response.is_truncated:
-      sort = 'name'
-      sort_asc = True
-      if self.request.get('s', None) is not None:
-        warning_message = 'Because the result was truncated, the sort option was ignored.'
-    else:
-      # Sort files
-      sort = self.request.get('s', 'name')
-      sort_asc = True
-      if sort.endswith('-desc'): 
-        sort = sort.replace('-desc', '', 1)
-        sort_asc = False    
-    
-    files.sort(cmp=lambda x, y: S3Utils.file_comparator(x, y, sort, sort_asc))
-  
-    # Render response
-    template_values = {
-      'title': path,
-      'path_components': path_components,
-      'path': path,
-      'sort': sort,
-      'sort_asc': sort_asc,
-      's3response': s3response,
-      'warning_message': warning_message
-    }
-  
-    self.render(&quot;list.mako&quot;, template_values)
 
+	def handle(self, s3response):
+		files = s3response.files
+		path_components = s3response.path_components
+		path = s3response.path
+		warning_message = None
+		
+		if s3response.is_truncated:
+			sort = 'name'
+			sort_asc = True
+			if self.request.get('s', None) is not None:
+				warning_message = 'Because the result was truncated, the sort option was ignored.'
+		else:
+		# Sort files
+			sort = self.request.get('s', 'name')
+			sort_asc = True
+			if sort.endswith('-desc'):
+				sort = sort.replace('-desc', '', 1)
+				sort_asc = False
+				
+		files.sort(cmp=lambda x, y: S3Utils.file_comparator(x, y, sort, sort_asc))
+		
+		# Render response
+		template_values = {
+		'title': path,
+		'path_components': path_components,
+		'path': path,
+		'sort': sort,
+		'sort_asc': sort_asc,
+		's3response': s3response,
+		'warning_message': warning_message
+		}
+		
+		self.render(&quot;list.mako&quot;, template_values)
+		
+		
 class JSONResponse(base.JSONResponse):
-  
-  def handle(self, s3response):
-    super(JSONResponse, self).handle(s3response.data)
 
+	def handle(self, s3response):
+		super(JSONResponse, self).handle(s3response.data)
+		
+		
 class RSSResponse(base.BaseResponse):
 
-  def handle(self, s3response):    
-    files = s3response.files
-    path = s3response.path
-
-    rss_items = []
-    files.sort(cmp=lambda x, y: S3Utils.file_comparator(x, y, 'date', False))
-
-    for file in files[:50]:
-      rss_items.append(file.to_rss_item())
-
-    pub_date = datetime.datetime.now()
-    if len(rss_items) &gt; 0:
-      pub_date = rss_items[0].pub_date
-
-    title = u'%s (Shrub)' % path
-    link = &quot;http://s3hub.appspot.com/%s&quot; % path
-
-    assigns = dict(title=title, description=u'RSS feed for %s' % s3response.url, items=rss_items, link=link, pub_date=pub_date)
-    self.render(&quot;rss.mako&quot;, assigns, 'text/xml;charset=utf-8')
-    
+	def handle(self, s3response):
+		files = s3response.files
+		path = s3response.path
+		
+		rss_items = []
+		files.sort(cmp=lambda x, y: S3Utils.file_comparator(x, y, 'date', False))
+		
+		for file in files[:50]:
+			rss_items.append(file.to_rss_item())
+			
+		pub_date = datetime.datetime.now()
+		if len(rss_items) &gt; 0:
+			pub_date = rss_items[0].pub_date
+			
+		title = u'%s (Shrub)' % path
+		link = &quot;http://s3hub.appspot.com/%s&quot; % path
+		
+		assigns = dict(title=title, description=u'RSS feed for %s' % s3response.url, items=rss_items, link=link, pub_date=pub_date)
+		self.render(&quot;rss.mako&quot;, assigns, 'text/xml;charset=utf-8')
+		
+		
 class ErrorResponse(base.BaseResponse):
-  &quot;&quot;&quot;Handle standard error response.&quot;&quot;&quot;
-
-  def render_error(self, status_code, error_message=None, title=&quot;Error&quot;):    
-    self.request_handler.response.set_status(status_code)
-    self.request_handler.render(&quot;error.mako&quot;, dict(title=title, s3url=None, status_code=status_code, message=error_message, path=None))
-
-  def handle(self, s3response):
-    title = None
-    message = None
-    url = s3response.url
-    request = self.request
-    request_url = request.url if request else None
-
-    status_code = s3response.status_code
-    error_message = s3response.message
-
-    if status_code == 403:
-      title = 'Permission denied'
-      message = 'Shrub does not have permission to access this bucket. Shrub can only act on public buckets.'
-    elif status_code == 404:
-      title = 'Not found'
-      message = 'This bucket or folder was not found. Try verifying that it exists.'
-    elif status_code in range(400, 500):
-      title = 'Client error'
-      message = 'There was an error trying to access S3.'
-    elif status_code in range(500, 600):
-      title = 'Not available. Please try again.'
-      message = 'There was an error trying to access S3. Please try again.' 
-    else:
-      title = 'Unknown error'
-      message = 'There was an unknown error.'
-
-    if error_message:
-      message += ' (%s)' % error_message
+	&quot;&quot;&quot;Handle standard error response.&quot;&quot;&quot;
+	
+	def render_error(self, status_code, error_message=None, title=&quot;Error&quot;):
+		self.request_handler.response.set_status(status_code)
+		self.request_handler.render(&quot;error.mako&quot;, dict(title=title, s3url=None, status_code=status_code, message=error_message, path=None))
+		
+	def handle(self, s3response):
+		title = None
+		message = None
+		url = s3response.url
+		request = self.request
+		request_url = request.url if request else None
+		
+		status_code = s3response.status_code
+		error_message = s3response.message
+		
+		if status_code == 403:
+			title = 'Permission denied'
+			message = 'Shrub does not have permission to access this bucket. Shrub can only act on public buckets.'
+		elif status_code == 404:
+			title = 'Not found'
+			message = 'This bucket or folder was not found. Try verifying that it exists.'
+		elif status_code in range(400, 500):
+			title = 'Client error'
+			message = 'There was an error trying to access S3.'
+		elif status_code in range(500, 600):
+			title = 'Not available. Please try again.'
+			message = 'There was an error trying to access S3. Please try again.'
+		else:
+			title = 'Unknown error'
+			message = 'There was an unknown error.'
+			
+		if error_message:
+			message += ' (%s)' % error_message
+			
+		self.request_handler.response.set_status(status_code)
+		self.request_handler.render(&quot;error.mako&quot;, dict(title=title, s3url=url, status_code=status_code, message=message, request_url=request_url))
 
-    self.request_handler.response.set_status(status_code)
-    self.request_handler.render(&quot;error.mako&quot;, dict(title=title, s3url=url, status_code=status_code, message=message, request_url=request_url))</diff>
      <filename>app/controllers/s3.py</filename>
    </modified>
    <modified>
      <diff>@@ -9,81 +9,81 @@ from shrub.utils import S3Utils
 from app.controllers.base import BaseResponse, JSONResponse
 
 class TapeResponse(BaseResponse):
-  
-  def __init__(self, request_handler):
-    self.request_handler = request_handler
 
-  def handle(self, s3response):
-    
-    list_url = self.request_handler.request.path_url
-    xspf_url = '%s?format=xspf' % list_url
-    
-    tracks = []
-    id3_urls = []
-    for file in s3response.files:
-      if file.extension == 'mp3':
-        tracks.append(file.xspf_track)
-        id3_urls.append('%s?format=id3-json' % file.appspot_url)
-          
-    values = dict(title='Mix Tape (%s)' % s3response.path, xspf_url=xspf_url, list_url=list_url, tracks=tracks, id3_urls=id3_urls)
-    
-    self.render(&quot;muxtape.mako&quot;, values)
-    
-    
+	def __init__(self, request_handler):
+		self.request_handler = request_handler
+		
+	def handle(self, s3response):
+	
+		list_url = self.request_handler.request.path_url
+		xspf_url = '%s?format=xspf' % list_url
+		
+		tracks = []
+		id3_urls = []
+		for file in s3response.files:
+			if file.extension == 'mp3':
+				tracks.append(file.xspf_track)
+				id3_urls.append('%s?format=id3-json' % file.appspot_url)
+				
+		values = dict(title='Mix Tape (%s)' % s3response.path, xspf_url=xspf_url, list_url=list_url, tracks=tracks, id3_urls=id3_urls, s3response=s3response)
+		
+		self.render(&quot;muxtape.mako&quot;, values)
+		
+		
 class XSPFResponse(BaseResponse):
-  
-  def handle(self, s3response):    
-    url = s3response.url
-    files = s3response.files
-    path = s3response.path
-    
-    files.sort(cmp=lambda x, y: S3Utils.file_comparator(x, y, 'name', True))
-    
-    tracks = []
-    
-    for file in files:
-      tracks.append(file.xspf_track)      
-      
-    title = u'%s (XSPF)' % path
-    
-    values = dict(title=title, creator='Shrub', info='http://shrub.appspot.com', location=url, tracks=tracks)
-  
-    self.render(&quot;xspf.mako&quot;, values, 'text/xml; charset=utf-8')
-    
-    
+
+	def handle(self, s3response):
+		url = s3response.url
+		files = s3response.files
+		path = s3response.path
+		
+		files.sort(cmp=lambda x, y: S3Utils.file_comparator(x, y, 'name', True))
+		
+		tracks = []
+		
+		for file in files:
+			tracks.append(file.xspf_track)
+			
+		title = u'%s (XSPF)' % path
+		
+		values = dict(title=title, creator='Shrub', info='http://shrub.appspot.com', location=url, tracks=tracks)
+		
+		self.render(&quot;xspf.mako&quot;, values, 'text/xml; charset=utf-8')
+		
+		
 class ID3Response(JSONResponse):
-  
-  def load_url(self, url, format='json', cache_key=None):
-    
-    if self.render_json_from_cache(cache_key):
-      return
-    
-    callback = self.request.get(&quot;callback&quot;, None)
-    
-    logging.info(&quot;Loading url: %s&quot; % url)
-    fetch_headers = dict(Range='bytes=0-1024') 
-    response = urlfetch.fetch(url, headers=fetch_headers, allow_truncated=True)
-    
-    try:
-      data = id3data.ID3Data(response.content)
-      id3r = id3reader.Reader(data, only_v2=True) 
-      
-      if not id3r.found:
-        self.render_json(dict(error='Not found'))
-        return
-            
-      values = dict(album=id3r.getValue('album'),
-        performer=id3r.getValue('performer'),
-        title=id3r.getValue('title'),
-        track=id3r.getValue('track'),
-        year=id3r.getValue('year'),
-        isTruncated=id3r.is_truncated,
-      )
 
-      if format == 'json':        
-        self.render_json(values, cache_key=cache_key, callback=callback)
-        
-    except id3reader.Id3Error, detail:
-      self.render_json(dict(error=str(detail)))
-    
-    
\ No newline at end of file
+	def load_url(self, url, format='json', cache_key=None):
+	
+		if self.render_json_from_cache(cache_key):
+			return
+			
+		callback = self.request.get(&quot;callback&quot;, None)
+		
+		logging.info(&quot;Loading url: %s&quot; % url)
+		fetch_headers = dict(Range='bytes=0-1024')
+		response = urlfetch.fetch(url, headers=fetch_headers, allow_truncated=True)
+		
+		try:
+			data = id3data.ID3Data(response.content)
+			id3r = id3reader.Reader(data, only_v2=True)
+			
+			if not id3r.found:
+				self.render_json(dict(error='Not found'))
+				return
+				
+			values = dict(album=id3r.getValue('album'),
+			performer=id3r.getValue('performer'),
+			title=id3r.getValue('title'),
+			track=id3r.getValue('track'),
+			year=id3r.getValue('year'),
+			isTruncated=id3r.is_truncated,
+			)
+			
+			if format == 'json':
+				self.render_json(values, cache_key=cache_key, callback=callback)
+				
+		except id3reader.Id3Error, detail:
+			self.render_json(dict(error=str(detail)))
+			
+</diff>
      <filename>app/controllers/tape.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,7 +2,8 @@ import os
 import simplejson
 
 def current_version(context):
-  return str(os.environ.get('CURRENT_VERSION_ID','Unknown'))
-
+	return str(os.environ.get('CURRENT_VERSION_ID', 'Unknown'))
+	
 def to_json(context, value):
-  return simplejson.dumps(value)
+	return simplejson.dumps(value)
+</diff>
      <filename>app/helpers/base.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,27 +1,28 @@
 def header_link(context, label, name, sort, sort_asc, path):
 
-  icon = ''
-  class_ = ''
-  sort_attr = '%s-desc' % name
-
-  if sort == name:
-    if not sort_asc:
-      class_ = 'asc'
-      sort_attr = name    
-      icon = 'bullet_arrow_down.png'
-    else:
-      class_ = 'desc'
-      sort_attr = '%s-desc' % name
-      icon = 'bullet_arrow_up.png'
-
-  context.write('''&lt;th class=&quot;sorted %s %s&quot; onclick=&quot;document.location.href='/%s/?s=%s'&quot;&gt;''' % (class_, name, path, sort_attr))
-  context.write('''&lt;a href=&quot;/%s/?s=%s&quot;&gt;%s&lt;/a&gt;''' % (path, sort_attr, label))
-
-  if icon: context.write('&lt;img src=&quot;/shrub/images/%s&quot;/&gt;&lt;/th&gt;' % icon)
-  return ''
-
+	icon = ''
+	class_ = ''
+	sort_attr = '%s-desc' % name
+	
+	if sort == name:
+		if not sort_asc:
+			class_ = 'asc'
+			sort_attr = name
+			icon = 'bullet_arrow_down.png'
+		else:
+			class_ = 'desc'
+			sort_attr = '%s-desc' % name
+			icon = 'bullet_arrow_up.png'
+			
+	context.write('''&lt;th class=&quot;sorted %s %s&quot; onclick=&quot;document.location.href='/%s/?s=%s'&quot;&gt;''' % (class_, name, path, sort_attr))
+	context.write('''&lt;a href=&quot;/%s/?s=%s&quot;&gt;%s&lt;/a&gt;''' % (path, sort_attr, label))
+	
+	if icon: context.write('&lt;img src=&quot;/shrub/images/%s&quot;/&gt;&lt;/th&gt;' % icon)
+	return ''
+	
 def if_even(context, n, if_label, else_label):
-  if n % 2 == 0:
-    return if_label
-  else:
-    return else_label
+	if n % 2 == 0:
+		return if_label
+	else:
+		return else_label
+</diff>
      <filename>app/helpers/list.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,4 +2,4 @@
 &lt;%namespace name=&quot;base&quot; module=&quot;app.helpers.base&quot;/&gt;
 
 &lt;hr/&gt;
-&lt;p&gt;&lt;a href=&quot;http://shrub.appspot.com/&quot;&gt;Shrub&lt;/a&gt;/1.2.3 &amp;copy; 2008 &amp;mdash; &lt;a href=&quot;http://rel.me&quot;&gt;rel.me&lt;/a&gt; (Gabriel Handford) &amp;mdash; Shrub is &lt;a href=&quot;http://github.com/gabriel/shrub&quot;&gt;open source&lt;/a&gt;&lt;/p&gt;
\ No newline at end of file
+&lt;p&gt;&lt;a href=&quot;http://shrub.appspot.com/&quot;&gt;Shrub&lt;/a&gt;/1.2.4 &amp;copy; 2008 &amp;mdash; &lt;a href=&quot;http://rel.me&quot;&gt;rel.me&lt;/a&gt; (Gabriel Handford) &amp;mdash; Shrub is &lt;a href=&quot;http://github.com/gabriel/shrub&quot;&gt;open source&lt;/a&gt;&lt;/p&gt;
\ No newline at end of file</diff>
      <filename>app/views/footer.mako</filename>
    </modified>
    <modified>
      <diff>@@ -91,7 +91,7 @@
     &lt;%include file=&quot;footer.mako&quot;/&gt;
     &lt;hr/&gt;
     &lt;p&gt;
-      &lt;span class=&quot;debug&quot;&gt;Proxied: &lt;a href=&quot;${s3response.url}&quot;&gt;${s3response.url}&lt;/a&gt;&lt;/span&gt;&lt;br/&gt;
+      &lt;span class=&quot;debug&quot;&gt;&lt;a href=&quot;${s3response.url}&quot;&gt;Proxied&lt;/a&gt;&lt;/span&gt;&lt;br/&gt;
       &lt;span class=&quot;debug&quot;&gt;Took: ${s3response.total_time}&lt;/span&gt;&lt;br/&gt;
       &lt;span class=&quot;debug&quot;&gt;Attempts: ${s3response.try_count}&lt;/span&gt;
     &lt;/p&gt;</diff>
      <filename>app/views/list.mako</filename>
    </modified>
    <modified>
      <diff>@@ -41,17 +41,14 @@
       %endfor
     &lt;/ul&gt;
     
-    %endif    
+    %endif
     
   &lt;/div&gt;
   
   &lt;div id=&quot;ft&quot;&gt;
-    &lt;p&gt;Based on &lt;a href=&quot;http://muxtape.com&quot;&gt;MuxTape&lt;/a&gt; and &lt;a href=&quot;http://opentape.fm/&quot;&gt;OpenTape&lt;/a&gt;.&lt;/p&gt;    
-    &lt;p&gt;Files: &lt;a href=&quot;${list_url}&quot;&gt;${list_url}&lt;/a&gt;&lt;/p&gt;    
+    &lt;p&gt;&lt;a href=&quot;${list_url}&quot;&gt;Files&lt;/a&gt; &amp;mdash; &lt;a href=&quot;${xspf_url}&quot;&gt;XSPF&lt;/a&gt; &amp;mdash; &lt;a href=&quot;#&quot; onClick=&quot;loadID3(); return false;&quot;&gt;Reload ID3&lt;/a&gt; &amp;mdash; &lt;a href=&quot;${s3response.url}&quot;&gt;Proxied&lt;/a&gt;&lt;/p&gt;    
+    &lt;p&gt;Based on &lt;a href=&quot;http://muxtape.com&quot;&gt;MuxTape&lt;/a&gt; and &lt;a href=&quot;http://opentape.fm/&quot;&gt;OpenTape&lt;/a&gt;.&lt;/p&gt;
     &lt;%include file=&quot;footer.mako&quot;/&gt;
-    &lt;hr/&gt;
-    &lt;p class=&quot;debug&quot;&gt;XSPF: ${xspf_url}&lt;/p&gt;    
-    &lt;p class=&quot;debug&quot;&gt;Debug: &lt;a href=&quot;&quot; onClick=&quot;debugReloadID3(); return false;&quot;&gt;Reload ID3&lt;/a&gt;&lt;/p&gt;
   &lt;/div&gt;
 &lt;/div&gt;
 
@@ -65,8 +62,7 @@ function playerReady(obj) {
 &lt;/script&gt;
 
 &lt;script type=&quot;text/javascript&quot;&gt;
-$(document).ready(function() {
-
+var loadID3 = function() {
   var flashvars = { type: &quot;xml&quot;, shuffle: &quot;false&quot;, repeat: &quot;list&quot;, file: &quot;${xspf_url}&quot;	}			
 	var params = { allowscriptaccess: &quot;always&quot; };			
 	var attributes = { id: &quot;shrub-player&quot;, name: &quot;shrub-player&quot;, styleclass: &quot;flash-player&quot; };
@@ -83,5 +79,7 @@ $(document).ready(function() {
   
   var id3Urls = ${base.to_json(id3_urls)};
   shrubTape.loadID3Urls(id3Urls);     
-});
+};
+
+$(document).ready(loadID3);
 &lt;/script&gt;</diff>
      <filename>app/views/muxtape.mako</filename>
    </modified>
    <modified>
      <diff>@@ -21,14 +21,16 @@ import wsgiref.handlers
 
 from google.appengine.ext import webapp
 
+from app.controllers.base import PrintEnvironmentHandler
 from app.controllers.s3 import DefaultPage, S3Page
 
 def main():
-  application = webapp.WSGIApplication([
-    ('/', DefaultPage),
-    ('/.*', S3Page),
-    ], debug=True)
-  wsgiref.handlers.CGIHandler().run(application)
+	application = webapp.WSGIApplication([
+		('/', DefaultPage),
+		('/shrub-env', PrintEnvironmentHandler),
+		('/.*', S3Page),
+		], debug=True)
+	wsgiref.handlers.CGIHandler().run(application)
 
 if __name__ == '__main__':
-  main()
+	main()</diff>
      <filename>main.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,9 +1,13 @@
-&quot;&quot;&quot;
-Main Shrub library
+import os
 
-To make an S3 bucket list request:
+def view_paths():
+	&quot;&quot;&quot;View path for views in this library.&quot;&quot;&quot;
+	return [os.path.join(os.path.dirname(__file__), &quot;views&quot;)]
 
-  s3 = S3()
-  s3response = s3.list(bucket_name, max_keys, prefix, delimiter, marker)
 
-&quot;&quot;&quot;
\ No newline at end of file
+class ShrubException(Exception):
+
+	def __init__(self, code, message):
+		super(Exception, self).__init__()
+		self.code = code
+		self.message = message</diff>
      <filename>shrub/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,4 +1 @@
-import os
 
-def view_path():  
-  return os.path.join(os.path.dirname(__file__), &quot;views&quot;)</diff>
      <filename>shrub/feeds/__init__.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,15 +2,18 @@ import rfc822
 import time
 
 class Item:
-  '''Item in an RSS feed'''
-  
-  def __init__(self, title, link, description=None, pub_date=None, guid=None):
-    self.title = title
-    self.link = link
-    self.description = description
-    self.pub_date = pub_date
-    self.guid = guid
-  
-  @property
-  def rfc822_pub_date(self):
-    return rfc822.formatdate(time.mktime(self.pub_date.timetuple()))
\ No newline at end of file
+	'''Item in an RSS feed'''
+	
+	def __init__(self, title, link, description=None, pub_date=None, guid=None):
+		self.title = title
+		self.link = link
+		self.description = description
+		self.pub_date = pub_date
+		self.guid = guid
+		
+	@property
+	def rfc822_pub_date(self):
+		if self.pub_date is None:
+			return None
+		return rfc822.formatdate(time.mktime(self.pub_date.timetuple()))
+</diff>
      <filename>shrub/feeds/rss.py</filename>
    </modified>
    <modified>
      <diff>@@ -1,9 +1,10 @@
 
 class Track:
-  '''Track in an XSPF feed'''
-  
-  def __init__(self, location, meta, title, info):    
-    self.location = location
-    self.meta = meta
-    self.title = title
-    self.info = info
\ No newline at end of file
+	'''Track in an XSPF feed'''
+	
+	def __init__(self, location, meta, title, info):
+		self.location = location
+		self.meta = meta
+		self.title = title
+		self.info = info
+</diff>
      <filename>shrub/feeds/xspf.py</filename>
    </modified>
    <modified>
      <diff>@@ -7,109 +7,110 @@ import shrub.feeds.xspf
 import shrub.utils
 
 class S3File:
-  
-  DefaultLocation = 's3.amazonaws.com'
-  DefaultContentType = 'application/octet-stream'
 
-  def __init__(self, bucket=None, key=None):
-    self.id = u'%s/%s' % (bucket, key)
-    self.bucket = bucket    
-    self.key = key
-    self.name = key
-    self.metadata = {}
-    self.content_type = self.DefaultContentType
-    self.filename = None
-    self.etag = None
-    self.last_modified = None
-    self.owner = None
-    self.storage_class = None
-    self.size = None
-    self.is_folder = False
-    
-    self.pretty_last_modified_cache = None
-    self.pretty_size_cache = None
-  
-  def __hash__(self):
-    return self.id.__hash__()
-    
-  def __eq__(self, other):
-    return self.id.__eq__(other.id)
-    
-  def __str__(self):
-    return u'%s/%s' % (self.bucket, self.key)
-  
-  def __json__(self):
-    return dict(bucket=self.bucket, key=self.key, etag=self.etag, lastModified=self.last_modified,
-      size=self.size, storageClass=self.storage_class)
-    
-  def name_with_prefix(self, prefix):
-    if prefix:
-      if prefix.endswith('/'): return prefix + self.name
-      else: return &quot;%s/%s&quot; % (prefix, self.name)
-    
-    return self.name    
-    
-  def pretty_last_modified(self, default):
-    if not self.last_modified: return default    
-    if not self.pretty_last_modified_cache: self.pretty_last_modified_cache = self.last_modified.strftime(&quot;%b %d, %Y, %I:%M %p&quot;)      
-    return self.pretty_last_modified_cache
-    
-  def __pretty_size(self, size):
-    if size == 0: return &quot;-&quot;
-    suffixes = [(&quot;B&quot;,2**10), (&quot;K&quot;,2**20), (&quot;M&quot;,2**30), (&quot;G&quot;,2**40), (&quot;T&quot;,2**50)]
-    for suf, lim in suffixes:
-      if size &gt; lim:
-        continue
-      else:
-        return round(size/float(lim/2**10),2).__str__()+suf
-    
-  def pretty_size(self, default):
-    if not self.size: return default    
-    if not self.pretty_size_cache: self.pretty_size_cache = self.__pretty_size(self.size)
-    return self.pretty_size_cache
-    
-  @property
-  def name_without_extension(self):
-    position = self.name.rfind('.')
-    if position == -1: return self.name
-    return self.name[0:position]
-  
-  @property
-  def extension(self):
-    position = self.name.rfind('.')
-    if position == -1: return
-    return self.name[position + 1:]    
-  
-  def to_appspot_url(self):
-    if self.is_folder:
-      name = re.sub(re.escape('_\$folder\$\Z'), '/', self.key)      
-    else:
-      name = self.key
-    
-    return u'http://%s/%s/%s' % (shrub.utils.current_url(), urllib.quote(self.bucket.encode('utf-8')), urllib.quote(name))
-  appspot_url = property(to_appspot_url)
-      
-  def to_url(self, secure=False):
-    scheme = 'http';
-    if secure: scheme = 'https'    
-    return u'%s://%s/%s/%s' % (scheme, self.DefaultLocation, urllib.quote(self.bucket.encode('utf-8')), urllib.quote(self.key.encode('utf-8')))
-  url = property(to_url)  
-  
-  def to_rss_item(self):
-    link = self.url
-    if self.is_folder:
-      link = self.appspot_url
-      
-    description = None
-    if not self.is_folder:
-      #description = ' &amp;nbsp; &lt;a href=&quot;%s&quot;&gt;Download&lt;/a&gt;' % self.to_url(False)
-      description = ''
-      pretty_size = self.pretty_size(None)
-      if pretty_size: description += 'Size: %sb' % pretty_size      
-    
-    return shrub.feeds.rss.Item(self.name, None, description, pub_date=self.last_modified, guid=link)
-  rss_item = property(to_rss_item)
-    
-  def to_xspf_track(self):
-    return shrub.feeds.xspf.Track(location=self.url, meta=self.extension, title=self.name_without_extension, info=None)
-  xspf_track = property(to_xspf_track)
\ No newline at end of file
+	DefaultLocation = 's3.amazonaws.com'
+	DefaultContentType = 'application/octet-stream'
+	
+	def __init__(self, bucket=None, key=None):
+		self.id = u'%s/%s' % (bucket, key)
+		self.bucket = bucket
+		self.key = key
+		self.name = key
+		self.metadata = {}
+		self.content_type = self.DefaultContentType
+		self.filename = None
+		self.etag = None
+		self.last_modified = None
+		self.owner = None
+		self.storage_class = None
+		self.size = None
+		self.is_folder = False
+		
+		self.pretty_last_modified_cache = None
+		self.pretty_size_cache = None
+		
+	def __hash__(self):
+		return self.id.__hash__()
+		
+	def __eq__(self, other):
+		return self.id.__eq__(other.id)
+		
+	def __str__(self):
+		return u'%s/%s' % (self.bucket, self.key)
+		
+	def __json__(self):
+		return dict(bucket=self.bucket, key=self.key, etag=self.etag, lastModified=self.last_modified,
+		size=self.size, storageClass=self.storage_class)
+		
+	def name_with_prefix(self, prefix):
+		if prefix:
+			if prefix.endswith('/'): return prefix + self.name
+			else: return &quot;%s/%s&quot; % (prefix, self.name)
+			
+		return self.name
+		
+	def pretty_last_modified(self, default):
+		if not self.last_modified: return default
+		if not self.pretty_last_modified_cache: self.pretty_last_modified_cache = self.last_modified.strftime(&quot;%b %d, %Y, %I:%M %p&quot;)
+		return self.pretty_last_modified_cache
+		
+	def __pretty_size(self, size):
+		if size == 0: return &quot;-&quot;
+		suffixes = [(&quot;B&quot;,2**10), (&quot;K&quot;,2**20), (&quot;M&quot;,2**30), (&quot;G&quot;,2**40), (&quot;T&quot;,2**50)]
+		for suf, lim in suffixes:
+			if size &gt; lim:
+				continue
+			else:
+				return round(size/float(lim/2**10),2).__str__()+suf
+				
+	def pretty_size(self, default):
+		if not self.size: return default
+		if not self.pretty_size_cache: self.pretty_size_cache = self.__pretty_size(self.size)
+		return self.pretty_size_cache
+		
+	@property
+	def name_without_extension(self):
+		position = self.name.rfind('.')
+		if position == -1: return self.name
+		return self.name[0:position]
+		
+	@property
+	def extension(self):
+		position = self.name.rfind('.')
+		if position == -1: return
+		return self.name[position + 1:]
+		
+	def to_appspot_url(self):
+		if self.is_folder:
+			name = re.sub(re.escape('_\$folder\$\Z'), '/', self.key)
+		else:
+			name = self.key
+			
+		return u'http://%s/%s/%s' % (shrub.utils.current_url(), urllib.quote(self.bucket.encode('utf-8')), urllib.quote(name))
+	appspot_url = property(to_appspot_url)
+	
+	def to_url(self, secure=False):
+		scheme = 'http';
+		if secure: scheme = 'https'
+		return u'%s://%s/%s/%s' % (scheme, self.DefaultLocation, urllib.quote(self.bucket.encode('utf-8')), urllib.quote(self.key.encode('utf-8')))
+	url = property(to_url)
+	
+	def to_rss_item(self):
+		link = self.url
+		if self.is_folder:
+			link = self.appspot_url
+			
+		description = None
+		if not self.is_folder:
+		#description = ' &amp;nbsp; &lt;a href=&quot;%s&quot;&gt;Download&lt;/a&gt;' % self.to_url(False)
+			description = ''
+			pretty_size = self.pretty_size(None)
+			if pretty_size: description += 'Size: %sb' % pretty_size
+			
+		return shrub.feeds.rss.Item(self.name, None, description, pub_date=self.last_modified, guid=link)
+	rss_item = property(to_rss_item)
+	
+	def to_xspf_track(self):
+		return shrub.feeds.xspf.Track(location=self.url, meta=self.extension, title=self.name_without_extension, info=None)
+	xspf_track = property(to_xspf_track)
+</diff>
      <filename>shrub/file.py</filename>
    </modified>
    <modified>
      <diff>@@ -5,49 +5,49 @@ from datetime import datetime
 
 from google.appengine.api import urlfetch
 
-from shrub.response import S3BucketResponse, S3ErrorResponse
+from shrub.response.base import S3BucketResponse, S3ErrorResponse
 from shrub.utils import S3Utils
 
 class S3:
-  
-  DefaultLocation = 's3.amazonaws.com'
-  
-  def _fetch(self, url, retry_count, **kwargs):
-    &quot;&quot;&quot;Calls urlfetch.fetch with retry count&quot;&quot;&quot;
-    try_count = 0
-    times = []
-    while try_count &lt; retry_count:
-      try:
-        try_count += 1
-        fetch_start = datetime.now()
-        # Fetch the url
-        response = urlfetch.fetch(url, **kwargs)
-        times.append(datetime.now() - fetch_start)
-        # Retry on 5xx errors as well as urlfetch exceptions
-        if int(response.status_code) in range(500, 600):
-          continue
-        return response, try_count, times
-      except Exception, error:
-        logging.error('Error(%s): %s' % (try_count, error))
-        if try_count &gt;= retry_count:
-          raise
-  
-  def list(self, bucket_name, max_keys, prefix, delimiter, marker, cache=60, retry_count=3):
-    if retry_count &lt; 0: raise ValueError, &quot;Invalid retry_count &lt; 0&quot;
-    
-    url_options = { }
-    
-    if max_keys: url_options['max-keys'] = str(max_keys)
-    if prefix: url_options['prefix'] = urllib.quote(prefix, '')
-    if delimiter: url_options['delimiter'] = urllib.quote(delimiter, '')
-    if marker: url_options['marker'] = urllib.quote(marker, '')
-    
-    url = u'http://%s/%s?%s' % (S3.DefaultLocation, bucket_name, S3Utils.params_to_url(url_options))
-    logging.info(&quot;URL: %s&quot;, url)
-    
-    headers = {'Cache-Control':'max-age=%s' % cache}
-    try:
-      response, try_count, times = self._fetch(url, retry_count, headers=headers)
-      return S3BucketResponse(url, int(response.status_code), response.content, try_count=try_count, times=times)
-    except Exception, error:
-      return S3ErrorResponse(url, 503, str(error), try_count=try_count, times=times)
+
+	DefaultLocation = 's3.amazonaws.com'
+	
+	def _fetch(self, url, retry_count, **kwargs):
+		&quot;&quot;&quot;Calls urlfetch.fetch with retry count&quot;&quot;&quot;
+		try_count = 0
+		times = []
+		response = None
+		while try_count &lt; retry_count:
+			try:
+				try_count += 1
+				fetch_start = datetime.now()
+				# Fetch the url
+				response = urlfetch.fetch(url, **kwargs)
+				times.append(datetime.now() - fetch_start)
+				# Retry on 5xx errors as well as urlfetch exceptions
+				if int(response.status_code) in xrange(500, 600):
+					continue
+				return response, try_count, times
+			except Exception, error:
+				logging.error('Error(%s): %s' % (try_count, error))
+				if try_count &gt;= retry_count:
+					raise
+		return response, try_count, times
+
+	def list(self, bucket_name, max_keys=None, prefix=None, delimiter=None, marker=None, cache=60, retry_count=3):
+		if retry_count &lt; 0: raise ValueError, &quot;Invalid retry_count &lt; 0&quot;
+
+		url_options = dict(prefix=prefix, delimiter=delimiter, marker=marker)
+		if max_keys: url_options['max-keys'] = str(max_keys)
+
+		url = u'http://%s/%s?%s' % (S3.DefaultLocation, bucket_name, S3Utils.params_to_url(url_options, True))
+		logging.info(&quot;URL: %s&quot;, url)
+		
+		headers = {'Cache-Control':'max-age=%s' % (cache)}
+		try:
+			response, try_count, times = self._fetch(url, retry_count, headers=headers)
+			return S3BucketResponse(url, int(response.status_code), response.content, try_count=try_count, times=times)
+		except Exception, error:
+			# TODO(gabe): Need to disable this in debug mode, so exceptions raise properly
+			return S3ErrorResponse(url, 503, str(error))
+</diff>
      <filename>shrub/s3.py</filename>
    </modified>
    <modified>
      <diff>@@ -2,56 +2,57 @@ import urllib
 import os
 
 def current_url():
-  name = os.environ['SERVER_NAME']
-  port = int(os.environ['SERVER_PORT'])
-  
-  if port == 80 or port == 443: return name    
-  return '%s:%s' % (name, port)
-
+	name = os.environ['SERVER_NAME']
+	port = int(os.environ['SERVER_PORT'])
+	
+	if port == 80 or port == 443: return name
+	return '%s:%s' % (name, port)
+	
 class S3Utils:
-  
-  @staticmethod
-  def params_to_url(params):
-    pairs = ['%s=%s' % (key, value) for key, value in params.items()]
-    paramString = '&amp;'.join(pairs)
-    if paramString: return paramString
-    return ''
-    
-  @staticmethod
-  def parse_request(request, prefix=None):
-    request_path = urllib.unquote(request.path)    
-    if prefix: 
-      request_path = re.sub('%s$' % prefix, '', request_path)
-    
-    bucket_name = None
-    prefix = None    
-    
-    if request_path != '/':
-      paths = request_path.split('/')[1:]
-      bucket_name = paths[0]
-      prefix = '/'.join(paths[1:])
-      #if prefix and not prefix.endswith('/'): prefix += '/'
-      
-    return bucket_name, prefix
-  
-  @staticmethod  
-  def file_comparator(x, y, sort, sort_asc):
-    # Change sort aliases
-    if sort == &quot;date&quot;: sort = &quot;last_modified&quot; 
-    if sort == &quot;name&quot;: sort = &quot;key&quot;
 
-    a = b = None
+	@staticmethod
+	def params_to_url(params, url_escape=False):
+		def maybe_escape(s):
+			return urllib.quote_plus(s) if url_escape else s
+		pairs = ['%s=%s' % (maybe_escape(key), maybe_escape(value)) for key, value in params.items() if key is not None and value is not None]
+		return '&amp;'.join(pairs)
 
-    if sort == &quot;key&quot; or sort == &quot;size&quot; or sort == &quot;last_modified&quot;:
-      a = getattr(x, sort)
-      b = getattr(y, sort)
+	@staticmethod
+	def parse_gae_request(request, prefix=None):
+		&quot;&quot;&quot;Parse bucket name and prefix from gae request.&quot;&quot;&quot;
+		request_path = urllib.unquote(request.path)
+		if prefix:
+			request_path = re.sub('%s$' % prefix, '', request_path)
+			
+		bucket_name = None
+		prefix = None
+		
+		if request_path != '/':
+			paths = request_path.split('/')[1:]
+			bucket_name = paths[0]
+			prefix = '/'.join(paths[1:])
 
-    if a is None and b is not None: return 1
-    elif a is not None and b is None: return -1
-    elif a is None and b is None: return 0
+		return bucket_name, prefix
 
-    if isinstance(a, str): a = a.lower()
-    if isinstance(b, str): b = b.lower()
+	@staticmethod
+	def file_comparator(x, y, sort, sort_asc):
+	# Change sort aliases
+		if sort == &quot;date&quot;: sort = &quot;last_modified&quot;
+		if sort == &quot;name&quot;: sort = &quot;key&quot;
+		
+		a = b = None
+		
+		if sort == &quot;key&quot; or sort == &quot;size&quot; or sort == &quot;last_modified&quot;:
+			a = getattr(x, sort)
+			b = getattr(y, sort)
+			
+		if a is None and b is not None: return 1
+		elif a is not None and b is None: return -1
+		elif a is None and b is None: return 0
+		
+		if isinstance(a, str): a = a.lower()
+		if isinstance(b, str): b = b.lower()
+		
+		if sort_asc: return cmp(a, b)
+		else: return cmp(b, a)
 
-    if sort_asc: return cmp(a, b)
-    else: return cmp(b, a)
\ No newline at end of file</diff>
      <filename>shrub/utils.py</filename>
    </modified>
  </modified>
  <removed type="array">
    <removed>
      <filename>shrub/feeds/helper.py</filename>
    </removed>
    <removed>
      <filename>shrub/feeds/views/rss.mako</filename>
    </removed>
    <removed>
      <filename>shrub/feeds/views/xspf.mako</filename>
    </removed>
    <removed>
      <filename>shrub/response.py</filename>
    </removed>
    <removed>
      <filename>shrub/sax.py</filename>
    </removed>
  </removed>
  <parents type="array">
    <parent>
      <id>8eedc52c315d485b11e06ca8af2be1711ff0e8ad</id>
    </parent>
  </parents>
  <author>
    <name>Gabriel Handford</name>
    <email>gabrielh@gmail.com</email>
  </author>
  <url>http://github.com/gabriel/shrub/commit/f176c970a02464b44cb8fde1f4144a707302174e</url>
  <id>f176c970a02464b44cb8fde1f4144a707302174e</id>
  <committed-date>2009-02-15T15:18:10-08:00</committed-date>
  <authored-date>2009-02-15T15:18:10-08:00</authored-date>
  <message>Refactoring</message>
  <tree>528654bfb83a3b6704dedd894bca9c05b3a4c06f</tree>
  <committer>
    <name>Gabriel Handford</name>
    <email>gabrielh@gmail.com</email>
  </committer>
</commit>
