diff --git a/.gitignore b/.gitignore
deleted file mode 100644
index 2425b0a1836c83880fd486766478a0eb5226d0fa..0000000000000000000000000000000000000000
--- a/.gitignore
+++ /dev/null
@@ -1,8 +0,0 @@
-*~
-*.pyc
-*.swp
-.tox
-MANIFEST
-dist/
-.envrc
-.direnv
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index bdb971161b78d8225b4df4eeb1c6b56978242ef4..0000000000000000000000000000000000000000
--- a/.travis.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-language: python
-
-python:
-  - 3.3
-  - 3.2
-
-install:
-  - pip install --use-mirrors Django==1.5
-  - python setup.py install
-script: ./run_tests.sh
diff --git a/CHANGES.md b/CHANGES.md
deleted file mode 100644
index eb107a357ce561e0ea88b62dc9c24e465b06b5fa..0000000000000000000000000000000000000000
--- a/CHANGES.md
+++ /dev/null
@@ -1,43 +0,0 @@
-
-As of 3.0.0:
-
-* API changes
-  * SQLStore implementations no longer create or use a 'settings'
-    table
-  * SRegResponse.fromSuccessResponse returns None when no signed
-    arguments were found
-  * Added functions to generate request/response HTML forms with
-    auto-submission javascript
-      * Consumer (relying party) API: AuthRequest.htmlMarkup
-      * Server API: server.OpenIDResponse.toHTML
-  * PAPE (Provider Authentication Policy Extension) module
-      * Updated extension for specification draft 2
-      * Request.fromSuccessResponse returns None if PAPE response
-        arguments were not signed
-
-* New features
-  * Demo server now supports OP-driven identifier selection
-  * Demo consumer now has a "stateless" option
-  * Fetchers now only read/request first megabyte of response
-
-* Bug fixes
-  * NOT NULL constraints were added to SQLStore tables where
-    appropriate
-  * message.fromPostArgs: use query.items() instead of iteritems(),
-    fixes #161 (Affects Django users)
-  * check_authentication requests: copy entire response, not just
-    signed fields.  Fixes missing namespace in check_authentication
-    requests
-  * Consumer._verifyDiscoveryResults: fall back to OpenID 1.0 type if
-    1.1 endpoint cannot be found; fixes discovery verification bug for
-    certain OpenID 1 identifiers
-  * SQLStore._execSQL: convert unicode arguments to str to avoid
-    postgresql api bug with unicode objects (Thanks to Marek Kuziel.)
-  * MySQLStore: Use ENGINE instead of TYPE when creating tables
-  * server.OpenIDResponse.toFormMarkup: Use return_to from the
-    request, not the response fields (Not all responses (i.e. cancel,
-    setup_needed) include a return_to field.)
-  * server.AssociationRequest.answer: include session_type in
-    no-encryption assoc responses
-  * OpenIDServiceEndpoint.getDisplayIdentifier: Don't include the
-    fragment in display identifiers.
diff --git a/PKG-INFO b/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..e7a66b80723a0bf4745cad7baf86a9820d148fd0
--- /dev/null
+++ b/PKG-INFO
@@ -0,0 +1,26 @@
+Metadata-Version: 1.1
+Name: python3-openid
+Version: 3.0.9
+Summary: OpenID support for modern servers and consumers.
+Home-page: http://github.com/necaris/python3-openid
+Author: Rami Chowdhury
+Author-email: rami.chowdhury@gmail.com
+License: UNKNOWN
+Download-URL: http://github.com/necaris/python3-openid/tarball/v3.0.9
+Description: This is a set of Python packages to support use of
+        the OpenID decentralized identity system in your application, update to Python
+        3.  Want to enable single sign-on for your web site?  Use the openid.consumer
+        package.  Want to run your own OpenID server? Check out openid.server.
+        Includes example code and support for a variety of storage back-ends.
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
diff --git a/README.md b/README.md
deleted file mode 100644
index a1691c8a5388df5cc5ba34071aef8b212c45b214..0000000000000000000000000000000000000000
--- a/README.md
+++ /dev/null
@@ -1,53 +0,0 @@
-*NOTE*: This started out as a fork of the Python OpenID library, with changes
-to make it Python 3 compatible. It's now a port of that library, including
-cleanups and updates to the code in general.
-
-[![Build Status](https://travis-ci.org/necaris/python3-openid.png?branch=master)](https://travis-ci.org/necaris/python3-openid)
-
-# REQUIREMENTS
-
- - Python 3.x (tested on 3.2, 3.3, 3.4)
-
-# INSTALLATION
-
-The recommended way is to install from PyPI with `pip`:
-
-    pip install python3-openid
-
-Alternatively, you can run the following command:
-
-    python setup.py install
-
-
-# GETTING STARTED
-
-The library should follow the existing `python-openid` API as closely as possible.
-
-*NOTE*: documentation will be auto-generated as soon as I can figure out how to update the documentation tools.
-
-*NOTE*: The examples directory includes an example server and consumer
-implementation.  See the README file in that directory for more
-information on running the examples.
-
-# LOGGING
-
-This library offers a logging hook that will record unexpected
-conditions that occur in library code. If a condition is recoverable,
-the library will recover and issue a log message. If it is not
-recoverable, the library will raise an exception. See the
-documentation for the `openid.oidutil` module for more on the logging
-hook.
-
-# DOCUMENTATION
-
-The documentation in this library is in Epydoc format, which is
-detailed at:
-
-  http://epydoc.sourceforge.net/
-
-# CONTACT
-
-Going forward, the plan is to maintain this library on GitHub, so any bug
-reports, suggestions, and feature requests should be raised [here](issues).
-
-There are also the `#python-openid` and `#openid` channels on FreeNode IRC.
diff --git a/admin/runtests b/admin/runtests
index b2a3a79f68a9c9904658ae5910bb54ea63ba7abc..1343ea110f5928157b8d39027734ecf225791399 100755
--- a/admin/runtests
+++ b/admin/runtests
@@ -1,5 +1,8 @@
 #!/usr/bin/env python
-import os.path, sys, warnings
+import os.path
+import sys
+import unittest
+
 
 test_modules = [
     'cryptutil',
@@ -14,19 +17,19 @@ def fixpath():
         d = os.path.dirname(sys.argv[0])
     parent = os.path.normpath(os.path.join(d, '..'))
     if parent not in sys.path:
-        print "putting %s in sys.path" % (parent,)
+        print ("putting %s in sys.path" % (parent,))
         sys.path.insert(0, parent)
 
 def otherTests():
     failed = []
     for module_name in test_modules:
-        print 'Testing %s...' % (module_name,) ,
+        print ('Testing %s...' % (module_name,))
         sys.stdout.flush()
         module_name = 'openid.test.' + module_name
         try:
             test_mod = __import__(module_name, {}, {}, [None])
         except ImportError:
-            print 'Failed to import test %r' % (module_name,)
+            print ('Failed to import test %r' % (module_name,))
             failed.append(module_name)
         else:
             try:
@@ -37,7 +40,7 @@ def otherTests():
                 sys.excepthook(*sys.exc_info())
                 failed.append(module_name)
             else:
-                print 'Succeeded.'
+                print ('Succeeded.')
 
 
     return failed
@@ -72,9 +75,10 @@ def pyunitTests():
 
     try:
         from openid.test import test_examples
-    except ImportError, e:
+    except ImportError as e:
         if 'twill' in str(e):
-            warnings.warn("Could not import twill; skipping test_examples.")
+            raise unittest.SkipTest('Skipping test_examples. '
+                                    'Could not import twill.')
         else:
             raise
     else:
@@ -110,10 +114,10 @@ def pyunitTests():
         m = __import__('openid.test.%s' % (name,), {}, {}, ['unused'])
         try:
             s.addTest(m.pyUnitTests())
-        except AttributeError, ex:
+        except AttributeError as ex:
             # because the AttributeError doesn't actually say which
             # object it was.
-            print "Error loading tests from %s:" % (name,)
+            print ("Error loading tests from %s:" % (name,))
             raise
 
     runner = unittest.TextTestRunner() # verbosity=2)
@@ -125,7 +129,7 @@ def pyunitTests():
 def splitDir(d, count):
     # in python2.4 and above, it's easier to spell this as
     # d.rsplit(os.sep, count)
-    for i in xrange(count):
+    for i in range(count):
         d = os.path.dirname(d)
     return d
 
@@ -144,7 +148,9 @@ def _import_djopenid():
     djinit = os.path.join(djdir, '__init__.py')
 
     djopenid = types.ModuleType('djopenid')
-    execfile(djinit, djopenid.__dict__)
+    with open(djinit) as f:
+        code = compile(f.read(), djinit, 'exec')
+        exec(code, djopenid.__dict__)
     djopenid.__file__ = djinit
 
     # __path__ is the magic that makes child modules of the djopenid package
@@ -167,12 +173,11 @@ def django_tests():
 
     try:
         import django.test.simple
-    except ImportError, e:
-        warnings.warn("django.test.simple not found; "
-                      "django examples not tested.")
-        return 0
+    except ImportError as e:
+        raise unittest.SkipTest("django.test.simple not found; "
+                                "django examples not tested.")
     import djopenid.server.models, djopenid.consumer.models
-    print "Testing Django examples:"
+    print ("Testing Django examples:")
 
     # These tests do get put in to a pyunit test suite, so we could run them
     # with the other pyunit tests, but django also establishes a test database
@@ -193,7 +198,7 @@ def main():
     django_failures = django_tests()
 
     if other_failed:
-        print 'Failures:', ', '.join(other_failed)
+        print ('Failures:', ', '.join(other_failed))
 
     failed = (bool(other_failed) or
               bool(not pyunit_result.wasSuccessful()) or
diff --git a/contrib/upgrade-store-1.1-to-2.0 b/contrib/upgrade-store-1.1-to-2.0
index 1f587c357c48ab496301b01163594c2c9004d100..6a346826af565590301d058ebfc54e344e4097c7 100644
--- a/contrib/upgrade-store-1.1-to-2.0
+++ b/contrib/upgrade-store-1.1-to-2.0
@@ -119,13 +119,17 @@ def main(argv=None):
             return 1
         password = askForPassword()
         try:
-            import psycopg
+            import psycopg2
         except ImportError:
-            print "You need psycopg installed to update a postgres DB."
-            return 1
+            try:
+                from psycopg2cffi import compat
+                compat.register()
+            except:
+                print "You need psycopg2 installed to update a postgres DB."
+                return 1
 
         try:
-            db_conn = psycopg.connect(database = options.postgres_db_name,
+            db_conn = psycopg2.connect(database = options.postgres_db_name,
                                       user = options.username,
                                       host = options.db_host,
                                       password = password)
diff --git a/contrib/upgrade-store-1.1-to-2.0~ b/contrib/upgrade-store-1.1-to-2.0~
new file mode 100644
index 0000000000000000000000000000000000000000..2f6f0ebd6ad73ab6ab6ddcfc6304faa479c85f45
--- /dev/null
+++ b/contrib/upgrade-store-1.1-to-2.0~
@@ -0,0 +1,170 @@
+#!/usr/bin/env python
+# SQL Store Upgrade Script
+# for version 1.x to 2.0 of the OpenID library.
+# Doesn't depend on the openid library, so you can run this python
+# script to update databases for ruby or PHP as well.
+#
+# Testers note:
+#
+#   A SQLite3 db with the 1.2 schema exists in
+#   openid/test/data/openid-1.2-consumer-sqlitestore.db if you want something
+#   to try upgrading.
+#
+#   TODO:
+#    * test data for mysql and postgresql.
+#    * automated tests.
+
+import os
+import getpass
+import sys
+from optparse import OptionParser
+
+
+def askForPassword():
+    return getpass.getpass("DB Password: ")
+
+def askForConfirmation(dbname,tablename):
+    print """The table %s from the database %s will be dropped, and
+    an empty table with the new nonce table schema will replace it."""%(
+    tablename, dbname)
+    return raw_input("Continue? ").lower().strip().startswith('y')
+
+def doSQLiteUpgrade(db_conn, nonce_table_name='oid_nonces'):
+    cur = db_conn.cursor()
+    cur.execute('DROP TABLE %s'%nonce_table_name)
+    sql = """
+    CREATE TABLE %s (
+        server_url VARCHAR,
+        timestamp INTEGER,
+        salt CHAR(40),
+        UNIQUE(server_url, timestamp, salt)
+    );
+    """%nonce_table_name
+    cur.execute(sql)
+    cur.close()
+
+def doMySQLUpgrade(db_conn, nonce_table_name='oid_nonces'):
+    cur = db_conn.cursor()
+    cur.execute('DROP TABLE %s'%nonce_table_name)
+    sql = """
+    CREATE TABLE %s (
+        server_url BLOB,
+        timestamp INTEGER,
+        salt CHAR(40),
+        PRIMARY KEY (server_url(255), timestamp, salt)
+    )
+    TYPE=InnoDB;
+    """%nonce_table_name
+    cur.execute(sql)
+    cur.close()
+
+def doPostgreSQLUpgrade(db_conn, nonce_table_name='oid_nonces'):
+    cur = db_conn.cursor()
+    cur.execute('DROP TABLE %s'%nonce_table_name)
+    sql = """
+    CREATE TABLE %s (
+        server_url VARCHAR(2047),
+        timestamp INTEGER,
+        salt CHAR(40),
+        PRIMARY KEY (server_url, timestamp, salt)
+    );
+    """%nonce_table_name
+    cur.execute(sql)
+    cur.close()
+    db_conn.commit()
+
+def main(argv=None):
+    parser = OptionParser()
+    parser.add_option("-u", "--user", dest="username",
+                      default=os.environ.get('USER'),
+                      help="User name to use to connect to the DB.  "
+                      "Defaults to USER environment variable.")
+    parser.add_option('-t', '--table', dest='tablename', default='oid_nonces',
+                      help='The name of the nonce table to drop and recreate. '
+                      ' Defaults to "oid_nonces", the default table name for '
+                      'the openid stores.')
+    parser.add_option('--mysql', dest='mysql_db_name',
+                      help='Upgrade a table from this MySQL database.  '
+                      'Requires username for database.')
+    parser.add_option('--pg', '--postgresql', dest='postgres_db_name',
+                      help='Upgrade a table from this PostgreSQL database.  '
+                      'Requires username for database.')
+    parser.add_option('--sqlite', dest='sqlite_db_name',
+                      help='Upgrade a table from this SQLite database file.')
+    parser.add_option('--host', dest='db_host',
+                      default='localhost',
+                      help='Host on which to find MySQL or PostgreSQL DB.')
+    (options, args) = parser.parse_args(argv)
+
+    db_conn = None
+
+    if options.sqlite_db_name:
+        try:
+            from pysqlite2 import dbapi2 as sqlite
+        except ImportError:
+            print "You must have pysqlite2 installed in your PYTHONPATH."
+            return 1
+        try:
+            db_conn = sqlite.connect(options.sqlite_db_name)
+        except Exception, e:
+            print "Could not connect to SQLite database:", str(e)
+            return 1
+
+        if askForConfirmation(options.sqlite_db_name, options.tablename):
+            doSQLiteUpgrade(db_conn, nonce_table_name=options.tablename)
+
+    if options.postgres_db_name:
+        if not options.username:
+            print "A username is required to open a PostgreSQL Database."
+            return 1
+        password = askForPassword()
+        try:
+            import psycopg2
+        except ImportError:
+            print "You need psycopg2 installed to update a postgres DB."
+            return 1
+
+        try:
+            db_conn = psycopg2.connect(database = options.postgres_db_name,
+                                      user = options.username,
+                                      host = options.db_host,
+                                      password = password)
+        except Exception, e:
+            print "Could not connect to PostgreSQL database:", str(e)
+            return 1
+
+        if askForConfirmation(options.postgres_db_name, options.tablename):
+            doPostgreSQLUpgrade(db_conn, nonce_table_name=options.tablename)
+
+    if options.mysql_db_name:
+        if not options.username:
+            print "A username is required to open a MySQL Database."
+            return 1
+        password = askForPassword()
+        try:
+            import MySQLdb
+        except ImportError:
+            print "You must have MySQLdb installed to update a MySQL DB."
+            return 1
+
+        try:
+            db_conn = MySQLdb.connect(options.db_host, options.username,
+                                      password, options.mysql_db_name)
+        except Exception, e:
+            print "Could not connect to MySQL database:", str(e)
+            return 1
+
+        if askForConfirmation(options.mysql_db_name, options.tablename):
+            doMySQLUpgrade(db_conn, nonce_table_name=options.tablename)
+
+    if db_conn:
+        db_conn.close()
+    else:
+        parser.print_help()
+
+    return 0
+
+
+if __name__ == '__main__':
+    retval = main()
+    sys.exit(retval)
diff --git a/dev-requirements.txt b/dev-requirements.txt
deleted file mode 100644
index d6792cc1ce17ee9522046630324b272e6bbda0da..0000000000000000000000000000000000000000
--- a/dev-requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-#
-nose==1.3.1
-Django
diff --git a/examples/README b/examples/README
deleted file mode 100644
index 87c0831944666409677e4964fd422245ef7dfd85..0000000000000000000000000000000000000000
--- a/examples/README
+++ /dev/null
@@ -1,91 +0,0 @@
-Python OpenID library example code
-==================================
-
-The examples directory contains working code illustrating the use of
-the library for performing OpenID authentication, both as a consumer
-and a server. There are two kinds of examples, one that can run
-without any external dependencies, and one that uses the Django Web
-framework. The examples do not illustrate how to use all of the
-features of the library, but they should be a good starting point to
-see how to use this library with your code.
-
-Both the Django libraries and the BaseHTTPServer examples require that
-the OpenID library is installed or that it has been added to Python's
-search path (PYTHONPATH environment variable or sys.path).
-
-The Django example is probably a good place to start reading the
-code. There is little that is Django-specific about the OpenID logic
-in the example, and it should be easy to port to any framework. To run
-the django examples, see the README file in the djopenid subdirectory.
-
-The other examples use Python's built-in BaseHTTPServer and have a
-good deal of ad-hoc dispatching and rendering code mixed in
-
-Using the BaseHTTPServer examples
-=================================
-
-This directory contains a working server and consumer that use this
-OpenID library. They are both written using python's standard
-BaseHTTPServer.
-
-
-To run the example system:
-
-1. Make sure you've installed the library, as explained in the
-   installation instructions.
-
-2. Start the consumer server:
-
-        python consumer.py --port 8001
-
-
-3. In another terminal, start the identity server:
-
-        python server.py --port 8000
-
-   (Hit Ctrl-C in either server's window to stop that server.)
-
-
-4. Open your web broswer, and go to the consumer server:
-
-        http://localhost:8001/
-
-   Note that all pages the consumer server shows will have "Python OpenID
-   Consumer Example" across the top.
-
-
-5. Enter an identity url managed by the sample identity server:
-
-        http://localhost:8000/id/bob
-
-
-6. The browser will be redirected to the sample server, which will be
-   requesting that you log in to proceed.  Enter the username for the
-   identity URL into the login box:
-
-        bob
-
-   Note that all pages the identity server shows will have "Python
-   OpenID Server Example" across the top.
-
-
-7. After you log in as bob, the server example will ask you if you
-   want to allow http://localhost:8001/ to know your identity.  Say
-   yes.
-
-
-8. You should end up back on the consumer site, at a page indicating
-   you've logged in successfully.
-
-
-That's a basic OpenID login procedure.  You can continue through it,
-playing with variations to see how they work.  The python code is
-intended to be a straightforward example of how to use the python
-OpenID library to function as either an identity server or consumer.
-
-Getting help
-============
-
-Please send bug reports, patches, and other feedback to
-
-  http://openid.net/developers/dev-mailing-lists/
diff --git a/examples/djopenid/README b/examples/djopenid/README
deleted file mode 100644
index 2d28b473f61ec5bb5f1a82f3959c39fb4f9843e7..0000000000000000000000000000000000000000
--- a/examples/djopenid/README
+++ /dev/null
@@ -1,54 +0,0 @@
-DJANGO EXAMPLE PACKAGE
-======================
-
-This package implements an example consumer and server for the Django
-Python web framework.  You can get Django (and learn more about it) at
-
-  http://www.djangoproject.com/
-
-SETUP
-=====
-
- 1. Install the OpenID library
-
- 2. Install Django >= 1.5 (with Python 3 support)
-
- 3. Modify djopenid/settings.py appropriately; you may wish to change
-    the database type or path, although the default settings should be
-    sufficient for most systems.
-
- 4. In examples/djopenid/ run:
-
-    python manage.py syncdb
-
- 5. To run the example consumer or server, run
-
-    python manage.py runserver PORT
-
-    where PORT is the port number on which to listen.
-
-    Note that if you want to try both the consumer and server at the
-    same time, run the command twice with two different values for
-    PORT.
-
- 6. Point your web browser at the server at
-
-    http://localhost:PORT/
-
-    to begin.
-
-ABOUT THE CODE
-==============
-
-The example server and consumer code provided in this package are
-intended to be instructional in the use of this OpenID library.  While
-it is not recommended to use the example code in production, the code
-should be sufficient to explain the general use of the library.
-
-If you aren't familiar with the Django web framework, you can quickly
-start looking at the important code by looking in the 'views' modules:
-
-  djopenid.consumer.views
-  djopenid.server.views
-
-Each view is a python callable that responds to an HTTP request.
diff --git a/examples/djopenid/server/tests.py b/examples/djopenid/server/tests.py
index 8ac86fa0ff342bc509f38cc90a5618a335ce7e28..de5f1e924cef06c528854246f17d08f91ea468e2 100644
--- a/examples/djopenid/server/tests.py
+++ b/examples/djopenid/server/tests.py
@@ -43,22 +43,22 @@ class TestProcessTrustResult(TestCase):
 
         response = views.processTrustResult(self.request)
 
-        self.failUnlessEqual(response.status_code, 302)
+        self.assertEqual(response.status_code, 302)
         finalURL = response['location']
-        self.failUnless('openid.mode=id_res' in finalURL, finalURL)
-        self.failUnless('openid.identity=' in finalURL, finalURL)
-        self.failUnless('openid.sreg.postcode=12345' in finalURL, finalURL)
+        self.assertIn('openid.mode=id_res', finalURL)
+        self.assertIn('openid.identity=', finalURL)
+        self.assertIn('openid.sreg.postcode=12345', finalURL)
 
     def test_cancel(self):
         self.request.POST['cancel'] = 'Yes'
 
         response = views.processTrustResult(self.request)
 
-        self.failUnlessEqual(response.status_code, 302)
+        self.assertEqual(response.status_code, 302)
         finalURL = response['location']
-        self.failUnless('openid.mode=cancel' in finalURL, finalURL)
-        self.failIf('openid.identity=' in finalURL, finalURL)
-        self.failIf('openid.sreg.postcode=12345' in finalURL, finalURL)
+        self.assertIn('openid.mode=cancel', finalURL)
+        self.assertNotIn('openid.identity=', finalURL)
+        self.assertNotIn('openid.sreg.postcode=12345', finalURL)
 
 
 class TestShowDecidePage(TestCase):
@@ -98,6 +98,6 @@ class TestGenericXRDS(TestCase):
         requested_url = 'http://requested.invalid/'
         (endpoint,) = applyFilter(requested_url, response.content)
 
-        self.failUnlessEqual(YADIS_CONTENT_TYPE, response['Content-Type'])
-        self.failUnlessEqual(type_uris, endpoint.type_uris)
-        self.failUnlessEqual(endpoint_url, endpoint.uri)
+        self.assertEqual(YADIS_CONTENT_TYPE, response['Content-Type'])
+        self.assertEqual(type_uris, endpoint.type_uris)
+        self.assertEqual(endpoint_url, endpoint.uri)
diff --git a/examples/djopenid/settings.py b/examples/djopenid/settings.py
index 1a33dcfdc80b92285b0f963a81e99413d4cdfa73..b1ccc2699a05f5baaa9815c0c7162a4eac9c7212 100644
--- a/examples/djopenid/settings.py
+++ b/examples/djopenid/settings.py
@@ -38,6 +38,7 @@ TIME_ZONE = 'Europe/London'
 # http://www.w3.org/TR/REC-html40/struct/dirlang.html#langcodes
 # http://blogs.law.harvard.edu/tech/stories/storyReader$15
 LANGUAGE_CODE = 'en-us.UTF-8'
+USE_I18N = False
 
 SITE_ID = 1
 
diff --git a/examples/djopenid/util.py b/examples/djopenid/util.py
index f37a4d04a0003502291862e71112b5aaa0c07311..fc21ceb5c78397e9f1f55bf9436e4bf2067f2d54 100644
--- a/examples/djopenid/util.py
+++ b/examples/djopenid/util.py
@@ -2,7 +2,6 @@
 """
 Utility code for the Django example consumer and server.
 """
-
 from urllib.parse import urljoin
 
 from django.db import connection
@@ -15,6 +14,12 @@ from django.shortcuts import render_to_response
 
 from django.conf import settings
 
+try:
+    import psycopg2
+except ImportError:
+    from psycopg2cffi import compat
+    compat.register()
+
 from openid.store.filestore import FileOpenIDStore
 from openid.store import sqlstore
 from openid.yadis.constants import YADIS_CONTENT_TYPE
diff --git a/openid/__init__.py b/openid/__init__.py
index 25eaaaae000c856837ad85ccdedc775ee31c9e16..8577355396434f9a5c26a52b02e839e317be7745 100644
--- a/openid/__init__.py
+++ b/openid/__init__.py
@@ -23,7 +23,7 @@ module.
     and limitations under the License.
 """
 
-version_info = (3, 0, 4)
+version_info = (3, 0, 9)
 
 __version__ = ".".join(str(x) for x in version_info)
 
diff --git a/openid/fetchers.py b/openid/fetchers.py
index 469e733ecdcc5479e4bbaf11e86b412a60866dca..68ff06976f5032c0b6d13da5e5469211ed73a98d 100644
--- a/openid/fetchers.py
+++ b/openid/fetchers.py
@@ -496,7 +496,7 @@ class HTTPLib2Fetcher(HTTPFetcher):
             final_url = url
 
         return HTTPResponse(
-            body=content,
+            body=content.decode(), # TODO Don't assume ASCII
             final_url=final_url,
             headers=dict(list(httplib2_response.items())),
             status=httplib2_response.status,
diff --git a/openid/oidutil.py b/openid/oidutil.py
index dbbba126e505faeb5e5380dc60dc76440b4d204d..70637c44e21d019530a36861e09a1b1e90d69e62 100644
--- a/openid/oidutil.py
+++ b/openid/oidutil.py
@@ -11,12 +11,17 @@ __all__ = ['log', 'appendArgs', 'toBase64', 'fromBase64', 'autoSubmitHTML',
 import binascii
 import logging
 
-import urllib.parse as urlparse
+# import urllib.parse as urlparse
 from urllib.parse import urlencode
 
 
+xxe_safe_elementtree_modules = [
+    'defusedxml.cElementTree',
+    'defusedxml.ElementTree',
+    ]
+
+
 elementtree_modules = [
-    'lxml.etree',
     'xml.etree.cElementTree',
     'xml.etree.ElementTree',
     'cElementTree',
@@ -61,6 +66,27 @@ for (var i = 0; i < elements.length; i++) {
     return html
 
 
+def importSafeElementTree(module_names=None):
+    """Find a working ElementTree implementation that is not vulnerable
+    to XXE, using `defusedxml`.
+
+    >>> XXESafeElementTree = importSafeElementTree()
+
+    @param module_names: The names of modules to try to use as
+        a safe ElementTree. Defaults to C{L{xxe_safe_elementtree_modules}}
+
+    @returns: An ElementTree module that is not vulnerable to XXE.
+    """
+    if module_names is None:
+        module_names = xxe_safe_elementtree_modules
+    try:
+        return importElementTree(module_names)
+    except ImportError:
+        raise ImportError('Unable to find a ElementTree module '
+            'that is not vulnerable to XXE. '
+            'Tried importing %r' % (module_names,))
+
+
 def importElementTree(module_names=None):
     """Find a working ElementTree implementation, trying the standard
     places that such a thing might show up.
diff --git a/openid/store/sqlstore.py b/openid/store/sqlstore.py
index 2c6e4082c15ecb017a3fd11da1cc21186f09b349..690ed19bc61aba05f76b8bb1a98ba34b556c98f3 100644
--- a/openid/store/sqlstore.py
+++ b/openid/store/sqlstore.py
@@ -10,6 +10,12 @@ python -c 'from openid.store import sqlstore; import pysqlite2.dbapi2;'
 import re
 import time
 
+try:
+    import psycopg2
+except ImportError:
+    from psycopg2cffi import compat
+    compat.register()
+
 from openid.association import Association
 from openid.store.interface import OpenIDStore
 from openid.store import nonce
@@ -228,6 +234,8 @@ class SQLStore(OpenIDStore):
         else:
             associations = []
             for values in rows:
+                values = list(values)
+                values[1] = self.blobDecode(values[1])
                 assoc = Association(*values)
                 if assoc.expiresIn == 0:
                     self.txn_removeAssociation(server_url, assoc.handle)
@@ -339,9 +347,6 @@ class SQLiteStore(SQLStore):
 
     clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < ?;'
 
-    def blobDecode(self, buf):
-        return str(buf)
-
     def blobEncode(self, s):
         return memoryview(s)
 
@@ -417,13 +422,6 @@ class MySQLStore(SQLStore):
 
     clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
 
-    def blobDecode(self, blob):
-        if type(blob) is str:
-            # Versions of MySQLdb >= 1.2.2
-            return blob
-        else:
-            # Versions of MySQLdb prior to 1.2.2 (as far as we can tell)
-            return blob.tostring()
 
 class PostgreSQLStore(SQLStore):
     """
@@ -434,16 +432,7 @@ class PostgreSQLStore(SQLStore):
 
     All other methods are implementation details.
     """
-
-    try:
-        import psycopg as exceptions
-    except ImportError:
-        # psycopg2 has the dbapi extension where the exception classes
-        # are available on the connection object. A psycopg2
-        # connection will use the correct exception classes because of
-        # this, and a psycopg connection will fall through to use the
-        # psycopg imported above.
-        exceptions = None
+    exceptions = None
 
     create_nonce_sql = """
     CREATE TABLE %(nonces)s (
@@ -510,9 +499,9 @@ class PostgreSQLStore(SQLStore):
     clean_nonce_sql = 'DELETE FROM %(nonces)s WHERE timestamp < %%s;'
 
     def blobEncode(self, blob):
-        try:
-            from psycopg2 import Binary
-        except ImportError:
-            from psycopg import Binary
+        from psycopg2 import Binary
 
         return Binary(blob)
+
+    def blobDecode(self, blob):
+        return blob.tobytes()
diff --git a/openid/test/__init__.py b/openid/test/__init__.py
index 266929737d664fe0f48fc03c0c427fac1a75421d..cb3c3b2169a593150cf979fa08ddcbda3e643047 100644
--- a/openid/test/__init__.py
+++ b/openid/test/__init__.py
@@ -154,8 +154,8 @@ def djangoExampleTests():
     try:
         import django.test.simple
     except ImportError:
-        warnings.warn("django.test.simple not found; skipping django examples.")
-        return 0
+        raise unittest.SkipTest("Skipping django examples. "
+                                "django.test.simple not found.")
 
     import djopenid.server.models
     import djopenid.consumer.models
diff --git a/openid/test/storetest.py b/openid/test/storetest.py
index 159e424a8351b1af437d316c6482b5449af2cc44..9aadb245f5197a5ddfa1aa6910bd2ef85d51c9a4 100644
--- a/openid/test/storetest.py
+++ b/openid/test/storetest.py
@@ -1,16 +1,21 @@
 import unittest
 import string
 import time
-import socket
-import random
 import os
-import warnings
+import uuid
+
+try:
+    import psycopg2
+except ImportError:
+    from psycopg2cffi import compat
+    compat.register()
 
 from openid.association import Association
 from openid.cryptutil import randomString
 from openid.store.nonce import mkNonce, split
 
-db_host = 'dbtest'
+
+db_host = os.environ.get('TEST_DB_HOST', 'dbtest')
 
 allowed_handle = []
 for c in string.printable:
@@ -27,12 +32,12 @@ generateSecret = randomString
 
 
 def getTmpDbName():
-    hostname = socket.gethostname()
-    hostname = hostname.replace('.', '_')
-    hostname = hostname.replace('-', '_')
-    return "%s_%d_%s_openid_test" % \
-           (hostname, os.getpid(), \
-            random.randrange(1, int(time.time())))
+    """Returns a name suitable for creating a temporary database.
+
+    Restrictions:
+     - Maximum length 64 characters (MySQL)
+    """
+    return "openid_test_%s" % uuid.uuid4().hex
 
 
 def testStore(store):
@@ -55,7 +60,7 @@ def testStore(store):
 
     def checkRetrieve(url, handle=None, expected=None):
         retrieved_assoc = store.getAssociation(url, handle)
-        assert retrieved_assoc == expected, (retrieved_assoc, expected)
+        assert retrieved_assoc == expected, (retrieved_assoc.__dict__, expected.__dict__)
         if expected is not None:
             if retrieved_assoc is expected:
                 print ('Unexpected: retrieved a reference to the expected '
@@ -250,15 +255,15 @@ def test_sqlite():
 
 
 def test_mysql():
-    # TODO fix
     from openid.store import sqlstore
     try:
         import MySQLdb
     except ImportError:
-        warnings.warn("Could not import MySQLdb. Skipping MySQL store tests.")
-        pass
+        raise unittest.SkipTest('Skipping MySQL store tests. '
+                                'Could not import MySQLdb.')
+
     else:
-        db_user = 'openid_test'
+        db_user = os.environ.get('TEST_MYSQL_USER', 'openid_test')
         db_passwd = ''
         db_name = getTmpDbName()
 
@@ -269,10 +274,10 @@ def test_mysql():
             conn = MySQLdb.connect(user=db_user, passwd=db_passwd,
                                    host=db_host)
         except MySQLdb.OperationalError as why:
-            if int(why) == 2005:
-                print(('Skipping MySQL store test (cannot connect '
-                       'to test server on host %r)' % (db_host,)))
-                return
+            if why.args[0] == 2005:
+                raise unittest.SkipTest('Skipping MySQL store test. '
+                                        'Cannot connect to server on host %r.'
+                                        % (db_host,))
             else:
                 raise
 
@@ -298,7 +303,7 @@ def test_postgresql():
     Tests the PostgreSQLStore on a locally-hosted PostgreSQL database
     cluster, version 7.4 or later.  To run this test, you must have:
 
-    - The 'psycopg' python module (version 1.1) installed
+    - The 'psycopg2' python module (version 1.1) installed
 
     - PostgreSQL running locally
 
@@ -321,19 +326,24 @@ def test_postgresql():
     """
     from openid.store import sqlstore
     try:
-        import psycopg
+        import psycopg2
     except ImportError:
-        warnings.warn("Could not import psycopg. Skipping PostgreSQL store tests.")
-        pass
+        raise unittest.SkipTest('Skipping PostgreSQL store tests. '
+                                'Could not import psycopg2.')
     else:
         db_name = getTmpDbName()
-        db_user = 'openid_test'
+        db_user = os.environ.get('TEST_POSTGRES_USER', 'openid_test')
 
         # Connect once to create the database; reconnect to access the
         # new database.
-        conn_create = psycopg.connect(database='template1', user=db_user,
-                                      host=db_host)
-        conn_create.autocommit()
+        try:
+            conn_create = psycopg2.connect(database='template1', user=db_user,
+                                           host=db_host)
+        except psycopg2.OperationalError as why:
+            raise unittest.SkipTest('Skipping PostgreSQL store test: %s'
+                                     % why)
+
+        conn_create.autocommit = True
 
         # Create the test database.
         cursor = conn_create.cursor()
@@ -341,7 +351,7 @@ def test_postgresql():
         conn_create.close()
 
         # Connect to the test database.
-        conn_test = psycopg.connect(database=db_name, user=db_user,
+        conn_test = psycopg2.connect(database=db_name, user=db_user,
                                     host=db_host)
 
         # OK, we're in the right environment. Create the store
@@ -363,9 +373,9 @@ def test_postgresql():
         time.sleep(1)
 
         # Remove the database now that the test is over.
-        conn_remove = psycopg.connect(database='template1', user=db_user,
+        conn_remove = psycopg2.connect(database='template1', user=db_user,
                                       host=db_host)
-        conn_remove.autocommit()
+        conn_remove.autocommit = True
 
         cursor = conn_remove.cursor()
         cursor.execute('DROP DATABASE %s;' % (db_name,))
diff --git a/openid/test/test_examples.py b/openid/test/test_examples.py
index 16ab8d3b3fd344e8fb3d74d032fdb1a1ef2484a4..9ddbfdedf17bb81e8fa3bbb6bc335f84b4fb4b5b 100644
--- a/openid/test/test_examples.py
+++ b/openid/test/test_examples.py
@@ -7,6 +7,8 @@ import sys
 import time
 from io import StringIO
 
+# TODO: Replace twill with MechanicalSoup or similar
+'''
 import twill.commands
 import twill.parse
 import twill.unit
@@ -177,7 +179,7 @@ class TestServer(unittest.TestCase):
     def tearDown(self):
         twill.set_output(None)
         twill.set_errout(None)
-
+'''
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/openid/test/test_fetchers.py b/openid/test/test_fetchers.py
index 8e9f5def776c968693ea84d17b15443fa67e734f..c97ffbaf2417f1a003e8cf659db098c2519adcb8 100644
--- a/openid/test/test_fetchers.py
+++ b/openid/test/test_fetchers.py
@@ -107,10 +107,9 @@ def run_fetcher_tests(server):
                 try:
                     __import__(library_name)
                 except ImportError:
-                    warnings.warn(
+                    raise unittest.SkipTest(
                         'Skipping tests for %r fetcher because '
                         'the library did not import.' % (library_name,))
-                    pass
                 else:
                     assert False, ('%s present but not detected' % (
                         library_name,))
diff --git a/openid/test/test_parsehtml.py b/openid/test/test_parsehtml.py
index b3c647577844207dc9983b533a27d9fd140c0936..387ddc55a0a2374627ebc18f6d9d68f766f4094c 100644
--- a/openid/test/test_parsehtml.py
+++ b/openid/test/test_parsehtml.py
@@ -1,4 +1,3 @@
-from html.parser import HTMLParseError
 import os.path
 import unittest
 import sys
@@ -33,8 +32,6 @@ class _TestCase(unittest.TestCase):
 
             msg = "%r != %r for case %s" % (found, self.expected, self.case)
             self.assertEqual(found, self.expected, msg)
-        except HTMLParseError:
-            self.assertTrue(self.expected == 'None', (self.case, self.expected))
         else:
             self.assertTrue(self.expected == 'EOF', (self.case, self.expected))
 
diff --git a/openid/test/test_rpverify.py b/openid/test/test_rpverify.py
index 75829120d9ac802d87484310eabb917125fd3e98..bd877ab2d60e939c43bd79ef5d896ae8968ba871 100644
--- a/openid/test/test_rpverify.py
+++ b/openid/test/test_rpverify.py
@@ -75,6 +75,13 @@ class TestExtractReturnToURLs(unittest.TestCase):
     def test_badXML(self):
         self.failUnlessDiscoveryFailure('>')
 
+    def test_failure(self):
+        try:
+            self.failUnlessDiscoveryFailure('')
+        except Exception as e:
+            print(e)
+            raise
+
     def test_noEntries(self):
         self.failUnlessXRDSHasReturnURLs(b'''\
 <?xml version="1.0" encoding="UTF-8"?>
diff --git a/openid/yadis/etxrd.py b/openid/yadis/etxrd.py
index ee0c5dd67aa682cc77c6e83833c44a7330e9e4d2..b42e2acf5c780452569534fe1a13e08efac08693 100644
--- a/openid/yadis/etxrd.py
+++ b/openid/yadis/etxrd.py
@@ -25,21 +25,10 @@ import functools
 from datetime import datetime
 from time import strptime
 
-from openid.oidutil import importElementTree
-ElementTree = importElementTree()
+from openid.oidutil import importElementTree, importSafeElementTree
 
-# the different elementtree modules don't have a common exception
-# model. We just want to be able to catch the exceptions that signify
-# malformed XML data and wrap them, so that the other library code
-# doesn't have to know which XML library we're using.
-try:
-    # Make the parser raise an exception so we can sniff out the type
-    # of exceptions
-    ElementTree.XML('> purposely malformed XML <')
-except (SystemExit, MemoryError, AssertionError, ImportError):
-    raise
-except:
-    XMLError = sys.exc_info()[0]
+ElementTree = importElementTree()
+SafeElementTree = importSafeElementTree()
 
 from openid.yadis import xri
 
@@ -66,8 +55,10 @@ def parseXRDS(text):
         not contain an XRDS.
     """
     try:
-        element = ElementTree.XML(text)
-    except XMLError as why:
+        element = SafeElementTree.XML(text)
+    except (SystemExit, MemoryError, AssertionError, ImportError):
+        raise
+    except Exception as why:
         exc = XRDSError('Error parsing document as XML')
         exc.reason = why
         raise exc
@@ -187,8 +178,7 @@ def getCanonicalID(iname, xrd_tree):
     childID = canonicalID.lower()
 
     for xrd in xrd_list[1:]:
-        # XXX: can't use rsplit until we require python >= 2.4.
-        parent_sought = childID[:childID.rindex('!')]
+        parent_sought = childID.rsplit("!", 1)[0]
         parent = xri.XRI(xrd.findtext(canonicalID_tag))
         if parent_sought != parent.lower():
             raise XRDSFraud("%r can not come from %s" % (childID, parent))
diff --git a/openid/yadis/parsehtml.py b/openid/yadis/parsehtml.py
index 42cd91e89450239cf8c61bdb1f8a062a56373e4f..c78711196c8ad2d56af07d1f12b1b9d65b078d07 100644
--- a/openid/yadis/parsehtml.py
+++ b/openid/yadis/parsehtml.py
@@ -1,8 +1,9 @@
 __all__ = ['findHTMLMeta', 'MetaNotFound']
 
-from html.parser import HTMLParser, HTMLParseError
+from html.parser import HTMLParser
 import html.entities
 import re
+import sys
 
 from openid.yadis.constants import YADIS_HEADER_NAME
 
@@ -96,7 +97,13 @@ class YadisHTMLParser(HTMLParser):
     TERMINATED = 4
 
     def __init__(self):
-        super(YadisHTMLParser, self).__init__(strict=False)
+        if (sys.version_info.minor <= 2):
+            # Python 3.2 and below actually require the `strict` argument
+            # to `html.parser.HTMLParser` -- otherwise it's deprecated and
+            # we don't want to pass it
+            super(YadisHTMLParser, self).__init__(strict=False)
+        else:
+            super(YadisHTMLParser, self).__init__()
         self.phase = self.TOP
 
     def _terminate(self):
@@ -187,10 +194,6 @@ def findHTMLMeta(stream):
         chunks.append(chunk)
         try:
             parser.feed(chunk)
-        except HTMLParseError as why:
-            # HTML parse error, so bail
-            chunks.append(stream.read())
-            break
         except ParseDone as why:
             uri = why.args[0]
             if uri is None:
diff --git a/pylintrc b/pylintrc
deleted file mode 100644
index fb36e4c06d86fd80817e6c2cab7b6b37bdbec63f..0000000000000000000000000000000000000000
--- a/pylintrc
+++ /dev/null
@@ -1,40 +0,0 @@
-[REPORTS]
-
-include-ids=y
-
-[BASIC]
-
-# Required attributes for module, separated by a comma
-required-attributes=__all__
-
-# Regular expression which should only match functions or classes name which do
-# not require a docstring
-no-docstring-rgx=__.*__
-
-# Regular expression which should only match correct module names
-module-rgx=[a-z_][a-z0-9_]*$
-
-# Regular expression which should only match correct module level names
-const-rgx=(([a-z_][a-z0-9_]{3,30})|(__.*__)|([A-Z_][A-Z0-9_]{3,30}))$
-
-# Regular expression which should only match correct class names
-class-rgx=[A-Z_][a-zA-Z0-9]+$
-
-# Regular expression which should only match correct function names
-function-rgx=[a-z_][A-Za-z0-9_]{2,30}$
-
-# Regular expression which should only match correct method names
-method-rgx=[a-z_][A-Za-z0-9_]{2,30}$
-
-# Regular expression which should only match correct list comprehension /
-# generator expression variable names
-inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
-
-# Good variable names which should always be accepted, separated by a comma
-good-names=i,j,k,ex,Run,_
-
-# Bad variable names which should always be refused, separated by a comma
-bad-names=foo,bar,baz,toto,tutu,tata
-
-# List of builtins function names that should not be used, separated by a comma
-bad-functions=input
diff --git a/python3_openid.egg-info/PKG-INFO b/python3_openid.egg-info/PKG-INFO
new file mode 100644
index 0000000000000000000000000000000000000000..e7a66b80723a0bf4745cad7baf86a9820d148fd0
--- /dev/null
+++ b/python3_openid.egg-info/PKG-INFO
@@ -0,0 +1,26 @@
+Metadata-Version: 1.1
+Name: python3-openid
+Version: 3.0.9
+Summary: OpenID support for modern servers and consumers.
+Home-page: http://github.com/necaris/python3-openid
+Author: Rami Chowdhury
+Author-email: rami.chowdhury@gmail.com
+License: UNKNOWN
+Download-URL: http://github.com/necaris/python3-openid/tarball/v3.0.9
+Description: This is a set of Python packages to support use of
+        the OpenID decentralized identity system in your application, update to Python
+        3.  Want to enable single sign-on for your web site?  Use the openid.consumer
+        package.  Want to run your own OpenID server? Check out openid.server.
+        Includes example code and support for a variety of storage back-ends.
+Platform: UNKNOWN
+Classifier: Development Status :: 5 - Production/Stable
+Classifier: Environment :: Web Environment
+Classifier: Intended Audience :: Developers
+Classifier: License :: OSI Approved :: Apache Software License
+Classifier: Operating System :: POSIX
+Classifier: Programming Language :: Python
+Classifier: Programming Language :: Python :: 3
+Classifier: Topic :: Internet :: WWW/HTTP
+Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content :: CGI Tools/Libraries
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
+Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
diff --git a/python3_openid.egg-info/SOURCES.txt b/python3_openid.egg-info/SOURCES.txt
new file mode 100644
index 0000000000000000000000000000000000000000..db6fea429213a05a19ebda1c916505f2af8fa72b
--- /dev/null
+++ b/python3_openid.egg-info/SOURCES.txt
@@ -0,0 +1,176 @@
+LICENSE
+MANIFEST.in
+NEWS.md
+background-associations.txt
+setup.cfg
+setup.py
+admin/builddiscover.py
+admin/fixperms
+admin/gettlds.py
+admin/runtests
+admin/setversion
+contrib/associate
+contrib/openid-parse
+contrib/upgrade-store-1.1-to-2.0
+contrib/upgrade-store-1.1-to-2.0~
+examples/__init__.py
+examples/consumer.py
+examples/discover
+examples/server.py
+examples/djopenid/__init__.py
+examples/djopenid/manage.py
+examples/djopenid/settings.py
+examples/djopenid/urls.py
+examples/djopenid/util.py
+examples/djopenid/views.py
+examples/djopenid/consumer/__init__.py
+examples/djopenid/consumer/models.py
+examples/djopenid/consumer/urls.py
+examples/djopenid/consumer/views.py
+examples/djopenid/server/__init__.py
+examples/djopenid/server/models.py
+examples/djopenid/server/tests.py
+examples/djopenid/server/urls.py
+examples/djopenid/server/views.py
+examples/djopenid/templates/index.html
+examples/djopenid/templates/xrds.xml
+examples/djopenid/templates/consumer/index.html
+examples/djopenid/templates/consumer/request_form.html
+examples/djopenid/templates/server/endpoint.html
+examples/djopenid/templates/server/idPage.html
+examples/djopenid/templates/server/index.html
+examples/djopenid/templates/server/pape_request_info.html
+examples/djopenid/templates/server/trust.html
+openid/__init__.py
+openid/association.py
+openid/codecutil.py
+openid/cryptutil.py
+openid/dh.py
+openid/extension.py
+openid/fetchers.py
+openid/kvform.py
+openid/message.py
+openid/oidutil.py
+openid/sreg.py
+openid/urinorm.py
+openid/consumer/__init__.py
+openid/consumer/consumer.py
+openid/consumer/discover.py
+openid/consumer/html_parse.py
+openid/extensions/__init__.py
+openid/extensions/ax.py
+openid/extensions/sreg.py
+openid/extensions/draft/__init__.py
+openid/extensions/draft/pape2.py
+openid/extensions/draft/pape5.py
+openid/server/__init__.py
+openid/server/server.py
+openid/server/trustroot.py
+openid/store/__init__.py
+openid/store/filestore.py
+openid/store/interface.py
+openid/store/memstore.py
+openid/store/nonce.py
+openid/store/sqlstore.py
+openid/test/__init__.py
+openid/test/cryptutil.py
+openid/test/datadriven.py
+openid/test/dh.py
+openid/test/dhpriv
+openid/test/discoverdata.py
+openid/test/kvform.py
+openid/test/linkparse.py
+openid/test/linkparse.txt
+openid/test/n2b64
+openid/test/oidutil.py
+openid/test/storetest.py
+openid/test/support.py
+openid/test/test_accept.py
+openid/test/test_association.py
+openid/test/test_association_response.py
+openid/test/test_auth_request.py
+openid/test/test_ax.py
+openid/test/test_codecutil.py
+openid/test/test_consumer.py
+openid/test/test_discover.py
+openid/test/test_etxrd.py
+openid/test/test_examples.py
+openid/test/test_extension.py
+openid/test/test_fetchers.py
+openid/test/test_htmldiscover.py
+openid/test/test_message.py
+openid/test/test_negotiation.py
+openid/test/test_nonce.py
+openid/test/test_openidyadis.py
+openid/test/test_pape.py
+openid/test/test_pape_draft2.py
+openid/test/test_pape_draft5.py
+openid/test/test_parsehtml.py
+openid/test/test_rpverify.py
+openid/test/test_server.py
+openid/test/test_services.py
+openid/test/test_sreg.py
+openid/test/test_symbol.py
+openid/test/test_urinorm.py
+openid/test/test_verifydisco.py
+openid/test/test_xri.py
+openid/test/test_xrires.py
+openid/test/test_yadis_discover.py
+openid/test/trustroot.py
+openid/test/urinorm.txt
+openid/test/data/accept.txt
+openid/test/data/example-xrds.xml
+openid/test/data/openid-1.2-consumer-sqlitestore.db
+openid/test/data/test1-discover.txt
+openid/test/data/test1-parsehtml.txt
+openid/test/data/trustroot.txt
+openid/test/data/test_discover/openid.html
+openid/test/data/test_discover/openid2.html
+openid/test/data/test_discover/openid2_xrds.xml
+openid/test/data/test_discover/openid2_xrds_no_local_id.xml
+openid/test/data/test_discover/openid_1_and_2.html
+openid/test/data/test_discover/openid_1_and_2_xrds.xml
+openid/test/data/test_discover/openid_1_and_2_xrds_bad_delegate.xml
+openid/test/data/test_discover/openid_and_yadis.html
+openid/test/data/test_discover/openid_no_delegate.html
+openid/test/data/test_discover/unicode.html
+openid/test/data/test_discover/unicode3.html
+openid/test/data/test_discover/yadis_0entries.xml
+openid/test/data/test_discover/yadis_2_bad_local_id.xml
+openid/test/data/test_discover/yadis_2entries_delegate.xml
+openid/test/data/test_discover/yadis_2entries_idp.xml
+openid/test/data/test_discover/yadis_another_delegate.xml
+openid/test/data/test_discover/yadis_idp.xml
+openid/test/data/test_discover/yadis_idp_delegate.xml
+openid/test/data/test_discover/yadis_no_delegate.xml
+openid/test/data/test_etxrd/README
+openid/test/data/test_etxrd/delegated-20060809-r1.xrds
+openid/test/data/test_etxrd/delegated-20060809-r2.xrds
+openid/test/data/test_etxrd/delegated-20060809.xrds
+openid/test/data/test_etxrd/no-xrd.xml
+openid/test/data/test_etxrd/not-xrds.xml
+openid/test/data/test_etxrd/prefixsometimes.xrds
+openid/test/data/test_etxrd/ref.xrds
+openid/test/data/test_etxrd/sometimesprefix.xrds
+openid/test/data/test_etxrd/spoof1.xrds
+openid/test/data/test_etxrd/spoof2.xrds
+openid/test/data/test_etxrd/spoof3.xrds
+openid/test/data/test_etxrd/status222.xrds
+openid/test/data/test_etxrd/subsegments.xrds
+openid/test/data/test_etxrd/valid-populated-xrds.xml
+openid/yadis/__init__.py
+openid/yadis/accept.py
+openid/yadis/constants.py
+openid/yadis/discover.py
+openid/yadis/etxrd.py
+openid/yadis/filters.py
+openid/yadis/manager.py
+openid/yadis/parsehtml.py
+openid/yadis/services.py
+openid/yadis/xri.py
+openid/yadis/xrires.py
+python3_openid.egg-info/PKG-INFO
+python3_openid.egg-info/SOURCES.txt
+python3_openid.egg-info/dependency_links.txt
+python3_openid.egg-info/requires.txt
+python3_openid.egg-info/top_level.txt
\ No newline at end of file
diff --git a/python3_openid.egg-info/dependency_links.txt b/python3_openid.egg-info/dependency_links.txt
new file mode 100644
index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc
--- /dev/null
+++ b/python3_openid.egg-info/dependency_links.txt
@@ -0,0 +1 @@
+
diff --git a/python3_openid.egg-info/requires.txt b/python3_openid.egg-info/requires.txt
new file mode 100644
index 0000000000000000000000000000000000000000..36969f2c4b81b8d563bf352a569328e8c5a75a74
--- /dev/null
+++ b/python3_openid.egg-info/requires.txt
@@ -0,0 +1 @@
+defusedxml
diff --git a/python3_openid.egg-info/top_level.txt b/python3_openid.egg-info/top_level.txt
new file mode 100644
index 0000000000000000000000000000000000000000..c2f7f6396d52c4c7297a5b2c05879bbba550ae60
--- /dev/null
+++ b/python3_openid.egg-info/top_level.txt
@@ -0,0 +1 @@
+openid
diff --git a/run_tests.sh b/run_tests.sh
deleted file mode 100755
index 4ea33c0940b38f5e4bf166e0419295b22ba9dc43..0000000000000000000000000000000000000000
--- a/run_tests.sh
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/bin/sh
-
-# naive version that doesn't run enough tests:
-# python openid/test/test*.py
-
-python3 -m unittest openid.test.test_suite
diff --git a/setup.cfg b/setup.cfg
index 5706708196be26fd3906d912e2de5e5bcc384a6d..bf52e6a907b11e9a1ae936764188ce3c82ec3e1a 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,3 +1,9 @@
 [sdist]
-force_manifest=1
-formats=gztar,zip
+force_manifest = 1
+formats = gztar,zip
+
+[egg_info]
+tag_build = 
+tag_svn_revision = 0
+tag_date = 0
+
diff --git a/setup.py b/setup.py
index 7818cff64bb17ca4adeab74fa7168f082a88e2b6..dcc219cee93babf97dea776e7d10aec45fbdbf18 100644
--- a/setup.py
+++ b/setup.py
@@ -1,7 +1,7 @@
 import sys
 import os
 
-from distutils.core import setup
+from setuptools import setup
 
 import openid
 
@@ -37,6 +37,9 @@ Includes example code and support for a variety of storage back-ends.''',
     maintainer_email='rami.chowdhury@gmail.com',
     download_url=('http://github.com/necaris/python3-openid/tarball'
                   '/v{}'.format(version)),
+    install_requires=[
+        'defusedxml',
+    ],
     classifiers=[
         "Development Status :: 5 - Production/Stable",
         "Environment :: Web Environment",
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index 0939a81bc1d586a2989b76af08bd9d1a9c30103b..0000000000000000000000000000000000000000
--- a/tox.ini
+++ /dev/null
@@ -1,14 +0,0 @@
-# Tox (http://tox.testrun.org/) is a tool for running tests
-# in multiple virtualenvs. This configuration file will run the
-# test suite on all supported python versions. To use it, "pip install tox"
-# and then run "tox" from this directory.
-
-[tox]
-envlist = py32, py33
-
-[testenv]
-commands = ./run_tests.sh
-deps =
-    Django==1.5
-    nose
-    pycrypto