Commits

Lynn Rees committed 1e70b7c

[svn]

  • Participants
  • Parent commits 3f1f6dd
  • Branches wsgiform
  • Tags svn.21

Comments (0)

Files changed (8)

branches/0.3/ez_setup.py

+#!python
+"""Bootstrap setuptools installation
+
+If you want to use setuptools in your package's setup.py, just include this
+file in the same directory with it, and add this to the top of your setup.py::
+
+    from ez_setup import use_setuptools
+    use_setuptools()
+
+If you want to require a specific version of setuptools, set a download
+mirror, or use an alternate download directory, you can do so by supplying
+the appropriate options to ``use_setuptools()``.
+
+This file can also be run as a script to install or upgrade setuptools.
+"""
+import sys
+DEFAULT_VERSION = "0.6c3"
+DEFAULT_URL     = "http://cheeseshop.python.org/packages/%s/s/setuptools/" % sys.version[:3]
+
+md5_data = {
+    'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca',
+    'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb',
+    'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b',
+    'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a',
+    'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618',
+    'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac',
+    'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5',
+    'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4',
+    'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c',
+    'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b',
+    'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27',
+    'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277',
+    'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa',
+    'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e',
+    'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e',
+}
+
+import sys, os
+
+def _validate_md5(egg_name, data):
+    if egg_name in md5_data:
+        from md5 import md5
+        digest = md5(data).hexdigest()
+        if digest != md5_data[egg_name]:
+            print >>sys.stderr, (
+                "md5 validation of %s failed!  (Possible download problem?)"
+                % egg_name
+            )
+            sys.exit(2)
+    return data
+
+
+def use_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    download_delay=15
+):
+    """Automatically find/download setuptools and make it available on sys.path
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end with
+    a '/').  `to_dir` is the directory where setuptools will be downloaded, if
+    it is not already available.  If `download_delay` is specified, it should
+    be the number of seconds that will be paused before initiating a download,
+    should one be required.  If an older version of setuptools is installed,
+    this routine will print a message to ``sys.stderr`` and raise SystemExit in
+    an attempt to abort the calling script.
+    """
+    try:
+        import setuptools
+        if setuptools.__version__ == '0.0.1':
+            print >>sys.stderr, (
+            "You have an obsolete version of setuptools installed.  Please\n"
+            "remove it from your system entirely before rerunning this script."
+            )
+            sys.exit(2)
+    except ImportError:
+        egg = download_setuptools(version, download_base, to_dir, download_delay)
+        sys.path.insert(0, egg)
+        import setuptools; setuptools.bootstrap_install_from = egg
+
+    import pkg_resources
+    try:
+        pkg_resources.require("setuptools>="+version)
+
+    except pkg_resources.VersionConflict, e:
+        # XXX could we install in a subprocess here?
+        print >>sys.stderr, (
+            "The required version of setuptools (>=%s) is not available, and\n"
+            "can't be installed while this script is running. Please install\n"
+            " a more recent version first.\n\n(Currently using %r)"
+        ) % (version, e.args[0])
+        sys.exit(2)
+
+def download_setuptools(
+    version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir,
+    delay = 15
+):
+    """Download setuptools from a specified location and return its filename
+
+    `version` should be a valid setuptools version number that is available
+    as an egg for download under the `download_base` URL (which should end
+    with a '/'). `to_dir` is the directory where the egg will be downloaded.
+    `delay` is the number of seconds to pause before an actual download attempt.
+    """
+    import urllib2, shutil
+    egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3])
+    url = download_base + egg_name
+    saveto = os.path.join(to_dir, egg_name)
+    src = dst = None
+    if not os.path.exists(saveto):  # Avoid repeated downloads
+        try:
+            from distutils import log
+            if delay:
+                log.warn("""
+---------------------------------------------------------------------------
+This script requires setuptools version %s to run (even to display
+help).  I will attempt to download it for you (from
+%s), but
+you may need to enable firewall access for this script first.
+I will start the download in %d seconds.
+
+(Note: if this machine does not have network access, please obtain the file
+
+   %s
+
+and place it in this directory before rerunning this script.)
+---------------------------------------------------------------------------""",
+                    version, download_base, delay, url
+                ); from time import sleep; sleep(delay)
+            log.warn("Downloading %s", url)
+            src = urllib2.urlopen(url)
+            # Read/write all in one block, so we don't create a corrupt file
+            # if the download is interrupted.
+            data = _validate_md5(egg_name, src.read())
+            dst = open(saveto,"wb"); dst.write(data)
+        finally:
+            if src: src.close()
+            if dst: dst.close()
+    return os.path.realpath(saveto)
+
+def main(argv, version=DEFAULT_VERSION):
+    """Install or upgrade setuptools and EasyInstall"""
+
+    try:
+        import setuptools
+    except ImportError:
+        egg = None
+        try:
+            egg = download_setuptools(version, delay=0)
+            sys.path.insert(0,egg)
+            from setuptools.command.easy_install import main
+            return main(list(argv)+[egg])   # we're done here
+        finally:
+            if egg and os.path.exists(egg):
+                os.unlink(egg)
+    else:
+        if setuptools.__version__ == '0.0.1':
+            # tell the user to uninstall obsolete version
+            use_setuptools(version)
+
+    req = "setuptools>="+version
+    import pkg_resources
+    try:
+        pkg_resources.require(req)
+    except pkg_resources.VersionConflict:
+        try:
+            from setuptools.command.easy_install import main
+        except ImportError:
+            from easy_install import main
+        main(list(argv)+[download_setuptools(delay=0)])
+        sys.exit(0) # try to force an exit
+    else:
+        if argv:
+            from setuptools.command.easy_install import main
+            main(argv)
+        else:
+            print "Setuptools version",version,"or greater has been installed."
+            print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)'
+
+
+
+def update_md5(filenames):
+    """Update our built-in md5 registry"""
+
+    import re
+    from md5 import md5
+
+    for name in filenames:
+        base = os.path.basename(name)
+        f = open(name,'rb')
+        md5_data[base] = md5(f.read()).hexdigest()
+        f.close()
+
+    data = ["    %r: %r,\n" % it for it in md5_data.items()]
+    data.sort()
+    repl = "".join(data)
+
+    import inspect
+    srcfile = inspect.getsourcefile(sys.modules[__name__])
+    f = open(srcfile, 'rb'); src = f.read(); f.close()
+
+    match = re.search("\nmd5_data = {\n([^}]+)}", src)
+    if not match:
+        print >>sys.stderr, "Internal error!"
+        sys.exit(2)
+
+    src = src[:match.start(1)] + repl + src[match.end(1):]
+    f = open(srcfile,'w')
+    f.write(src)
+    f.close()
+
+
+if __name__=='__main__':
+    if len(sys.argv)>2 and sys.argv[1]=='--md5update':
+        update_md5(sys.argv[2:])
+    else:
+        main(sys.argv[1:])
+
+
+
+
+

branches/0.3/setup.py

+# Copyright (c) 2006 L. C. Rees.  All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1.  Redistributions of source code must retain the above copyright notice,
+# this list of conditions and the following disclaimer.
+# 2.  Redistributions in binary form must reproduce the above copyright
+# notice, this list of conditions and the following disclaimer in the
+# documentation and/or other materials provided with the distribution.
+# 3.  Neither the name of the Portable Site Information Project nor the names
+# of its contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
+# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
+# POSSIBILITY OF SUCH DAMAGE.
+
+'''setup - setuptools based setup for wsgiform.'''
+
+import ez_setup
+ez_setup.use_setuptools()
+
+try:
+    from setuptools import setup
+except:
+    from distutils.core import setup
+
+setup(name='wsgiform',
+      version='0.2.7',
+      description='''WSGI form parsing and validation middleware''',
+      long_description='''WSGI middleware for validating and parsing HTML form
+submissions into dictionaries, individual 'environ' entries, FieldStorage
+instances, or keyword arguments passed to a WSGI application in the 'environ'
+dictionary. Supports data escaping and sterilization.
+
+Simplistic usage example:
+
+from wsgiform import form
+
+@form()
+def app(environ, start_response):
+    return environ['wsgiform.dict']
+
+if __name__ == '__main__':
+    http = make_server('', 8080, app)
+    http.serve_forever()''',
+      author='L. C. Rees',
+      author_email='lcrees@gmail.com',
+      license='BSD',
+      test_suite='test_wsgiform',
+      packages = ['wsgiform'],
+      zip_safe = True,
+      keywords='WSGI dispatch middleware web HTTP decorators',
+      classifiers=['Development Status :: 3 - Alpha',
+                    'Environment :: Web Environment',
+                    'License :: OSI Approved :: BSD License',
+                    'Natural Language :: English',
+                    'Operating System :: OS Independent',
+                    'Programming Language :: Python',
+                    'Topic :: Internet :: WWW/HTTP :: WSGI :: Middleware'])

branches/0.3/test_wsgiform.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice,
+#       this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of WsgiForm nor the names of its contributors may be
+#       used to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Unit tests for wsgiform.'''
+
+import wsgiform.form as wsgiform
+import unittest
+import StringIO
+import copy
+
+class TestWsgiForm(unittest.TestCase):
+    
+    '''Test cases for wsgiform.'''
+        
+    test_env = {
+        'CONTENT_LENGTH': '118',
+        'wsgi.multiprocess': 0,
+        'wsgi.version': (1, 0),
+        'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+        'SERVER_NAME': '127.0.0.1',
+        'wsgi.run_once': 0,
+        'wsgi.errors': StringIO.StringIO(),
+        'wsgi.multithread': 0,
+        'SCRIPT_NAME': '',
+        'wsgi.url_scheme': 'http',
+        'wsgi.input': StringIO.StringIO(
+'num=12121&str1=test&name=%3Ctag+id%3D%22Test%22%3EThis+is+a+%27test%27+%26+another.%3C%2Ftag%3E&state=NV&Submit=Submit'
+            ), 
+        'REQUEST_METHOD': 'POST',
+        'HTTP_HOST': '127.0.0.1',
+        'PATH_INFO': '/',
+        'SERVER_PORT': '80',
+        'SERVER_PROTOCOL': 'HTTP/1.0'}       
+
+    def dummy_app(self, environ, func):
+        return environ
+
+    def dummy_sr(self):
+        pass
+
+    def testfieldstorage(self):
+        '''Parses form data into a FieldStorage instance.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='fieldstorage')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.fieldstorage'].getfirst('num'), '12121')
+
+    def testdictionary(self):
+        '''Parses form data into a dictionary.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['num'], '12121')
+
+    def testkwargs(self):
+        '''Parses form data into keyword arguments.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['num'], '12121')
+
+    def testenviron(self):
+        '''Parses form data into individual environ entries.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.num'], '12121')
+
+    def testdict_escape(self):
+        '''Parses form data into a dictionary with HTML escaping. '''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='escape')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], '<tag id="Test">This is a 'test' & another.</tag>')         
+
+    def testkwargs_escape(self):
+        '''Parses form data into keyword arguments with HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='escape')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testenviron_escape(self):
+        '''Parses form data into individual environ entries with HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='escape')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testdict_strictescape(self):
+        '''Parses form data into a dictionary with strict HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='escape', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testkwargs_strictescape(self):
+        '''Parses form data into keyword arguments with strict HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='escape', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testenviron_strictescape(self):
+        '''Parses form data into individual environ entries with strict HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='escape', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testdict_sterilize(self):
+        '''Parses form data into a dictionary with data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='sterilize')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testkwargs_sterilize(self):
+        '''Parses form data into keyword arguments with data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='sterilize')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testenviron_sterilize(self):
+        '''Parses form data into individual environ entries with data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='sterilize')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], 'tag idTestThis is a test  another.tag')
+
+    def testdict_strictsterilize(self):
+        '''Parses form data into a dictionary with strict data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='sterilize', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testkwargs_strictsterilize(self):
+        '''Parses form data into keyword arguments with strict data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='sterilize', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testenviron_strictsterilize(self):
+        '''Parses form data into individual environ entries with strict data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='sterilize', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], 'tag idTestThis is a test  another.tag')
+
+
+if __name__ == '__main__': unittest.main()

branches/0.3/wsgiform/__init__.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice,
+#       this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of WsgiForm nor the names of its contributors may be
+#       used to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+
+__author__ = 'L.C. Rees (lcrees@gmail.com)'
+__revision__ = '0.2.7'

branches/0.3/wsgiform/form.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice, 
+#       this list of conditions and the following disclaimer.
+#    
+#    2. Redistributions in binary form must reproduce the above copyright 
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of WsgiForm nor the names of its contributors may be
+#       used to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''WSGI middleware for parsing form submissions into dictionaries, individual
+'environ' entries, FieldStorage instances, or keyword arguments that can be
+passed in the 'environ' dictionary to a WSGI application. Features include hooks
+for form validation, escaping, and data sterilization.
+'''
+
+import cgi
+from copy import copy
+from wsgiform.util import escapeform, hyperform, sterileform
+
+__all__ = ['WsgiForm', 'form']
+
+def _errorapp(environ, start_response):
+    '''Simple error handler for form validation errors.'''
+    start_response('200 OK', ('Content-type', 'text/plain'))
+    return ['Data in field %s was invalid.' % environ['wsgiform.error']]
+
+def form(**kw):
+    '''Decorator for form processing.'''
+    def decorator(application):
+        return WsgiForm(application, **kw)
+    return decorator
+     
+
+class WsgiForm(object):
+
+    '''Class that parses form data into dictionaries, individual 'environ'
+    entries, FieldStorage instances, or keyword arguments that can be passed to
+    WSGI applications in the environ dictionary.
+    '''
+    
+    _environ = None
+    _keys = {'fieldstorage':'wsgiform.fieldstorage', 'dict':'wsgiform.dict',
+        'kwargs':'wsgize.kwargs', 'environ':'wsgiform.%s',
+        'routing_args':'wsgiorg.routing_args'}
+    _funclist = {'escape':escapeform, 'hyperescape':hyperform,
+        'sterilize':sterileform} 
+
+    def __init__(self, app, **kw):
+        self._app = app
+        # Style and key for form data passing format
+        self._style = kw.get('style', 'dict')
+        self._key = self._keys.get(self._style)
+        # Dictionary of validating functions where keywords == form field names 
+        self._validators = kw.get('validators', {})
+        # Stop on errors or empty fields
+        self._strict = kw.get('strict', False)
+        # Function to escape metachars
+        self._func = self._funclist[(kw.get('func', 'escape'))]
+        # Function to handle form validation errors
+        self._errorhandler = kw.get('errapp', _errorapp)
+        # Preserve wsgi.input
+        self._keep = kw.get('keep', True)
+        
+    def __call__(self, env, start_response):
+        if self._keep:
+            backup = copy(env['wsgi.input'])
+        else:
+            backup = env['wsgi.input']
+        # Return fieldstorage
+        if self._style == 'fieldstorage':
+            env[self._key] = cgi.FieldStorage(fp=env['wsgi.input'], environ=env,
+                keep_blank_values=True)
+        else:
+            qdict, self._environ = self._func(env, self._strict), env
+            if self._validators and not self._validate(qdict):
+                return self._errorhandler(env, start_response)
+            # Make individual environ entries
+            if self._style == 'environ':
+                for k, v in qdict.iteritems(): env[self._key % k] = v
+            # wsgi.routing_vars style
+            elif self._style == 'routing_args':
+                args, kwargs = env.get(self._key, ((), {}))
+                env[self._key] = (args, kwargs.update(qdict))
+            # Make kwargs or dict
+            else:
+                env[self._key] = qdict
+        env['wsgi.input'] = backup
+        return self._app(env, start_response)
+    
+    def _validate(self, qdict):
+        '''Validates form data.
+
+        qdict Dictionary of validators indexed by form field        
+        '''
+        for key, value in qdict.iteritems():
+            try:
+                if not self._validators[key](value):
+                    self._environ['wsgiform.error'] = key
+                    return False
+            except KeyError: return False
+        return True

branches/0.3/wsgiform/tests/test_wsgiform.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice,
+#       this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of WsgiForm nor the names of its contributors may be
+#       used to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Unit tests for wsgiform.'''
+
+import wsgiform.form as wsgiform
+import unittest
+import StringIO
+import copy
+
+class TestWsgiForm(unittest.TestCase):
+    
+    '''Test cases for wsgiform.'''
+        
+    test_env = {
+        'CONTENT_LENGTH': '118',
+        'wsgi.multiprocess': 0,
+        'wsgi.version': (1, 0),
+        'CONTENT_TYPE': 'application/x-www-form-urlencoded',
+        'SERVER_NAME': '127.0.0.1',
+        'wsgi.run_once': 0,
+        'wsgi.errors': StringIO.StringIO(),
+        'wsgi.multithread': 0,
+        'SCRIPT_NAME': '',
+        'wsgi.url_scheme': 'http',
+        'wsgi.input': StringIO.StringIO(
+'num=12121&str1=test&name=%3Ctag+id%3D%22Test%22%3EThis+is+a+%27test%27+%26+another.%3C%2Ftag%3E&state=NV&Submit=Submit'
+            ), 
+        'REQUEST_METHOD': 'POST',
+        'HTTP_HOST': '127.0.0.1',
+        'PATH_INFO': '/',
+        'SERVER_PORT': '80',
+        'SERVER_PROTOCOL': 'HTTP/1.0'}       
+
+    def dummy_app(self, environ, func):
+        return environ
+
+    def dummy_sr(self):
+        pass
+
+    def testfieldstorage(self):
+        '''Parses form data into a FieldStorage instance.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='fieldstorage')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.fieldstorage'].getfirst('num'), '12121')
+
+    def testdictionary(self):
+        '''Parses form data into a dictionary.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['num'], '12121')
+
+    def testkwargs(self):
+        '''Parses form data into keyword arguments.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['num'], '12121')
+
+    def testenviron(self):
+        '''Parses form data into individual environ entries.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.num'], '12121')
+
+    def testdict_escape(self):
+        '''Parses form data into a dictionary with HTML escaping. '''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='escape')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], '<tag id="Test">This is a 'test' & another.</tag>')         
+
+    def testkwargs_escape(self):
+        '''Parses form data into keyword arguments with HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='escape')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testenviron_escape(self):
+        '''Parses form data into individual environ entries with HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='escape')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testdict_strictescape(self):
+        '''Parses form data into a dictionary with strict HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='escape', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testkwargs_strictescape(self):
+        '''Parses form data into keyword arguments with strict HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='escape', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testenviron_strictescape(self):
+        '''Parses form data into individual environ entries with strict HTML escaping.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='escape', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], '<tag id="Test">This is a 'test' & another.</tag>')
+
+    def testdict_sterilize(self):
+        '''Parses form data into a dictionary with data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='sterilize')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testkwargs_sterilize(self):
+        '''Parses form data into keyword arguments with data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='sterilize')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testenviron_sterilize(self):
+        '''Parses form data into individual environ entries with data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='sterilize')
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], 'tag idTestThis is a test  another.tag')
+
+    def testdict_strictsterilize(self):
+        '''Parses form data into a dictionary with strict data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='dict', func='sterilize', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.dict']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testkwargs_strictsterilize(self):
+        '''Parses form data into keyword arguments with strict data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='kwargs', func='sterilize', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgize.kwargs']['name'], 'tag idTestThis is a test  another.tag')
+
+    def testenviron_strictsterilize(self):
+        '''Parses form data into individual environ entries with strict data sterilization.'''
+        form = wsgiform.WsgiForm(self.dummy_app, style='environ', func='sterilize', strict=True)
+        env = form(copy.deepcopy(self.test_env), self.dummy_sr)
+        self.assertEqual(env['wsgiform.name'], 'tag idTestThis is a test  another.tag')
+
+
+if __name__ == '__main__': unittest.main()

branches/0.3/wsgiform/util.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice,
+#       this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of WsgiForm nor the names of its contributors may be
+#       used to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Utilities for server-side form processing.'''
+
+import cgi, re, string
+from xml.sax import saxutils
+
+_trans = string.maketrans('', '')
+
+def _formparse(environ, strict=False):
+    '''Extracts strings from form submissions.
+
+    @param environ Environment dictionary
+    @param strict Stops on errors (default: False)
+    '''
+    qdict = cgi.parse(environ['wsgi.input'], environ, strict, strict)
+    for key, value in qdict.iteritems():
+        if len(value) == 1: qdict[key] = value[0]
+    return qdict
+
+def _runfunc(qdict, func):
+    '''Runs a function on a dictionary.
+
+    @param qdict Dictionary
+    @param func Function
+    '''
+    # Handle single entries
+    for key, value in qdict.iteritems():
+        if isinstance(value, basestring):
+            qdict[key] = func(value)
+        # Handle lists
+        elif isinstance(value, list):
+            for num, item in enumerate(value):
+                if isinstance(item, basestring): value[num] = func(item)
+    return qdict
+
+def escape(data):
+    '''Escapes &, <, >, ", and ' with HTML entities.
+
+    @param data Text data
+    '''
+    return saxutils.escape(data, {'"':"&quot;", "'":'&#39;'})
+
+def escapeform(environ, strict=False):
+    '''Escapes common XML/HTML entities in form data.
+
+    @param environ Environment dictionary
+    @param strict Stops on errors (default: False)
+    '''    
+    return _runfunc(_formparse(environ, strict), escape)
+
+def hyperescape(data):
+    '''Escapes punctuation with HTML entities except ., -, and _.
+
+    @param data Text data
+    '''
+    # Escape &
+    data = re.sub(r'&(?!#\d\d;)', '&#38;', data)
+    # Escape ;
+    data = re.sub(r'(?<!&#\d\d);', '&#59;', data)
+    # Escape #
+    data = re.sub(r'(?<!&)#(?!\d\d;)', '&#35;', data)
+    # Escape other chars except ., -, and _
+    for char in '<>"\'()!${}*+,%/:=?@[\\]^`|~':
+        data = data.replace(char, '&#%d;' % ord(char))
+    return data
+
+def hyperform(environ, strict=False):
+    '''Hyper-escapes all XML/HTML entitites in form data.
+
+    @param environ Environment dictionary
+    @param strict Stops on errors (default: False)
+    '''    
+    return _runfunc(_formparse(environ, strict), hyperescape) 
+
+def sterileform(environ, strict=False):
+    '''Removes all form data characters except alphanumerics, ., -, and _.
+
+    @param environ Environment dictionary
+    @param strict Stops on errors (default: False)
+    '''
+    return _runfunc(_formparse(environ, strict), sterilize)
+
+def sterilize(data):
+    '''Removes all ASCII characters except alphanums, ., -, and _.
+
+    @param data Text data
+    '''
+    return data.translate(_trans, '&#;<>"\'()!${}*+,%/:=?@[\\]^`|~')
+
+__all__ = ['hyperescape', 'escape', 'sterilize', 'escapeform', 'hyperform',
+    'sterileform']

branches/0.3/wsgiform/validators.py

+# Copyright (c) 2006 L. C. Rees
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+#    1. Redistributions of source code must retain the above copyright notice,
+#       this list of conditions and the following disclaimer.
+#
+#    2. Redistributions in binary form must reproduce the above copyright
+#       notice, this list of conditions and the following disclaimer in the
+#       documentation and/or other materials provided with the distribution.
+#
+#    3. Neither the name of WsgiForm nor the names of its contributors may be
+#       used to endorse or promote products derived from this software without
+#       specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+'''Form validation utilities.'''
+
+import re, urllib
+from xml.dom.expatbuilder import parseString
+from HTMLParser import HTMLParser
+
+# Miscellaneous specialized regexes
+_patterns = {
+# US Social Security Number - Dennis Flynn - regexlib.com 
+'ssn':r'(^|\s)(00[1-9]|0[1-9]0|0[1-9][1-9]|[1-6]\d{2}|7[0-6]\d|77[0-2])(-?|[\. ])([1-9]0|0[1-9]|[1-9][1-9])\3(\d{3}[1-9]|[1-9]\d{3}|\d[1-9]\d{2}|\d{2}[1-9]\d)($|\s|[;:,!\.\?])',
+# US currency checker - Michael Ash - regexlib.com
+'usd':r'^\$(\d{1,3}(\,\d{3})*|(\d+))(\.\d{2})?$',
+# MAC Address - Ted Rudyk - regexlib.com 
+'macaddr':r'^([0-9a-fA-F][0-9a-fA-F]:){5}([0-9a-fA-F][0-9a-fA-F])$',
+# IP4 address - Andrew Polshaw - regexlib.com
+'ip4':r'^(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])\.(\d{1,2}|1\d\d|2[0-4]\d|25[0-5])$',
+# IP6 address - Glynn Beeken - regexlib.com
+'ip6':r'^(^(([0-9A-F]{1,4}(((:[0-9A-F]{1,4}){5}::[0-9A-F]{1,4})|((:[0-9A-F]{1,4}){4}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,1})|((:[0-9A-F]{1,4}){3}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,2})|((:[0-9A-F]{1,4}){2}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,3})|(:[0-9A-F]{1,4}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,4})|(::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,5})|(:[0-9A-F]{1,4}){7}))$|^(::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,6})$)|^::$)|^((([0-9A-F]{1,4}(((:[0-9A-F]{1,4}){3}::([0-9A-F]{1,4}){1})|((:[0-9A-F]{1,4}){2}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,1})|((:[0-9A-F]{1,4}){1}::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,2})|(::[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,3})|((:[0-9A-F]{1,4}){0,5})))|([:]{2}[0-9A-F]{1,4}(:[0-9A-F]{1,4}){0,4})):|::)((25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{0,2})\.){3}(25[0-5]|2[0-4][0-9]|[0-1]?[0-9]{0,2})$$',
+# Complex password enforcer 1 upper 1 lower 1 num 1 special min/max - Matthew Hazzard - regexlib.com
+'password':r'(?=^.{8,255}$)((?=.*\d)(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[^A-Za-z0-9])(?=.*[a-z])|(?=.*[^A-Za-z0-9])(?=.*[A-Z])(?=.*[a-z])|(?=.*\d)(?=.*[A-Z])(?=.*[^A-Za-z0-9]))^.*',
+# Name check - tom ferguson - regexlib.com
+'namecheck':r'(^([ \u00c0-\u01ffa-zA-Z\'])+$)|(^[a-z\.]*\s?([a-z\-\']+\s)+[a-z\-\']+$)',
+# US state abbreviations - Michael Ash - regexlib.com
+'usstates':r'^(A[LKSZRAEP]|C[AOT]|D[EC]|F[LM]|G[AU]|HI|I[ADLN]|K[SY]|LA|M[ADEHINOPST]|N[CDEHJMVY]|O[HKR]|P[ARW]|RI|S[CD]|T[NX]|UT|V[AIT]|W[AIVY])$',
+# Canada provice abbeviations - Matthew Hartman - regexlib.com
+'canada':r'^(N[BLSTU]|[AMN]B|[BQ]C|ON|PE|SK)$',
+# Credit Card - David Conorozzo - regexlib.com
+'creditcard':r'^3(?:[47]\d([ -]?)\d{4}(?:\1\d{4}){2}|0[0-5]\d{11}|[68]\d{12})$|^4(?:\d\d\d)?([ -]?)\d{4}(?:\2\d{4}){2}$|^6011([ -]?)\d{4}(?:\3\d{4}){2}$|^5[1-5]\d\d([ -]?)\d{4}(?:\4\d{4}){2}$|^2014\d{11}$|^2149\d{11}$|^2131\d{11}$|^1800\d{11}$|^3\d{15}$',
+# Malice protection - Shahar Bracha - regexlib.com
+'demalice':r'((script)|(&lt;)|(&gt;)|(%3c)|(%3e)|(SELECT) |(UPDATE) |(INSERT) |(DELETE)|(GRANT) |(REVOKE)|(UNION)|(&amp;lt;)|(&amp;gt;))|(^[^<>`~!/@\#}$%:;)(_^{&*=|\'+]+$)',
+# Email - Shaune Stark - regexlib.com
+'email':r'^([0-9a-zA-Z]([-.\w]*[0-9a-zA-Z])*@(([0-9a-zA-Z])+([-\w]*[0-9a-zA-Z])*\.)+[a-zA-Z]{2,9})$',
+# URL - Brian Bothwell - regexlib.com
+'url':r'^(file|ftp|gopher|hdl|http|https|imap|mailto|mms|news|nntp|prospero|rsync|rtsp|rtspu|shttp|sip|snews|svn|svn+ssh|telnet|wais)\://([a-zA-Z0-9\.\-]+(\:[a-zA-Z0-9\.&amp;%\$\-]+)*@)?((25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9])\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[1-9]|0)\.(25[0-5]|2[0-4][0-9]|[0-1]{1}[0-9]{2}|[1-9]{1}[0-9]{1}|[0-9])|([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9\-]+\.[a-zA-Z]{2,4})(\:[0-9]+)?(/[^/][a-zA-Z0-9\.\,\?\'\\/\+&amp;%\$#\=~_\-@]*)*$',
+# DateTime mm/dd/yyyy hh:MM:ss - Michael Ash - regexlib.com
+'datetime':r'(?=\d)^(?:(?!(?:10\D(?:0?[5-9]|1[0-4])\D(?:1582))|(?:0?9\D(?:0?[3-9]|1[0-3])\D(?:1752)))((?:0?[13578]|1[02])|(?:0?[469]|11)(?!\/31)(?!-31)(?!\.31)|(?:0?2(?=.?(?:(?:29.(?!000[04]|(?:(?:1[^0-6]|[2468][^048]|[3579][^26])00))(?:(?:(?:\d\d)(?:[02468][048]|[13579][26])(?!\x20BC))|(?:00(?:42|3[0369]|2[147]|1[258]|09)\x20BC))))))|(?:0?2(?=.(?:(?:\d\D)|(?:[01]\d)|(?:2[0-8])))))([-.\/])(0?[1-9]|[12]\d|3[01])\2(?!0000)((?=(?:00(?:4[0-5]|[0-3]?\d)\x20BC)|(?:\d{4}(?!\x20BC)))\d{4}(?:\x20BC)?)(?:$|(?=\x20\d)\x20))?((?:(?:0?[1-9]|1[012])(?::[0-5]\d){0,2}(?:\x20[aApP][mM]))|(?:[01]\d|2[0-3])(?::[0-5]\d){1,2})?$',
+# DateTime yyyy/mm/dd hh:MM:ss - Michael Ash - regexlib.com
+'eurodatetime':r'^(?=\d)(?:(?!(?:1582(?:\.|-|\/)10(?:\.|-|\/)(?:0?[5-9]|1[0-4]))|(?:1752(?:\.|-|\/)0?9(?:\.|-|\/)(?:0?[3-9]|1[0-3])))(?=(?:(?!000[04]|(?:(?:1[^0-6]|[2468][^048]|[3579][^26])00))(?:(?:\d\d)(?:[02468][048]|[13579][26]))\D0?2\D29)|(?:\d{4}\D(?!(?:0?[2469]|11)\D31)(?!0?2(?:\.|-|\/)(?:29|30))))(\d{4})([-\/.])(0?\d|1[012])\2((?!00)[012]?\d|3[01])(?:$|(?=\x20\d)\x20))?((?:(?:0?[1-9]|1[012])(?::[0-5]\d){0,2}(?:\x20[aApP][mM]))|(?:[01]\d|2[0-3])(?::[0-5]\d){1,2})?$',
+# US Phone - Tim N Tousley - regexlib.com 
+'usphone':r'^([a-zA-Z,#/ \.\(\)\-\+\*]*[2-9])([a-zA-Z,#/ \.\(\)\-\+\*]*[0-9]){2}([a-zA-Z,#/ \.\(\)\-\+\*]*[2-9])([a-zA-Z,#/ \.\(\)\-\+\*]*[0-9]){6}[0-9a-zA-Z,#/ \.\(\)\-\+\*]*$',
+# UK Phone - M h - regexlib.com 
+'ukphone':r'^\s*\(?(020[7,8]{1}\)?[ ]?[1-9]{1}[0-9{2}[ ]?[0-9]{4})|(0[1-8]{1}[0-9]{3}\)?[ ]?[1-9]{1}[0-9]{2}[ ]?[0-9]{3})\s*$',
+# International phone - Dmitry Kandiner - regexlib.com
+'intlphone':r'^(\+[1-9][0-9]*(\([0-9]*\)|-[0-9]*-))?[0]?[1-9][0-9\- ]*$',
+# Phone checker - Tim N Tousley - regexlib.com 
+'phone':r'^([a-zA-Z,#/ \.\(\)\-\+\*]*[0-9]){7}[0-9a-zA-Z,#/ \.\(\)\-\+\*]*$',
+# US-Canada postal/zip codes - Matthew Aznoe - regexlib.com 
+'napost':r'^((\d{5}-\d{4})|(\d{5})|([AaBbCcEeGgHhJjKkLlMmNnPpRrSsTtVvXxYy]\d[A-Za-z]\s?\d[A-Za-z]\d))$',
+# US Zip codes 5+4 +/- hyphen
+'zip':r'(^\d{5}$)|(\d{5}-*\d{4}$)',
+# UK postal codes - Scott Pite - regexlib.com
+'ukpost':r'^[A-Za-z]{1,2}[\d]{1,2}([A-Za-z])?\s?[\d][A-Za-z]{2}$',
+# ISBN - Michael Ash - regexlib.com
+'isbn':r'ISBN\x20(?=.{13}$)\d{1,5}([- ])\d{1,7}\1\d{1,6}\1(\d|X)$',
+# Number checker - Paul Auger - regexlib.com
+'num':r'^((\d?)|(([-+]?\d+\.?\d*)|([-+]?\d*\.?\d+))|(([-+]?\d+\.?\d*\,\ ?)*([-+]?\d+\.?\d*))|(([-+]?\d*\.?\d+\,\ ?)*([-+]?\d*\.?\d+))|(([-+]?\d+\.?\d*\,\ ?)*([-+]?\d*\.?\d+))|(([-+]?\d*\.?\d+\,\ ?)*([-+]?\d+\.?\d*)))$',
+# Euro VAT - Michal Valasek - regexlib.com
+'vat':r'((DK|FI|HU|LU|MT|SI)(-)?\d{8})|((BE|EE|DE|EL|LT|PT)(-)?\d{9})|((PL|SK)(-)?\d{10})|((IT|LV)(-)?\d{11})|((LT|SE)(-)?\d{12})|(AT(-)?U\d{8})|(CY(-)?\d{8}[A-Z])|(CZ(-)?\d{8,10})|(FR(-)?[\dA-HJ-NP-Z]{2}\d{9})|(IE(-)?\d[A-Z\d]\d{5}[A-Z])|(NL(-)?\d{9}B\d{2})|(ES(-)?[A-Z\d]\d{7}[A-Z\d])',
+}
+
+patterns = dict((k, re.compile(_patterns[k])) for k in _patterns) 
+
+def _validate(key, data):
+    '''Validates data by key.'''
+    if patterns[key].match(data): return True
+    
+def isascii(data):
+    '''Validates data is ASCII'''
+    return isinstance(str(data), str)
+
+def isunicode(data):
+    '''Validates data is unicode.'''
+    return isinstance(unicode(data), str)
+
+def isnumber(data):
+    '''Validates data is number.'''
+    return data.isdigits()
+
+def isalpha(data):
+    '''Validates data is all alphabetical.'''   
+    return data.isalpha()
+
+def isalphanum(data):
+    '''Validates data is alphanumeric.'''
+    return data.isalnum()
+
+def isspace(data):
+    '''Validates data is all spaces.'''
+    return data.isspace()
+
+def istitle(data):
+    '''Validates data is in title form.'''
+    return data.istitle()
+
+def isupper(data):
+    '''Validates data is uppercase.'''
+    return data.isupper()
+
+def islower(data):
+    '''Validates data is lowercase.'''
+    return data.islower()
+
+def islength(data, length):
+    '''Validates data is under or equals a defined length.'''
+    return len(data) <= length
+
+def iszipcode(data):
+    '''Validates data is a valid US ZIP code.'''
+    return _validate('zip', data)
+
+def isukpost(data):
+    '''Validates data is a valid UK postal code.'''
+    return _validate('ukpost', data)
+
+def iscnabbr(data):
+    '''Validates data is a valid Canadian provincial abbreviation.'''
+    return _validate('canada', data)
+
+def iscreditcardnum(data):
+    '''Validates data is a valid format for a credit card number.'''
+    return _validate('creditcard', data)
+
+def isdatetime(data):
+    '''Validates data is a valid format for mm/dd/yyyy hh:MM:ss.'''
+    return _validate('datetime', data)
+
+def notmalice(data):
+    '''Validates data is free of some potentially malicious content.'''
+    return _validate('demalice', data)
+
+def isemail(data):
+    '''Validates data is a valid email address.'''
+    return _validate('email', data)
+
+def isedatetime(data):
+    '''Validates data is a valid format for yyyy/mm/dd hh:MM:ss.'''
+    return _validate('eurodatetime', data)
+
+def isip4(data):
+    '''Validates data is a valid IPv4 address.'''
+    return _validate('ip4', data)
+
+def isip6(data):
+    '''Validates data is a valid IPv6 address.'''
+    return _validate('ip6', data)
+
+def isurl(data):
+    '''Validates data is a valid URL.'''
+    return _validate('url', data)
+
+def isliveurl(data):
+    '''Validates data is a live URL.'''
+    try:
+        urllib.urlopen(data)
+    except:
+        return False
+    return True        
+
+def isisbn(data):
+    '''Validates data is ISBN.'''
+    return _validate('isbn', data)
+
+def ismacaddr(data):
+    '''Validates data is a valid MAC address.'''
+    return _validate('macaddr', data)
+
+def isname(data):
+    '''Validates data is seems like a valid personal name.'''
+    return _validate('namecheck', data)
+
+def isnapost(data):
+    '''Validates data is a US ZIP or Canadian postal code.'''
+    return _validate('napost', data)
+
+def ispassword(data):
+    '''Validates that data is a password has 1 upper 1 lower 1 num
+    1 and at least 8 characters.
+    '''
+    return _validate('password', data)
+
+def isphone(data):
+    '''Validates that data is a phone number.'''
+    return _validate('phone', data)
+
+def isssn(data):
+    '''Validates that data is a social security number.'''
+    return _validate('ssn', data)
+
+def isukphone(data):
+    '''Validates that data is a UK phone number.'''
+    return _validate('ukphone', data)
+
+def isusd(data):
+    '''Validates that data is a UK phone number.'''
+    return _validate('usd', data)
+
+def isusphone(data):
+    '''Validates that data is a US phone number.'''
+    return _validate('usphone', data)
+
+def isusstateabbr(data):
+    '''Validates that data is a US state abbreviation.'''
+    return _validate('usstates', data)
+
+def isvat(data):
+    '''Validates that data is a EU VAT number.'''
+    return _validate('vat', data)
+
+def isxml(data):
+    '''Validates data is well-formed XML.'''
+    try:
+        parseString(data)
+    except:
+        return False
+    return True
+
+def ishtml(data):
+    '''Validates that data is not obviously broken HTML.'''
+    try:
+        parser = HTMLParser()
+        parser.feed(data)
+    except:
+        return False
+    return True
+
+def isempty(data):
+    '''Validates data is not empty.'''
+    return data.strip == ''