changeset 0:af82aaec0377

initial import of redirector
author k0s <k0scist@gmail.com>
date Mon, 07 Sep 2009 15:15:48 -0400
parents
children 55578cf505dd
files ini-redirector.ini redirector.ini redirector/__init__.py redirector/factory.py redirector/redirector.py redirector/redirectors.py redirector/templates/metarefresh.html redirector/templates/redirects.html sample/bar.txt sample/foo.txt sample/index.html setup.py
diffstat 12 files changed, 412 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
new file mode 100644
--- /dev/null
+++ b/ini-redirector.ini
@@ -0,0 +1,10 @@
+[/foo.txt]
+to = /bar.txt
+type = 301
+created = August 1, 2009
+
+[http://127.0.0.1:5521/(.*)]
+to = http://localhost:5521/\1
+type = metarefresh
+seconds = 10
+created = August 1, 2009
new file mode 100644
--- /dev/null
+++ b/redirector.ini
@@ -0,0 +1,24 @@
+#!/usr/bin/env paster
+
+[DEFAULT]
+debug = true
+email_to = jhammel@openplans.org
+smtp_server = localhost
+error_email_from = paste@localhost
+
+[server:main]
+use = egg:Paste#http
+host = 0.0.0.0
+port = 5521
+
+[composite:main]
+use = egg:Paste#urlmap
+/ = redirector
+
+set debug = false
+
+[app:redirector]
+paste.app_factory = redirector.factory:factory
+redirector.redirector = ini-redirector
+redirector.ini-redirector.ini = %(here)s/ini-redirector.ini
+app.directory = %(here)s/sample
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/redirector/__init__.py
@@ -0,0 +1,1 @@
+#
new file mode 100644
--- /dev/null
+++ b/redirector/factory.py
@@ -0,0 +1,18 @@
+import os
+from redirector import Redirector
+from paste.httpexceptions import HTTPExceptionHandler
+from paste.urlparser import StaticURLParser
+
+def factory(global_conf, **app_conf):
+    """create a sample redirector"""
+    assert 'app.directory' in app_conf 
+    directory = app_conf['app.directory']
+    assert os.path.isdir(directory)
+    keystr = 'redirector.'
+    args = dict([(key.split(keystr, 1)[-1], value)
+                 for key, value in app_conf.items()
+                 if key.startswith(keystr) ])
+    app = StaticURLParser(directory)
+    redirector = Redirector(app, **args)
+    return HTTPExceptionHandler(redirector)
+    
new file mode 100644
--- /dev/null
+++ b/redirector/redirector.py
@@ -0,0 +1,137 @@
+"""
+redirector: a view with webob
+"""
+
+import re
+
+from genshi.template import TemplateLoader
+from pkg_resources import iter_entry_points
+from pkg_resources import resource_filename
+from urlparse import urlparse
+from webob import Request, Response, exc
+
+class Redirector(object):
+
+    ### class level variables
+    defaults = { 'auto_reload': 'False',
+                 'auth': 'False',
+                 'date_format': '%c',
+                 'path': '.redirect',
+                 'redirector': None,
+                 'seconds': 5, # seconds for a meta-refresh tag to redirect
+                 }
+
+    def __init__(self, app, **kw):
+        self.app = app # WSGI app
+
+        # set instance attributes
+        for key in self.defaults:
+            setattr(self, key, kw.get(key, self.defaults[key]))
+        self.response_functions = { 'GET': self.get,
+                                    'POST': self.post,
+                                    }
+        # bool options
+        for var in 'auto_reload', 'auth':
+            setattr(self, var, getattr(self, var).lower() == 'true')
+
+        # pick a redirector back end
+        assert self.redirector
+        if isinstance(self.redirector, basestring):
+            name = self.redirector
+            self.redirector = iter_entry_points('redirector.redirectors', name=name).next()
+            # take first entry point;
+            # will raise StopIteration if there are none
+
+            self.redirector = self.redirector.load()
+        keystr = name + '.'
+        kwargs = dict([(key.split(keystr, 1)[1], value)
+                       for key, value in kw.items()
+                       if key.startswith(keystr)])
+        self.redirector = self.redirector(**kwargs)
+
+        # genshi template loader
+        templates_dir = resource_filename(__name__, 'templates')
+        self.loader = TemplateLoader(templates_dir,
+                                     auto_reload=self.auto_reload)
+
+        # redirect exceptions
+        self.status_map = dict([(key, value) 
+                                for key, value in exc.status_map.items()
+                                if key in set([301, 302, 307])])
+
+
+    ### methods dealing with HTTP
+    def __call__(self, environ, start_response):
+        request = Request(environ)
+
+        path = request.path_info.strip('/').split('/')
+        if path and path[0] == self.path:
+            ### TODO: check if authorized
+            res = self.make_response(request)
+            return res(environ, start_response)
+        else:
+
+            ### query redirection tables
+            for redirect in self.redirector.redirects():
+                
+                _from = redirect['from']
+                parsed_url = urlparse(_from)
+                if parsed_url[0]:
+                    url = request.url
+                else:
+                    url = request.path_info
+                from_re = re.compile(_from)
+
+                # redirect on match
+                match = from_re.match(url)
+                if match:
+                    location = from_re.sub(redirect['to'], url)
+                    _type = redirect['type']
+                    if isinstance(_type, int):
+                        raise self.status_map[_type](location=location)
+                    else:
+                        res = self.meta_refresh(request, redirect, location)
+                        return res(environ, start_response)
+
+            return self.app(environ, start_response)
+                                
+    def make_response(self, request):
+        return self.response_functions.get(request.method, self.error)(request)
+
+    def get_response(self, text, content_type='text/html'):
+        res = Response(content_type=content_type, body=text)
+        return res
+
+    def get(self, request):
+        """
+        return response to a GET requst
+        """
+
+        data = { 'redirects': self.redirector.redirects(),
+                 'status_map': self.status_map,
+                 'date_format': self.date_format }
+
+        template = self.loader.load('redirects.html')
+        res = template.generate(**data).render('html', doctype='html')
+        return self.get_response(res)
+
+    def post(self, request):
+        """
+        return response to a POST request
+        """
+        return self.get(request)
+
+    def error(self, request):
+        """deal with non-supported methods"""
+        return exc.HTTPMethodNotAllowed("Only %r operations are allowed" % self.response_functions.keys())
+        
+
+    def meta_refresh(self, request, redirect, location):
+        data = { 'request': request,
+                 'redirect': redirect,
+                 'location': location,
+                 'seconds': redirect.get('seconds', self.seconds),
+                 'date_format': self.date_format }
+        template = self.loader.load('metarefresh.html')
+        res = template.generate(**data).render('html', doctype='html')
+        return self.get_response(res)
new file mode 100644
--- /dev/null
+++ b/redirector/redirectors.py
@@ -0,0 +1,71 @@
+from datetime import datetime
+from dateutil.parser import parse
+from ConfigParser import ConfigParser
+
+
+class TestRedirector(object):
+    def redirects(self):
+        return [ { 'from': '/foo.txt', 'to': '/bar.txt',
+                   'type': 301, 
+                   'created': datetime.now(),
+                   'expires': None },
+                 { 'from': 'http://127.0.0.1:5521/(.*)',
+                   'to': r'http://localhost:5521/\1',
+                   'type': 'metarefresh',
+                   'seconds': 10,
+                   'created': datetime.now(),
+                   'expires': None },
+                 ]
+    def set(self, _from, to, type=301, expires=None, seconds=None, reason=None):
+        # test only....does not set anything
+        return 
+
+    def add(self, _from, to, type=301, expires=None, seconds=None):
+        self.set(_from, to, type, expires, seconds)
+
+class IniRedirector(object):
+    def __init__(self, ini):
+        self.ini = ini
+
+    def redirects(self):
+        parser = ConfigParser()
+        parser.read(self.ini)
+
+        redirects = []
+
+        for section in parser.sections():
+            redirect = { 'from': section }
+            assert parser.has_option(section, 'to')
+            assert parser.has_option(section, 'type')
+            redirect['to'] = parser.get(section, 'to')
+            if parser.has_option(section, 'created'):
+                redirect['created'] = parse(parser.get(section, 'created'))
+            else:
+                redirect['created'] = None
+            _type = parser.get(section, 'type')
+            try:
+                _type = int(_type)
+            except ValueError:
+                assert _type == 'metarefresh'
+            redirect['type'] = _type
+            if parser.has_option(section, 'expires'):
+                redirect['expires'] = parse(parser.get(section, 'expires'))
+            else:
+                redirect['expires'] = None
+            if parser.has_option(section, 'reason'):
+                redirect['reason'] = parser.get(section, 'reason')
+            if parser.has_option(section, 'seconds'):
+                redirect['seconds'] = parser.getint(section, 'seconds')
+            redirects.append(redirect)
+        return redirects
+
+    def set(self, _from, to, type=301, expires=None, seconds=None, reason=None):
+        parser = ConfigParser()
+        parser.read(self.ini)
+        raise NotImplementedError # TODO
+
+    def add(self, _from, to, type=301, expires=None, seconds=None):
+        raise NotImplementedError # TODO
+        parser = ConfigParser()
+        parser.read(self.ini)
+        self.set(_from, to, type, expires, seconds)
new file mode 100644
--- /dev/null
+++ b/redirector/templates/metarefresh.html
@@ -0,0 +1,25 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/">
+  <head>
+    <meta http-equiv="refresh" content="${seconds};url=${location}" />
+  </head>
+  <body>
+    
+    <div>
+      You are being from ${request.url} to ${location} in ${seconds} seconds.  
+      Please update any bookmarks to this page.
+    </div>
+    
+    <div py:if="redirect['expires']">
+      This redirect will expire ${redirect['expires'].strftime(date_format)}
+    </div>
+
+    <div py:if="redirect.get('reason')">
+      ${redirect['reason']}
+    </div>
+
+  </body>
+</html>
new file mode 100644
--- /dev/null
+++ b/redirector/templates/redirects.html
@@ -0,0 +1,77 @@
+<!DOCTYPE html
+    PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+    "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml"
+      xmlns:py="http://genshi.edgewall.org/">
+<head>
+  <title>redirects</title>
+</head>
+<body>
+  <h1>redirects</h1>
+
+  <!-- TODO: separate these to its own template to xi:include -->
+  <table>
+    <tr>
+      <th>From</th>
+      <th></th>
+      <th>To</th>
+      <th>Type</th>
+      <th>Created</th>
+      <th>Expires</th>
+      <th></th>
+    </tr>
+
+    <tr py:for="redirect in redirects">
+      <td>${redirect['from']}</td>
+      <td>&rarr;</td>
+      <td>${redirect['to']}</td>
+      <td py:choose="isinstance(redirect['type'], int)">
+        <span py:when="True">
+          ${str(redirect['type'])} ${status_map[redirect['type']].title}
+        </span>
+        <span py:otherwise="">meta refresh tag</span>
+      </td>
+      <td>${redirect['created'].strftime(date_format)}</td>
+      <td>
+        <span py:replace="redirect['expires'] and redirect['expires'].strftime(date_format) or 'never'"/>
+      </td>
+      <td>
+        <input type="submit" name="delete:${redirect['from']}" value="Remove"/>
+      </td>
+    </tr>
+  </table>
+
+  <h2>Add a redirect</h2>
+
+  <form action="post">
+    <dl>
+      <dt>From</dt>
+      <dd><input type="text" name="from"/></dd>
+      
+      <dt>To</dt>
+      <dd><input type="text" name="to"/></dd>
+
+      <dt>Type</dt>
+      <dd>
+        <select name="type">
+          <option py:for="key in sorted(status_map.keys())" value="${key}">${key} ${status_map[key].title}</option>
+          <option value="metarefresh">meta-refresh page</option>
+        </select>
+      </dd>
+
+      <dt>Expires</dt>
+      <dd>
+        <input type="text" name="expires"/>
+      </dd>
+
+      <dt>Reason for this redirect</dt>
+      <dd>
+        <textarea name="reason" cols="80" rows="25"></textarea>
+      </dd>
+    </dl>
+    
+    <input type="submit" name="add" value="Add"/>
+  </form>
+
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/sample/bar.txt
@@ -0,0 +1,1 @@
+bar
new file mode 100644
--- /dev/null
+++ b/sample/foo.txt
@@ -0,0 +1,1 @@
+foo
new file mode 100644
--- /dev/null
+++ b/sample/index.html
@@ -0,0 +1,9 @@
+<html>
+<head>
+<title>foo bar</title>
+</head>
+<body>
+<p><a href="foo.txt">foo</a></p>
+<p><a href="bar.txt">bar</a></p>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/setup.py
@@ -0,0 +1,38 @@
+from setuptools import setup, find_packages
+import sys, os
+
+version = "0.1"
+
+setup(name='redirector',
+      version=version,
+      description="WSGI middleware/app for managing redirects",
+      long_description="""
+""",
+      classifiers=[], # Get strings from http://www.python.org/pypi?%3Aaction=list_classifiers
+      author='Jeff Hammel',
+      author_email='jhammel@openplans.org',
+      url='http://k0s.org',
+      license="",
+      packages=find_packages(exclude=['ez_setup', 'examples', 'tests']),
+      include_package_data=True,
+      zip_safe=False,
+      install_requires=[
+          # -*- Extra requirements: -*-
+         'WebOb',	
+         'Paste',
+         'PasteScript',
+         'genshi',
+         'python-dateutil',
+         
+      ],
+      entry_points="""
+      # -*- Entry points: -*-
+      [paste.app_factory]
+      main = redirector.factory:factory
+
+      [redirector.redirectors]
+      test-redirector = redirector.redirectors:TestRedirector
+      ini-redirector = redirector.redirectors:IniRedirector
+      """,
+      )
+