From 0818e627c14042c7b7c30726df544dd973348456 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Sat, 26 Jan 2019 00:25:02 -0600
Subject: [PATCH 01/12] Fix new indicator for sync

Missing for incremental syncs
---
 resources/lib/library.py | 1 +
 1 file changed, 1 insertion(+)

diff --git a/resources/lib/library.py b/resources/lib/library.py
index 959e5e7e..a69098e4 100644
--- a/resources/lib/library.py
+++ b/resources/lib/library.py
@@ -151,6 +151,7 @@ class Library(threading.Thread):
             self.worker_notify()
 
         if self.pending_refresh:
+            window('emby_sync.bool', True)
 
             if self.total_updates > self.progress_display:
                 queue_size = self.worker_queue_size()

From 467c474f4d30dd12c8a19f1c02ff6bc43b3aeaea Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Sat, 26 Jan 2019 02:23:44 -0600
Subject: [PATCH 02/12] Fix profile masterlock

fix missing content if a master lock is set.
---
 resources/lib/helper/xmls.py | 21 +++++++++++++--------
 1 file changed, 13 insertions(+), 8 deletions(-)

diff --git a/resources/lib/helper/xmls.py b/resources/lib/helper/xmls.py
index a1e98a7b..5ca11099 100644
--- a/resources/lib/helper/xmls.py
+++ b/resources/lib/helper/xmls.py
@@ -36,20 +36,25 @@ def sources():
         etree.SubElement(files, 'default', attrib={'pathversion': "1"})
 
     video = xml.find('video')
-    count = 2
+    count_http = 1
+    count_smb = 1
 
     for source in xml.findall('.//path'):
         if source.text == 'smb://':
-            count -= 1
+            count_smb -= 1
+        elif source.text == 'http://':
+            count_http -= 1
 
-        if count == 0:
+        if not count_http and not count_smb:
             break
     else:
-        for i in range(0, count):
-            source = etree.SubElement(video, 'source')
-            etree.SubElement(source, 'name').text = "Emby"
-            etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = "smb://"
-            etree.SubElement(source, 'allowsharing').text = "true"
+        for protocol in ('smb://', 'http://'):
+            if (protocol == 'smb://' and count_smb > 0) or (protocol == 'http://' and count_http > 0):
+
+                source = etree.SubElement(video, 'source')
+                etree.SubElement(source, 'name').text = "Emby"
+                etree.SubElement(source, 'path', attrib={'pathversion': "1"}).text = protocol
+                etree.SubElement(source, 'allowsharing').text = "true"
 
     try:
         files = xml.find('files')

From e5aea3fbdc2cdbad8e98dbaf4bdac2fad9a57dbf Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Sun, 27 Jan 2019 09:17:40 -0600
Subject: [PATCH 03/12] Allow homevideos under videos

Pictures won't display correctly, but restore previous behavior
---
 resources/lib/entrypoint/default.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/resources/lib/entrypoint/default.py b/resources/lib/entrypoint/default.py
index eea1511b..3c46a911 100644
--- a/resources/lib/entrypoint/default.py
+++ b/resources/lib/entrypoint/default.py
@@ -166,7 +166,7 @@ def listing():
         if path:
             if xbmc.getCondVisibility('Window.IsActive(Pictures)') and node in ('photos', 'homevideos'):
                 directory(label, path, artwork=artwork)
-            elif xbmc.getCondVisibility('Window.IsActive(Videos)') and node not in ('photos', 'homevideos', 'music', 'audiobooks'):
+            elif xbmc.getCondVisibility('Window.IsActive(Videos)') and node not in ('photos', 'music', 'audiobooks'):
                 directory(label, path, artwork=artwork, context=context)
             elif xbmc.getCondVisibility('Window.IsActive(Music)') and node in ('music'):
                 directory(label, path, artwork=artwork, context=context)

From 3ea6890c3463a987d6ad9fed46f743e45b1843a5 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Sun, 27 Jan 2019 14:49:35 -0600
Subject: [PATCH 04/12] Filter webservice requests

Only proceed if the id is a number.
---
 resources/lib/webservice.py | 8 ++++++++
 1 file changed, 8 insertions(+)

diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py
index 5ef21824..34826af0 100644
--- a/resources/lib/webservice.py
+++ b/resources/lib/webservice.py
@@ -164,6 +164,10 @@ class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
         '''
         try:
             params = self.get_params()
+
+            if not params.get('Id').isdigit():
+                raise IndexError("Incorrect Id format: %s" % params.get('Id'))
+            
             LOG.info("Webservice called with params: %s", params)
 
             path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s"
@@ -176,6 +180,10 @@ class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
 
             if not headers_only:
                 self.wfile.write(path)
+        except IndexError as error:
+
+            LOG.error(error)
+            self.send_error(403)
 
         except Exception as error:
 

From b2bc90cb06efb9f6c2996427be1840510ca366e5 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Wed, 30 Jan 2019 06:42:06 -0600
Subject: [PATCH 05/12] Move libraries import

---
 context.py                                    |    2 +
 context_play.py                               |    2 +
 default.py                                    |    2 +
 .../dateutil/test => libraries}/__init__.py   |    0
 .../libraries => libraries}/dateutil/LICENSE  |    0
 .../lib/libraries => libraries}/dateutil/NEWS |    0
 .../dateutil/README.rst                       |    0
 .../dateutil/__init__.py                      |    2 +-
 .../dateutil/_common.py                       |    0
 .../dateutil/easter.py                        |    0
 .../dateutil/parser/__init__.py               |   10 +-
 .../dateutil/parser/_parser.py                |    8 +-
 .../dateutil/parser/isoparser.py              |    4 +-
 .../dateutil/relativedelta.py                 |    0
 .../libraries => libraries}/dateutil/rrule.py |    0
 .../dateutil/test}/__init__.py                |    0
 .../dateutil/test/_common.py                  |    0
 .../test/property/test_isoparse_prop.py       |    0
 .../test/property/test_parser_prop.py         |    0
 .../dateutil/test/test_easter.py              |    0
 .../dateutil/test/test_import_star.py         |    0
 .../dateutil/test/test_imports.py             |    0
 .../dateutil/test/test_internals.py           |    0
 .../dateutil/test/test_isoparser.py           |    0
 .../dateutil/test/test_parser.py              |    0
 .../dateutil/test/test_relativedelta.py       |    0
 .../dateutil/test/test_rrule.py               |    0
 .../dateutil/test/test_tz.py                  |    0
 .../dateutil/test/test_utils.py               |    0
 .../dateutil/tz/__init__.py                   |    0
 .../dateutil/tz/_common.py                    |    2 +-
 .../dateutil/tz/_factories.py                 |    0
 .../libraries => libraries}/dateutil/tz/tz.py |   18 +-
 .../dateutil/tz/win.py                        |    0
 .../libraries => libraries}/dateutil/tzwin.py |    0
 .../libraries => libraries}/dateutil/utils.py |    0
 .../dateutil/zoneinfo/__init__.py             |    0
 .../zoneinfo/dateutil-zoneinfo.tar.gz         |  Bin
 .../dateutil/zoneinfo/rebuild.py              |    0
 .../requests/__init__.py                      |    0
 .../requests/adapters.py                      |    0
 .../libraries => libraries}/requests/api.py   |    0
 .../libraries => libraries}/requests/auth.py  |    0
 .../requests/cacert.pem                       |    0
 .../libraries => libraries}/requests/certs.py |    0
 .../requests/compat.py                        |    0
 .../requests/cookies.py                       |    0
 .../requests/exceptions.py                    |    0
 .../libraries => libraries}/requests/hooks.py |    0
 .../requests/models.py                        |    0
 .../requests/packages/README.rst              |    0
 .../requests/packages/__init__.py             |    0
 .../requests/packages/chardet/__init__.py     |    0
 .../requests/packages/chardet/big5freq.py     |    0
 .../requests/packages/chardet/big5prober.py   |    0
 .../requests/packages/chardet/chardetect.py   |    0
 .../packages/chardet/chardistribution.py      |    0
 .../packages/chardet/charsetgroupprober.py    |    0
 .../packages/chardet/charsetprober.py         |    0
 .../packages/chardet/codingstatemachine.py    |    0
 .../requests/packages/chardet/compat.py       |    0
 .../requests/packages/chardet/constants.py    |    0
 .../requests/packages/chardet/cp949prober.py  |    0
 .../requests/packages/chardet/escprober.py    |    0
 .../requests/packages/chardet/escsm.py        |    0
 .../requests/packages/chardet/eucjpprober.py  |    0
 .../requests/packages/chardet/euckrfreq.py    |    0
 .../requests/packages/chardet/euckrprober.py  |    0
 .../requests/packages/chardet/euctwfreq.py    |    0
 .../requests/packages/chardet/euctwprober.py  |    0
 .../requests/packages/chardet/gb2312freq.py   |    0
 .../requests/packages/chardet/gb2312prober.py |    0
 .../requests/packages/chardet/hebrewprober.py |    0
 .../requests/packages/chardet/jisfreq.py      |    0
 .../requests/packages/chardet/jpcntx.py       |    0
 .../packages/chardet/langbulgarianmodel.py    |    0
 .../packages/chardet/langcyrillicmodel.py     |    0
 .../packages/chardet/langgreekmodel.py        |    0
 .../packages/chardet/langhebrewmodel.py       |    0
 .../packages/chardet/langhungarianmodel.py    |    0
 .../packages/chardet/langthaimodel.py         |    0
 .../requests/packages/chardet/latin1prober.py |    0
 .../packages/chardet/mbcharsetprober.py       |    0
 .../packages/chardet/mbcsgroupprober.py       |    0
 .../requests/packages/chardet/mbcssm.py       |    0
 .../packages/chardet/sbcharsetprober.py       |    0
 .../packages/chardet/sbcsgroupprober.py       |    0
 .../requests/packages/chardet/sjisprober.py   |    0
 .../packages/chardet/universaldetector.py     |    0
 .../requests/packages/chardet/utf8prober.py   |    0
 .../requests/packages/urllib3/__init__.py     |    0
 .../requests/packages/urllib3/_collections.py |    0
 .../requests/packages/urllib3/connection.py   |    0
 .../packages/urllib3/connectionpool.py        |    0
 .../packages/urllib3/contrib/__init__.py      |    0
 .../packages/urllib3/contrib/appengine.py     |    0
 .../packages/urllib3/contrib/ntlmpool.py      |    0
 .../packages/urllib3/contrib/pyopenssl.py     |    0
 .../requests/packages/urllib3/exceptions.py   |    0
 .../requests/packages/urllib3/fields.py       |    0
 .../requests/packages/urllib3/filepost.py     |    0
 .../packages/urllib3/packages/__init__.py     |    0
 .../packages/urllib3/packages/ordered_dict.py |    0
 .../requests/packages/urllib3/packages/six.py |    0
 .../packages/ssl_match_hostname/__init__.py   |    0
 .../ssl_match_hostname/_implementation.py     |    0
 .../requests/packages/urllib3/poolmanager.py  |    0
 .../requests/packages/urllib3/request.py      |    0
 .../requests/packages/urllib3/response.py     |    0
 .../packages/urllib3/util/__init__.py         |    0
 .../packages/urllib3/util/connection.py       |    0
 .../requests/packages/urllib3/util/request.py |    0
 .../packages/urllib3/util/response.py         |    0
 .../requests/packages/urllib3/util/retry.py   |    0
 .../requests/packages/urllib3/util/ssl_.py    |    0
 .../requests/packages/urllib3/util/timeout.py |    0
 .../requests/packages/urllib3/util/url.py     |    0
 .../requests/sessions.py                      |    0
 .../requests/status_codes.py                  |    0
 .../requests/structures.py                    |    0
 .../libraries => libraries}/requests/utils.py |    0
 .../libraries/dateutil => libraries}/six.py   |    0
 resources/lib/downloader.py                   |    2 +-
 resources/lib/emby/core/http.py               |    2 +-
 resources/lib/entrypoint/service.py           |    2 +-
 resources/lib/helper/playutils.py             |    2 +-
 resources/lib/helper/utils.py                 |    3 +-
 resources/lib/libraries/__init__.py           |    2 -
 resources/lib/libraries/mutagen/__init__.py   |   43 -
 .../__pycache__/__init__.cpython-35.pyc       |  Bin 914 -> 0 bytes
 .../__pycache__/_compat.cpython-35.pyc        |  Bin 2754 -> 0 bytes
 .../__pycache__/_constants.cpython-35.pyc     |  Bin 3099 -> 0 bytes
 .../mutagen/__pycache__/_file.cpython-35.pyc  |  Bin 7763 -> 0 bytes
 .../__pycache__/_mp3util.cpython-35.pyc       |  Bin 8626 -> 0 bytes
 .../mutagen/__pycache__/_tags.cpython-35.pyc  |  Bin 2970 -> 0 bytes
 .../__pycache__/_toolsutil.cpython-35.pyc     |  Bin 6102 -> 0 bytes
 .../mutagen/__pycache__/_util.cpython-35.pyc  |  Bin 17420 -> 0 bytes
 .../__pycache__/_vorbis.cpython-35.pyc        |  Bin 10304 -> 0 bytes
 .../mutagen/__pycache__/aac.cpython-35.pyc    |  Bin 10050 -> 0 bytes
 .../mutagen/__pycache__/aiff.cpython-35.pyc   |  Bin 10216 -> 0 bytes
 .../mutagen/__pycache__/apev2.cpython-35.pyc  |  Bin 20902 -> 0 bytes
 .../__pycache__/easyid3.cpython-35.pyc        |  Bin 17989 -> 0 bytes
 .../__pycache__/easymp4.cpython-35.pyc        |  Bin 10727 -> 0 bytes
 .../mutagen/__pycache__/flac.cpython-35.pyc   |  Bin 28214 -> 0 bytes
 .../mutagen/__pycache__/m4a.cpython-35.pyc    |  Bin 3561 -> 0 bytes
 .../__pycache__/monkeysaudio.cpython-35.pyc   |  Bin 2904 -> 0 bytes
 .../mutagen/__pycache__/mp3.cpython-35.pyc    |  Bin 9514 -> 0 bytes
 .../__pycache__/musepack.cpython-35.pyc       |  Bin 8017 -> 0 bytes
 .../mutagen/__pycache__/ogg.cpython-35.pyc    |  Bin 16965 -> 0 bytes
 .../__pycache__/oggflac.cpython-35.pyc        |  Bin 4862 -> 0 bytes
 .../__pycache__/oggopus.cpython-35.pyc        |  Bin 4763 -> 0 bytes
 .../__pycache__/oggspeex.cpython-35.pyc       |  Bin 4600 -> 0 bytes
 .../__pycache__/oggtheora.cpython-35.pyc      |  Bin 4781 -> 0 bytes
 .../__pycache__/oggvorbis.cpython-35.pyc      |  Bin 4602 -> 0 bytes
 .../__pycache__/optimfrog.cpython-35.pyc      |  Bin 2545 -> 0 bytes
 .../__pycache__/trueaudio.cpython-35.pyc      |  Bin 2924 -> 0 bytes
 .../__pycache__/wavpack.cpython-35.pyc        |  Bin 3738 -> 0 bytes
 resources/lib/libraries/mutagen/_compat.py    |   86 -
 resources/lib/libraries/mutagen/_constants.py |  199 --
 resources/lib/libraries/mutagen/_file.py      |  253 ---
 resources/lib/libraries/mutagen/_mp3util.py   |  420 ----
 resources/lib/libraries/mutagen/_tags.py      |  101 -
 resources/lib/libraries/mutagen/_toolsutil.py |  231 --
 resources/lib/libraries/mutagen/_util.py      |  550 -----
 resources/lib/libraries/mutagen/_vorbis.py    |  330 ---
 resources/lib/libraries/mutagen/aac.py        |  410 ----
 resources/lib/libraries/mutagen/aiff.py       |  357 ---
 resources/lib/libraries/mutagen/apev2.py      |  710 ------
 .../lib/libraries/mutagen/asf/__init__.py     |  319 ---
 .../asf/__pycache__/__init__.cpython-35.pyc   |  Bin 8567 -> 0 bytes
 .../asf/__pycache__/_attrs.cpython-35.pyc     |  Bin 15131 -> 0 bytes
 .../asf/__pycache__/_objects.cpython-35.pyc   |  Bin 15903 -> 0 bytes
 .../asf/__pycache__/_util.cpython-35.pyc      |  Bin 10603 -> 0 bytes
 resources/lib/libraries/mutagen/asf/_attrs.py |  438 ----
 .../lib/libraries/mutagen/asf/_objects.py     |  437 ----
 resources/lib/libraries/mutagen/asf/_util.py  |  315 ---
 resources/lib/libraries/mutagen/easyid3.py    |  534 -----
 resources/lib/libraries/mutagen/easymp4.py    |  285 ---
 resources/lib/libraries/mutagen/flac.py       |  876 --------
 .../lib/libraries/mutagen/id3/__init__.py     | 1093 ----------
 .../id3/__pycache__/__init__.cpython-35.pyc   |  Bin 27785 -> 0 bytes
 .../id3/__pycache__/_frames.cpython-35.pyc    |  Bin 64560 -> 0 bytes
 .../id3/__pycache__/_specs.cpython-35.pyc     |  Bin 22816 -> 0 bytes
 .../id3/__pycache__/_util.cpython-35.pyc      |  Bin 4933 -> 0 bytes
 .../lib/libraries/mutagen/id3/_frames.py      | 1925 -----------------
 resources/lib/libraries/mutagen/id3/_specs.py |  635 ------
 resources/lib/libraries/mutagen/id3/_util.py  |  167 --
 resources/lib/libraries/mutagen/m4a.py        |  101 -
 .../lib/libraries/mutagen/monkeysaudio.py     |   86 -
 resources/lib/libraries/mutagen/mp3.py        |  362 ----
 .../lib/libraries/mutagen/mp4/__init__.py     | 1010 ---------
 .../mp4/__pycache__/__init__.cpython-35.pyc   |  Bin 31145 -> 0 bytes
 .../mp4/__pycache__/_as_entry.cpython-35.pyc  |  Bin 14269 -> 0 bytes
 .../mp4/__pycache__/_atom.cpython-35.pyc      |  Bin 6248 -> 0 bytes
 .../mp4/__pycache__/_util.cpython-35.pyc      |  Bin 590 -> 0 bytes
 .../lib/libraries/mutagen/mp4/_as_entry.py    |  542 -----
 resources/lib/libraries/mutagen/mp4/_atom.py  |  194 --
 resources/lib/libraries/mutagen/mp4/_util.py  |   21 -
 resources/lib/libraries/mutagen/musepack.py   |  270 ---
 resources/lib/libraries/mutagen/ogg.py        |  548 -----
 resources/lib/libraries/mutagen/oggflac.py    |  161 --
 resources/lib/libraries/mutagen/oggopus.py    |  158 --
 resources/lib/libraries/mutagen/oggspeex.py   |  154 --
 resources/lib/libraries/mutagen/oggtheora.py  |  148 --
 resources/lib/libraries/mutagen/oggvorbis.py  |  159 --
 resources/lib/libraries/mutagen/optimfrog.py  |   74 -
 resources/lib/libraries/mutagen/trueaudio.py  |   84 -
 resources/lib/libraries/mutagen/wavpack.py    |  125 --
 service.py                                    |    3 +
 209 files changed, 36 insertions(+), 14941 deletions(-)
 rename {resources/lib/libraries/dateutil/test => libraries}/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/LICENSE (100%)
 rename {resources/lib/libraries => libraries}/dateutil/NEWS (100%)
 rename {resources/lib/libraries => libraries}/dateutil/README.rst (100%)
 rename {resources/lib/libraries => libraries}/dateutil/__init__.py (78%)
 rename {resources/lib/libraries => libraries}/dateutil/_common.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/easter.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/parser/__init__.py (87%)
 rename {resources/lib/libraries => libraries}/dateutil/parser/_parser.py (99%)
 rename {resources/lib/libraries => libraries}/dateutil/parser/isoparser.py (99%)
 rename {resources/lib/libraries => libraries}/dateutil/relativedelta.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/rrule.py (100%)
 rename {resources/lib/libraries/requests/packages/urllib3/contrib => libraries/dateutil/test}/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/_common.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/property/test_isoparse_prop.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/property/test_parser_prop.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_easter.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_import_star.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_imports.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_internals.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_isoparser.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_parser.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_relativedelta.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_rrule.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_tz.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/test/test_utils.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/tz/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/tz/_common.py (99%)
 rename {resources/lib/libraries => libraries}/dateutil/tz/_factories.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/tz/tz.py (99%)
 rename {resources/lib/libraries => libraries}/dateutil/tz/win.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/tzwin.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/utils.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/zoneinfo/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz (100%)
 rename {resources/lib/libraries => libraries}/dateutil/zoneinfo/rebuild.py (100%)
 rename {resources/lib/libraries => libraries}/requests/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/adapters.py (100%)
 rename {resources/lib/libraries => libraries}/requests/api.py (100%)
 rename {resources/lib/libraries => libraries}/requests/auth.py (100%)
 rename {resources/lib/libraries => libraries}/requests/cacert.pem (100%)
 rename {resources/lib/libraries => libraries}/requests/certs.py (100%)
 rename {resources/lib/libraries => libraries}/requests/compat.py (100%)
 rename {resources/lib/libraries => libraries}/requests/cookies.py (100%)
 rename {resources/lib/libraries => libraries}/requests/exceptions.py (100%)
 rename {resources/lib/libraries => libraries}/requests/hooks.py (100%)
 rename {resources/lib/libraries => libraries}/requests/models.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/README.rst (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/big5freq.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/big5prober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/chardetect.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/chardistribution.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/charsetgroupprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/charsetprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/codingstatemachine.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/compat.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/constants.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/cp949prober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/escprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/escsm.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/eucjpprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/euckrfreq.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/euckrprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/euctwfreq.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/euctwprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/gb2312freq.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/gb2312prober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/hebrewprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/jisfreq.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/jpcntx.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/langbulgarianmodel.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/langcyrillicmodel.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/langgreekmodel.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/langhebrewmodel.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/langhungarianmodel.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/langthaimodel.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/latin1prober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/mbcharsetprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/mbcsgroupprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/mbcssm.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/sbcharsetprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/sbcsgroupprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/sjisprober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/universaldetector.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/chardet/utf8prober.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/_collections.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/connection.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/connectionpool.py (100%)
 create mode 100644 libraries/requests/packages/urllib3/contrib/__init__.py
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/contrib/appengine.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/contrib/ntlmpool.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/contrib/pyopenssl.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/exceptions.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/fields.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/filepost.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/ordered_dict.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/six.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/poolmanager.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/request.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/response.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/__init__.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/connection.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/request.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/response.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/retry.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/ssl_.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/timeout.py (100%)
 rename {resources/lib/libraries => libraries}/requests/packages/urllib3/util/url.py (100%)
 rename {resources/lib/libraries => libraries}/requests/sessions.py (100%)
 rename {resources/lib/libraries => libraries}/requests/status_codes.py (100%)
 rename {resources/lib/libraries => libraries}/requests/structures.py (100%)
 rename {resources/lib/libraries => libraries}/requests/utils.py (100%)
 rename {resources/lib/libraries/dateutil => libraries}/six.py (100%)
 delete mode 100644 resources/lib/libraries/__init__.py
 delete mode 100644 resources/lib/libraries/mutagen/__init__.py
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/_compat.py
 delete mode 100644 resources/lib/libraries/mutagen/_constants.py
 delete mode 100644 resources/lib/libraries/mutagen/_file.py
 delete mode 100644 resources/lib/libraries/mutagen/_mp3util.py
 delete mode 100644 resources/lib/libraries/mutagen/_tags.py
 delete mode 100644 resources/lib/libraries/mutagen/_toolsutil.py
 delete mode 100644 resources/lib/libraries/mutagen/_util.py
 delete mode 100644 resources/lib/libraries/mutagen/_vorbis.py
 delete mode 100644 resources/lib/libraries/mutagen/aac.py
 delete mode 100644 resources/lib/libraries/mutagen/aiff.py
 delete mode 100644 resources/lib/libraries/mutagen/apev2.py
 delete mode 100644 resources/lib/libraries/mutagen/asf/__init__.py
 delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/asf/_attrs.py
 delete mode 100644 resources/lib/libraries/mutagen/asf/_objects.py
 delete mode 100644 resources/lib/libraries/mutagen/asf/_util.py
 delete mode 100644 resources/lib/libraries/mutagen/easyid3.py
 delete mode 100644 resources/lib/libraries/mutagen/easymp4.py
 delete mode 100644 resources/lib/libraries/mutagen/flac.py
 delete mode 100644 resources/lib/libraries/mutagen/id3/__init__.py
 delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/id3/_frames.py
 delete mode 100644 resources/lib/libraries/mutagen/id3/_specs.py
 delete mode 100644 resources/lib/libraries/mutagen/id3/_util.py
 delete mode 100644 resources/lib/libraries/mutagen/m4a.py
 delete mode 100644 resources/lib/libraries/mutagen/monkeysaudio.py
 delete mode 100644 resources/lib/libraries/mutagen/mp3.py
 delete mode 100644 resources/lib/libraries/mutagen/mp4/__init__.py
 delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc
 delete mode 100644 resources/lib/libraries/mutagen/mp4/_as_entry.py
 delete mode 100644 resources/lib/libraries/mutagen/mp4/_atom.py
 delete mode 100644 resources/lib/libraries/mutagen/mp4/_util.py
 delete mode 100644 resources/lib/libraries/mutagen/musepack.py
 delete mode 100644 resources/lib/libraries/mutagen/ogg.py
 delete mode 100644 resources/lib/libraries/mutagen/oggflac.py
 delete mode 100644 resources/lib/libraries/mutagen/oggopus.py
 delete mode 100644 resources/lib/libraries/mutagen/oggspeex.py
 delete mode 100644 resources/lib/libraries/mutagen/oggtheora.py
 delete mode 100644 resources/lib/libraries/mutagen/oggvorbis.py
 delete mode 100644 resources/lib/libraries/mutagen/optimfrog.py
 delete mode 100644 resources/lib/libraries/mutagen/trueaudio.py
 delete mode 100644 resources/lib/libraries/mutagen/wavpack.py

diff --git a/context.py b/context.py
index 15256491..b2143b01 100644
--- a/context.py
+++ b/context.py
@@ -13,11 +13,13 @@ import xbmcaddon
 
 __addon__ = xbmcaddon.Addon(id='plugin.video.emby')
 __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8')
+__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'librarie')).decode('utf-8')
 __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8')
 __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8')
 
 sys.path.insert(0, __cache__)
 sys.path.insert(0, __pcache__)
+sys.path.insert(0, __libraries__)
 sys.path.append(__base__)
 
 #################################################################################################
diff --git a/context_play.py b/context_play.py
index 660fb468..58ee93fb 100644
--- a/context_play.py
+++ b/context_play.py
@@ -13,11 +13,13 @@ import xbmcaddon
 
 __addon__ = xbmcaddon.Addon(id='plugin.video.emby')
 __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8')
+__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8')
 __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8')
 __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8')
 
 sys.path.insert(0, __cache__)
 sys.path.insert(0, __pcache__)
+sys.path.insert(0, __libraries__)
 sys.path.append(__base__)
 
 #################################################################################################
diff --git a/default.py b/default.py
index 0305ee81..6f1b05a0 100644
--- a/default.py
+++ b/default.py
@@ -13,11 +13,13 @@ import xbmcaddon
 
 __addon__ = xbmcaddon.Addon(id='plugin.video.emby')
 __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8')
+__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8')
 __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8')
 __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8')
 
 sys.path.insert(0, __cache__)
 sys.path.insert(0, __pcache__)
+sys.path.insert(0, __libraries__)
 sys.path.append(__base__)
 
 #################################################################################################
diff --git a/resources/lib/libraries/dateutil/test/__init__.py b/libraries/__init__.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/__init__.py
rename to libraries/__init__.py
diff --git a/resources/lib/libraries/dateutil/LICENSE b/libraries/dateutil/LICENSE
similarity index 100%
rename from resources/lib/libraries/dateutil/LICENSE
rename to libraries/dateutil/LICENSE
diff --git a/resources/lib/libraries/dateutil/NEWS b/libraries/dateutil/NEWS
similarity index 100%
rename from resources/lib/libraries/dateutil/NEWS
rename to libraries/dateutil/NEWS
diff --git a/resources/lib/libraries/dateutil/README.rst b/libraries/dateutil/README.rst
similarity index 100%
rename from resources/lib/libraries/dateutil/README.rst
rename to libraries/dateutil/README.rst
diff --git a/resources/lib/libraries/dateutil/__init__.py b/libraries/dateutil/__init__.py
similarity index 78%
rename from resources/lib/libraries/dateutil/__init__.py
rename to libraries/dateutil/__init__.py
index 10ad93fa..a29ffaa9 100644
--- a/resources/lib/libraries/dateutil/__init__.py
+++ b/libraries/dateutil/__init__.py
@@ -1,6 +1,6 @@
 # -*- coding: utf-8 -*-
 try:
-    from ._version import version as __version__
+    from _version import version as __version__
 except ImportError:
     __version__ = 'unknown'
 
diff --git a/resources/lib/libraries/dateutil/_common.py b/libraries/dateutil/_common.py
similarity index 100%
rename from resources/lib/libraries/dateutil/_common.py
rename to libraries/dateutil/_common.py
diff --git a/resources/lib/libraries/dateutil/easter.py b/libraries/dateutil/easter.py
similarity index 100%
rename from resources/lib/libraries/dateutil/easter.py
rename to libraries/dateutil/easter.py
diff --git a/resources/lib/libraries/dateutil/parser/__init__.py b/libraries/dateutil/parser/__init__.py
similarity index 87%
rename from resources/lib/libraries/dateutil/parser/__init__.py
rename to libraries/dateutil/parser/__init__.py
index 216762c0..2d20777c 100644
--- a/resources/lib/libraries/dateutil/parser/__init__.py
+++ b/libraries/dateutil/parser/__init__.py
@@ -1,11 +1,11 @@
 # -*- coding: utf-8 -*-
-from ._parser import parse, parser, parserinfo
-from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
-from ._parser import UnknownTimezoneWarning
+from _parser import parse, parser, parserinfo
+from _parser import DEFAULTPARSER, DEFAULTTZPARSER
+from _parser import UnknownTimezoneWarning
 
-from ._parser import __doc__
+from _parser import __doc__
 
-from .isoparser import isoparser, isoparse
+from isoparser import isoparser, isoparse
 
 __all__ = ['parse', 'parser', 'parserinfo',
            'isoparse', 'isoparser',
diff --git a/resources/lib/libraries/dateutil/parser/_parser.py b/libraries/dateutil/parser/_parser.py
similarity index 99%
rename from resources/lib/libraries/dateutil/parser/_parser.py
rename to libraries/dateutil/parser/_parser.py
index e8a522c9..66940802 100644
--- a/resources/lib/libraries/dateutil/parser/_parser.py
+++ b/libraries/dateutil/parser/_parser.py
@@ -39,15 +39,15 @@ import warnings
 from calendar import monthrange
 from io import StringIO
 
-from .. import six
-from ..six import binary_type, integer_types, text_type
+import six
+from six import binary_type, integer_types, text_type
 
 from decimal import Decimal
 
 from warnings import warn
 
-from .. import relativedelta
-from .. import tz
+from dateutil import relativedelta
+from dateutil import tz
 
 __all__ = ["parse", "parserinfo"]
 
diff --git a/resources/lib/libraries/dateutil/parser/isoparser.py b/libraries/dateutil/parser/isoparser.py
similarity index 99%
rename from resources/lib/libraries/dateutil/parser/isoparser.py
rename to libraries/dateutil/parser/isoparser.py
index b63ef712..cd27f93d 100644
--- a/resources/lib/libraries/dateutil/parser/isoparser.py
+++ b/libraries/dateutil/parser/isoparser.py
@@ -9,12 +9,12 @@ ISO-8601 specification.
 """
 from datetime import datetime, timedelta, time, date
 import calendar
-from .. import tz
+from dateutil import tz
 
 from functools import wraps
 
 import re
-from .. import six
+import six
 
 __all__ = ["isoparse", "isoparser"]
 
diff --git a/resources/lib/libraries/dateutil/relativedelta.py b/libraries/dateutil/relativedelta.py
similarity index 100%
rename from resources/lib/libraries/dateutil/relativedelta.py
rename to libraries/dateutil/relativedelta.py
diff --git a/resources/lib/libraries/dateutil/rrule.py b/libraries/dateutil/rrule.py
similarity index 100%
rename from resources/lib/libraries/dateutil/rrule.py
rename to libraries/dateutil/rrule.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py b/libraries/dateutil/test/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/contrib/__init__.py
rename to libraries/dateutil/test/__init__.py
diff --git a/resources/lib/libraries/dateutil/test/_common.py b/libraries/dateutil/test/_common.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/_common.py
rename to libraries/dateutil/test/_common.py
diff --git a/resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py b/libraries/dateutil/test/property/test_isoparse_prop.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/property/test_isoparse_prop.py
rename to libraries/dateutil/test/property/test_isoparse_prop.py
diff --git a/resources/lib/libraries/dateutil/test/property/test_parser_prop.py b/libraries/dateutil/test/property/test_parser_prop.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/property/test_parser_prop.py
rename to libraries/dateutil/test/property/test_parser_prop.py
diff --git a/resources/lib/libraries/dateutil/test/test_easter.py b/libraries/dateutil/test/test_easter.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_easter.py
rename to libraries/dateutil/test/test_easter.py
diff --git a/resources/lib/libraries/dateutil/test/test_import_star.py b/libraries/dateutil/test/test_import_star.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_import_star.py
rename to libraries/dateutil/test/test_import_star.py
diff --git a/resources/lib/libraries/dateutil/test/test_imports.py b/libraries/dateutil/test/test_imports.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_imports.py
rename to libraries/dateutil/test/test_imports.py
diff --git a/resources/lib/libraries/dateutil/test/test_internals.py b/libraries/dateutil/test/test_internals.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_internals.py
rename to libraries/dateutil/test/test_internals.py
diff --git a/resources/lib/libraries/dateutil/test/test_isoparser.py b/libraries/dateutil/test/test_isoparser.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_isoparser.py
rename to libraries/dateutil/test/test_isoparser.py
diff --git a/resources/lib/libraries/dateutil/test/test_parser.py b/libraries/dateutil/test/test_parser.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_parser.py
rename to libraries/dateutil/test/test_parser.py
diff --git a/resources/lib/libraries/dateutil/test/test_relativedelta.py b/libraries/dateutil/test/test_relativedelta.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_relativedelta.py
rename to libraries/dateutil/test/test_relativedelta.py
diff --git a/resources/lib/libraries/dateutil/test/test_rrule.py b/libraries/dateutil/test/test_rrule.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_rrule.py
rename to libraries/dateutil/test/test_rrule.py
diff --git a/resources/lib/libraries/dateutil/test/test_tz.py b/libraries/dateutil/test/test_tz.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_tz.py
rename to libraries/dateutil/test/test_tz.py
diff --git a/resources/lib/libraries/dateutil/test/test_utils.py b/libraries/dateutil/test/test_utils.py
similarity index 100%
rename from resources/lib/libraries/dateutil/test/test_utils.py
rename to libraries/dateutil/test/test_utils.py
diff --git a/resources/lib/libraries/dateutil/tz/__init__.py b/libraries/dateutil/tz/__init__.py
similarity index 100%
rename from resources/lib/libraries/dateutil/tz/__init__.py
rename to libraries/dateutil/tz/__init__.py
diff --git a/resources/lib/libraries/dateutil/tz/_common.py b/libraries/dateutil/tz/_common.py
similarity index 99%
rename from resources/lib/libraries/dateutil/tz/_common.py
rename to libraries/dateutil/tz/_common.py
index e2e66e7b..ccabb7da 100644
--- a/resources/lib/libraries/dateutil/tz/_common.py
+++ b/libraries/dateutil/tz/_common.py
@@ -1,4 +1,4 @@
-from ..six import PY3
+from six import PY3
 
 from functools import wraps
 
diff --git a/resources/lib/libraries/dateutil/tz/_factories.py b/libraries/dateutil/tz/_factories.py
similarity index 100%
rename from resources/lib/libraries/dateutil/tz/_factories.py
rename to libraries/dateutil/tz/_factories.py
diff --git a/resources/lib/libraries/dateutil/tz/tz.py b/libraries/dateutil/tz/tz.py
similarity index 99%
rename from resources/lib/libraries/dateutil/tz/tz.py
rename to libraries/dateutil/tz/tz.py
index 4c23242a..6fcfce86 100644
--- a/resources/lib/libraries/dateutil/tz/tz.py
+++ b/libraries/dateutil/tz/tz.py
@@ -14,17 +14,17 @@ import sys
 import os
 import bisect
 
-from .. import six
-from ..six import string_types
-from ..six.moves import _thread
-from ._common import tzname_in_python2, _tzinfo
-from ._common import tzrangebase, enfold
-from ._common import _validate_fromutc_inputs
+import six
+from six import string_types
+from six.moves import _thread
+from _common import tzname_in_python2, _tzinfo
+from _common import tzrangebase, enfold
+from _common import _validate_fromutc_inputs
 
-from ._factories import _TzSingleton, _TzOffsetFactory
-from ._factories import _TzStrFactory
+from _factories import _TzSingleton, _TzOffsetFactory
+from _factories import _TzStrFactory
 try:
-    from .win import tzwin, tzwinlocal
+    from win import tzwin, tzwinlocal
 except ImportError:
     tzwin = tzwinlocal = None
 
diff --git a/resources/lib/libraries/dateutil/tz/win.py b/libraries/dateutil/tz/win.py
similarity index 100%
rename from resources/lib/libraries/dateutil/tz/win.py
rename to libraries/dateutil/tz/win.py
diff --git a/resources/lib/libraries/dateutil/tzwin.py b/libraries/dateutil/tzwin.py
similarity index 100%
rename from resources/lib/libraries/dateutil/tzwin.py
rename to libraries/dateutil/tzwin.py
diff --git a/resources/lib/libraries/dateutil/utils.py b/libraries/dateutil/utils.py
similarity index 100%
rename from resources/lib/libraries/dateutil/utils.py
rename to libraries/dateutil/utils.py
diff --git a/resources/lib/libraries/dateutil/zoneinfo/__init__.py b/libraries/dateutil/zoneinfo/__init__.py
similarity index 100%
rename from resources/lib/libraries/dateutil/zoneinfo/__init__.py
rename to libraries/dateutil/zoneinfo/__init__.py
diff --git a/resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
similarity index 100%
rename from resources/lib/libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
rename to libraries/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz
diff --git a/resources/lib/libraries/dateutil/zoneinfo/rebuild.py b/libraries/dateutil/zoneinfo/rebuild.py
similarity index 100%
rename from resources/lib/libraries/dateutil/zoneinfo/rebuild.py
rename to libraries/dateutil/zoneinfo/rebuild.py
diff --git a/resources/lib/libraries/requests/__init__.py b/libraries/requests/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/__init__.py
rename to libraries/requests/__init__.py
diff --git a/resources/lib/libraries/requests/adapters.py b/libraries/requests/adapters.py
similarity index 100%
rename from resources/lib/libraries/requests/adapters.py
rename to libraries/requests/adapters.py
diff --git a/resources/lib/libraries/requests/api.py b/libraries/requests/api.py
similarity index 100%
rename from resources/lib/libraries/requests/api.py
rename to libraries/requests/api.py
diff --git a/resources/lib/libraries/requests/auth.py b/libraries/requests/auth.py
similarity index 100%
rename from resources/lib/libraries/requests/auth.py
rename to libraries/requests/auth.py
diff --git a/resources/lib/libraries/requests/cacert.pem b/libraries/requests/cacert.pem
similarity index 100%
rename from resources/lib/libraries/requests/cacert.pem
rename to libraries/requests/cacert.pem
diff --git a/resources/lib/libraries/requests/certs.py b/libraries/requests/certs.py
similarity index 100%
rename from resources/lib/libraries/requests/certs.py
rename to libraries/requests/certs.py
diff --git a/resources/lib/libraries/requests/compat.py b/libraries/requests/compat.py
similarity index 100%
rename from resources/lib/libraries/requests/compat.py
rename to libraries/requests/compat.py
diff --git a/resources/lib/libraries/requests/cookies.py b/libraries/requests/cookies.py
similarity index 100%
rename from resources/lib/libraries/requests/cookies.py
rename to libraries/requests/cookies.py
diff --git a/resources/lib/libraries/requests/exceptions.py b/libraries/requests/exceptions.py
similarity index 100%
rename from resources/lib/libraries/requests/exceptions.py
rename to libraries/requests/exceptions.py
diff --git a/resources/lib/libraries/requests/hooks.py b/libraries/requests/hooks.py
similarity index 100%
rename from resources/lib/libraries/requests/hooks.py
rename to libraries/requests/hooks.py
diff --git a/resources/lib/libraries/requests/models.py b/libraries/requests/models.py
similarity index 100%
rename from resources/lib/libraries/requests/models.py
rename to libraries/requests/models.py
diff --git a/resources/lib/libraries/requests/packages/README.rst b/libraries/requests/packages/README.rst
similarity index 100%
rename from resources/lib/libraries/requests/packages/README.rst
rename to libraries/requests/packages/README.rst
diff --git a/resources/lib/libraries/requests/packages/__init__.py b/libraries/requests/packages/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/__init__.py
rename to libraries/requests/packages/__init__.py
diff --git a/resources/lib/libraries/requests/packages/chardet/__init__.py b/libraries/requests/packages/chardet/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/__init__.py
rename to libraries/requests/packages/chardet/__init__.py
diff --git a/resources/lib/libraries/requests/packages/chardet/big5freq.py b/libraries/requests/packages/chardet/big5freq.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/big5freq.py
rename to libraries/requests/packages/chardet/big5freq.py
diff --git a/resources/lib/libraries/requests/packages/chardet/big5prober.py b/libraries/requests/packages/chardet/big5prober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/big5prober.py
rename to libraries/requests/packages/chardet/big5prober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/chardetect.py b/libraries/requests/packages/chardet/chardetect.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/chardetect.py
rename to libraries/requests/packages/chardet/chardetect.py
diff --git a/resources/lib/libraries/requests/packages/chardet/chardistribution.py b/libraries/requests/packages/chardet/chardistribution.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/chardistribution.py
rename to libraries/requests/packages/chardet/chardistribution.py
diff --git a/resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py b/libraries/requests/packages/chardet/charsetgroupprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/charsetgroupprober.py
rename to libraries/requests/packages/chardet/charsetgroupprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/charsetprober.py b/libraries/requests/packages/chardet/charsetprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/charsetprober.py
rename to libraries/requests/packages/chardet/charsetprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/codingstatemachine.py b/libraries/requests/packages/chardet/codingstatemachine.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/codingstatemachine.py
rename to libraries/requests/packages/chardet/codingstatemachine.py
diff --git a/resources/lib/libraries/requests/packages/chardet/compat.py b/libraries/requests/packages/chardet/compat.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/compat.py
rename to libraries/requests/packages/chardet/compat.py
diff --git a/resources/lib/libraries/requests/packages/chardet/constants.py b/libraries/requests/packages/chardet/constants.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/constants.py
rename to libraries/requests/packages/chardet/constants.py
diff --git a/resources/lib/libraries/requests/packages/chardet/cp949prober.py b/libraries/requests/packages/chardet/cp949prober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/cp949prober.py
rename to libraries/requests/packages/chardet/cp949prober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/escprober.py b/libraries/requests/packages/chardet/escprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/escprober.py
rename to libraries/requests/packages/chardet/escprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/escsm.py b/libraries/requests/packages/chardet/escsm.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/escsm.py
rename to libraries/requests/packages/chardet/escsm.py
diff --git a/resources/lib/libraries/requests/packages/chardet/eucjpprober.py b/libraries/requests/packages/chardet/eucjpprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/eucjpprober.py
rename to libraries/requests/packages/chardet/eucjpprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/euckrfreq.py b/libraries/requests/packages/chardet/euckrfreq.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/euckrfreq.py
rename to libraries/requests/packages/chardet/euckrfreq.py
diff --git a/resources/lib/libraries/requests/packages/chardet/euckrprober.py b/libraries/requests/packages/chardet/euckrprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/euckrprober.py
rename to libraries/requests/packages/chardet/euckrprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/euctwfreq.py b/libraries/requests/packages/chardet/euctwfreq.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/euctwfreq.py
rename to libraries/requests/packages/chardet/euctwfreq.py
diff --git a/resources/lib/libraries/requests/packages/chardet/euctwprober.py b/libraries/requests/packages/chardet/euctwprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/euctwprober.py
rename to libraries/requests/packages/chardet/euctwprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/gb2312freq.py b/libraries/requests/packages/chardet/gb2312freq.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/gb2312freq.py
rename to libraries/requests/packages/chardet/gb2312freq.py
diff --git a/resources/lib/libraries/requests/packages/chardet/gb2312prober.py b/libraries/requests/packages/chardet/gb2312prober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/gb2312prober.py
rename to libraries/requests/packages/chardet/gb2312prober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/hebrewprober.py b/libraries/requests/packages/chardet/hebrewprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/hebrewprober.py
rename to libraries/requests/packages/chardet/hebrewprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/jisfreq.py b/libraries/requests/packages/chardet/jisfreq.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/jisfreq.py
rename to libraries/requests/packages/chardet/jisfreq.py
diff --git a/resources/lib/libraries/requests/packages/chardet/jpcntx.py b/libraries/requests/packages/chardet/jpcntx.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/jpcntx.py
rename to libraries/requests/packages/chardet/jpcntx.py
diff --git a/resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py b/libraries/requests/packages/chardet/langbulgarianmodel.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/langbulgarianmodel.py
rename to libraries/requests/packages/chardet/langbulgarianmodel.py
diff --git a/resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py b/libraries/requests/packages/chardet/langcyrillicmodel.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/langcyrillicmodel.py
rename to libraries/requests/packages/chardet/langcyrillicmodel.py
diff --git a/resources/lib/libraries/requests/packages/chardet/langgreekmodel.py b/libraries/requests/packages/chardet/langgreekmodel.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/langgreekmodel.py
rename to libraries/requests/packages/chardet/langgreekmodel.py
diff --git a/resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py b/libraries/requests/packages/chardet/langhebrewmodel.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/langhebrewmodel.py
rename to libraries/requests/packages/chardet/langhebrewmodel.py
diff --git a/resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py b/libraries/requests/packages/chardet/langhungarianmodel.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/langhungarianmodel.py
rename to libraries/requests/packages/chardet/langhungarianmodel.py
diff --git a/resources/lib/libraries/requests/packages/chardet/langthaimodel.py b/libraries/requests/packages/chardet/langthaimodel.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/langthaimodel.py
rename to libraries/requests/packages/chardet/langthaimodel.py
diff --git a/resources/lib/libraries/requests/packages/chardet/latin1prober.py b/libraries/requests/packages/chardet/latin1prober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/latin1prober.py
rename to libraries/requests/packages/chardet/latin1prober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py b/libraries/requests/packages/chardet/mbcharsetprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/mbcharsetprober.py
rename to libraries/requests/packages/chardet/mbcharsetprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py b/libraries/requests/packages/chardet/mbcsgroupprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/mbcsgroupprober.py
rename to libraries/requests/packages/chardet/mbcsgroupprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/mbcssm.py b/libraries/requests/packages/chardet/mbcssm.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/mbcssm.py
rename to libraries/requests/packages/chardet/mbcssm.py
diff --git a/resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py b/libraries/requests/packages/chardet/sbcharsetprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/sbcharsetprober.py
rename to libraries/requests/packages/chardet/sbcharsetprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py b/libraries/requests/packages/chardet/sbcsgroupprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/sbcsgroupprober.py
rename to libraries/requests/packages/chardet/sbcsgroupprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/sjisprober.py b/libraries/requests/packages/chardet/sjisprober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/sjisprober.py
rename to libraries/requests/packages/chardet/sjisprober.py
diff --git a/resources/lib/libraries/requests/packages/chardet/universaldetector.py b/libraries/requests/packages/chardet/universaldetector.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/universaldetector.py
rename to libraries/requests/packages/chardet/universaldetector.py
diff --git a/resources/lib/libraries/requests/packages/chardet/utf8prober.py b/libraries/requests/packages/chardet/utf8prober.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/chardet/utf8prober.py
rename to libraries/requests/packages/chardet/utf8prober.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/__init__.py b/libraries/requests/packages/urllib3/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/__init__.py
rename to libraries/requests/packages/urllib3/__init__.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/_collections.py b/libraries/requests/packages/urllib3/_collections.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/_collections.py
rename to libraries/requests/packages/urllib3/_collections.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/connection.py b/libraries/requests/packages/urllib3/connection.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/connection.py
rename to libraries/requests/packages/urllib3/connection.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/connectionpool.py b/libraries/requests/packages/urllib3/connectionpool.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/connectionpool.py
rename to libraries/requests/packages/urllib3/connectionpool.py
diff --git a/libraries/requests/packages/urllib3/contrib/__init__.py b/libraries/requests/packages/urllib3/contrib/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py b/libraries/requests/packages/urllib3/contrib/appengine.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/contrib/appengine.py
rename to libraries/requests/packages/urllib3/contrib/appengine.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py b/libraries/requests/packages/urllib3/contrib/ntlmpool.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/contrib/ntlmpool.py
rename to libraries/requests/packages/urllib3/contrib/ntlmpool.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py b/libraries/requests/packages/urllib3/contrib/pyopenssl.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/contrib/pyopenssl.py
rename to libraries/requests/packages/urllib3/contrib/pyopenssl.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/exceptions.py b/libraries/requests/packages/urllib3/exceptions.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/exceptions.py
rename to libraries/requests/packages/urllib3/exceptions.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/fields.py b/libraries/requests/packages/urllib3/fields.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/fields.py
rename to libraries/requests/packages/urllib3/fields.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/filepost.py b/libraries/requests/packages/urllib3/filepost.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/filepost.py
rename to libraries/requests/packages/urllib3/filepost.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/__init__.py b/libraries/requests/packages/urllib3/packages/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/packages/__init__.py
rename to libraries/requests/packages/urllib3/packages/__init__.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py b/libraries/requests/packages/urllib3/packages/ordered_dict.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/packages/ordered_dict.py
rename to libraries/requests/packages/urllib3/packages/ordered_dict.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/six.py b/libraries/requests/packages/urllib3/packages/six.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/packages/six.py
rename to libraries/requests/packages/urllib3/packages/six.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py b/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py
rename to libraries/requests/packages/urllib3/packages/ssl_match_hostname/__init__.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py b/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py
rename to libraries/requests/packages/urllib3/packages/ssl_match_hostname/_implementation.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/poolmanager.py b/libraries/requests/packages/urllib3/poolmanager.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/poolmanager.py
rename to libraries/requests/packages/urllib3/poolmanager.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/request.py b/libraries/requests/packages/urllib3/request.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/request.py
rename to libraries/requests/packages/urllib3/request.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/response.py b/libraries/requests/packages/urllib3/response.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/response.py
rename to libraries/requests/packages/urllib3/response.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/__init__.py b/libraries/requests/packages/urllib3/util/__init__.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/__init__.py
rename to libraries/requests/packages/urllib3/util/__init__.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/connection.py b/libraries/requests/packages/urllib3/util/connection.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/connection.py
rename to libraries/requests/packages/urllib3/util/connection.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/request.py b/libraries/requests/packages/urllib3/util/request.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/request.py
rename to libraries/requests/packages/urllib3/util/request.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/response.py b/libraries/requests/packages/urllib3/util/response.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/response.py
rename to libraries/requests/packages/urllib3/util/response.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/retry.py b/libraries/requests/packages/urllib3/util/retry.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/retry.py
rename to libraries/requests/packages/urllib3/util/retry.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/ssl_.py b/libraries/requests/packages/urllib3/util/ssl_.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/ssl_.py
rename to libraries/requests/packages/urllib3/util/ssl_.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/timeout.py b/libraries/requests/packages/urllib3/util/timeout.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/timeout.py
rename to libraries/requests/packages/urllib3/util/timeout.py
diff --git a/resources/lib/libraries/requests/packages/urllib3/util/url.py b/libraries/requests/packages/urllib3/util/url.py
similarity index 100%
rename from resources/lib/libraries/requests/packages/urllib3/util/url.py
rename to libraries/requests/packages/urllib3/util/url.py
diff --git a/resources/lib/libraries/requests/sessions.py b/libraries/requests/sessions.py
similarity index 100%
rename from resources/lib/libraries/requests/sessions.py
rename to libraries/requests/sessions.py
diff --git a/resources/lib/libraries/requests/status_codes.py b/libraries/requests/status_codes.py
similarity index 100%
rename from resources/lib/libraries/requests/status_codes.py
rename to libraries/requests/status_codes.py
diff --git a/resources/lib/libraries/requests/structures.py b/libraries/requests/structures.py
similarity index 100%
rename from resources/lib/libraries/requests/structures.py
rename to libraries/requests/structures.py
diff --git a/resources/lib/libraries/requests/utils.py b/libraries/requests/utils.py
similarity index 100%
rename from resources/lib/libraries/requests/utils.py
rename to libraries/requests/utils.py
diff --git a/resources/lib/libraries/dateutil/six.py b/libraries/six.py
similarity index 100%
rename from resources/lib/libraries/dateutil/six.py
rename to libraries/six.py
diff --git a/resources/lib/downloader.py b/resources/lib/downloader.py
index 85081155..43c43d1c 100644
--- a/resources/lib/downloader.py
+++ b/resources/lib/downloader.py
@@ -13,7 +13,7 @@ import xbmc
 import xbmcvfs
 import xbmcaddon
 
-from libraries import requests
+import requests
 from helper.utils import should_stop, delete_folder
 from helper import settings, stop, event, window, kodi_version, unzip, create_id
 from emby import Emby
diff --git a/resources/lib/emby/core/http.py b/resources/lib/emby/core/http.py
index a96c80c7..e7d585ea 100644
--- a/resources/lib/emby/core/http.py
+++ b/resources/lib/emby/core/http.py
@@ -6,7 +6,7 @@ import json
 import logging
 import time
 
-from libraries import requests
+import requests
 from exceptions import HTTPException
 
 #################################################################################################
diff --git a/resources/lib/entrypoint/service.py b/resources/lib/entrypoint/service.py
index 1153b8ab..2fe12f32 100644
--- a/resources/lib/entrypoint/service.py
+++ b/resources/lib/entrypoint/service.py
@@ -17,7 +17,7 @@ import client
 import library
 import setup
 import monitor
-from libraries import requests
+import requests
 from views import Views, verify_kodi_defaults
 from helper import _, window, settings, event, dialog, find, compare_version
 from downloader import get_objects
diff --git a/resources/lib/helper/playutils.py b/resources/lib/helper/playutils.py
index 9af3a72d..f3146cdc 100644
--- a/resources/lib/helper/playutils.py
+++ b/resources/lib/helper/playutils.py
@@ -14,8 +14,8 @@ import api
 import database
 import client
 import collections
+import requests
 from . import _, settings, window, dialog
-from libraries import requests
 from downloader import TheVoid
 from emby import Emby
 
diff --git a/resources/lib/helper/utils.py b/resources/lib/helper/utils.py
index 23cb5e11..71d5bbbb 100644
--- a/resources/lib/helper/utils.py
+++ b/resources/lib/helper/utils.py
@@ -17,6 +17,7 @@ import xbmcgui
 import xbmcvfs
 
 from . import _
+from dateutil import tz, parser
 
 #################################################################################################
 
@@ -449,8 +450,6 @@ def convert_to_local(date):
 
     ''' Convert the local datetime to local.
     '''
-    from libraries.dateutil import tz, parser
-
     try:
         date = parser.parse(date) if type(date) in (unicode, str) else date
         date = date.replace(tzinfo=tz.tzutc())
diff --git a/resources/lib/libraries/__init__.py b/resources/lib/libraries/__init__.py
deleted file mode 100644
index a81e44db..00000000
--- a/resources/lib/libraries/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-import requests
-import dateutil
diff --git a/resources/lib/libraries/mutagen/__init__.py b/resources/lib/libraries/mutagen/__init__.py
deleted file mode 100644
index 03ad7aee..00000000
--- a/resources/lib/libraries/mutagen/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Michael Urman
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-
-"""Mutagen aims to be an all purpose multimedia tagging library.
-
-::
-
-    import mutagen.[format]
-    metadata = mutagen.[format].Open(filename)
-
-`metadata` acts like a dictionary of tags in the file. Tags are generally a
-list of string-like values, but may have additional methods available
-depending on tag or format. They may also be entirely different objects
-for certain keys, again depending on format.
-"""
-
-from mutagen._util import MutagenError
-from mutagen._file import FileType, StreamInfo, File
-from mutagen._tags import Metadata, PaddingInfo
-
-version = (1, 31)
-"""Version tuple."""
-
-version_string = ".".join(map(str, version))
-"""Version string."""
-
-MutagenError
-
-FileType
-
-StreamInfo
-
-File
-
-Metadata
-
-PaddingInfo
diff --git a/resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/__init__.cpython-35.pyc
deleted file mode 100644
index 0d767fdced0b459f7462e04b1308d99da0fe85f6..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 914
zcmZWn&2AGh5T5->vUQt+1XLW5@R3L~5<*-mKu8EABnp*UE=$nX$xaft_Il;@M%pWV
z2i}3_;pQu+9=UR2oTh?`UCr2@8GrN5jQ55^c4zYI(~kt;C;aoJw0}j?d?i%^<fJ1g
zV^9(381fjRa85uapi{_G&^^d|p!<;buY4(}0q6|!4D=S{TV$n$K{<f4ZNMGCAs~Z`
z7%n2ncL50#Q<5GDVPHlQZe?&hV&Yo_aM(Jrz={j08}1CBA{TU0ir1~JO@myw%1Mo-
z6r4gAvRZH@r&ic*%-F#JW1K#zYhxY7uF=My=f-N`zN~#3ohXG9{KbF%@y8mgM{}vL
z5*kO0on8l@axrraWjZH9yp%I1O+`7mnFksTmlbzQ<l*j^p9G7r$f*>x#J=N#DcQIX
z-Z)F7_t#J>p;~Mn^Jz<DMaP$7MfhbY*YJu{`lTreYbB%-Q-!QVqF9Ei4P_LxSbkL<
zrCnmTh7qb+w+<^OEmHPU&gW?9$j$T&sS-wa_zbNRMEo2(;vg1be*=F5%r5DPz$o%N
zoAJD{*4R<vv$s_4Nmrw1$IhbA@2a`+X|RrBpB-*G?YECYP1Pc}FKNnf`H+;4#vewp
z&1h@4LV{a<P!y$^6@?#OkFIE)R5lLbe{G@5mL4?N@QQ8krZ>#P_Jd;(9D^0whNk7y
zGb1aXXi@uwhTwNMj^b(}-@)DumxB8J<n_VisH5grPoGWR%ju+%4)<#@JE!N_Og8K$
nMIkHcieg-MK6|cB*(!W_j{urIQvLXL^dQ=c`^o(zjj#Vd+O!M@

diff --git a/resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_compat.cpython-35.pyc
deleted file mode 100644
index 93f423d567c397fd76892bf3e94725f87947d426..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2754
zcmbtW-EQ1O6h32nf8)*OuOUCBq#z_Ll_;Wuizo_J!%d~Kjfh%_CCKvHlf=!gcNsfr
z62T4O33v-0fD7J$XP8^AdV$1sulUY*y-Aidsf0H(bA0CboAW=JZ#3$^cK&(ZtP=f2
zmmUlBBaHM9K@{OnP?xApB#B-M<}5}Q9a_}3NLu8!ZC=#hM%JdTLp_Ju4he~+RB+3N
zTOq-S4t1;4t5LfKsXDbMNV)`K51<W_lO&slo?@asO>&0XGbCrJJxg+q+IJ`daG8FA
zUh@=HD5@IzJ&I}ty+BdDpcg5cDCi}U%M`hEE}%<;qDk}RT}u8WX_NZhC<rs&ziftM
z%xD^JiLDY6ONG_WmERLX&6$AOlSb`(Y@z&sm5-(h1!gIlE@&8nt&t(v8W}QI&@cpI
zIUk0=-ke^d2=*=*^;amuR$&Svw&N|m=9so#b2vSrt2>#Bll>>pKs)hSJP3J%A6|KE
zVWbUAKO)YU1-od+6D|H)CQ<q-&JH|FJDEJmv<=R0q>w!HW^HSIcl$Iu=qC?9-+dZ)
zcGEbMpALfXFxZ#rZtp0=n(X>vzc&c7)xoJM;j?>uo>4-W_Ymo39$tAEh3X)3h0dwa
zrUrP0(MzG4h>Q~XRD-mxZghiQCki(2<1NJ?<zjkw{5s<w2i+ry1Jncqu$>o27`zX`
zGU)FPTxH-xd3qD<N7urR0>u8vj7u(}<1j3{qH~>|9~XdUYmwYrQBnnQDnH2v4_1tg
z4_2~{w_aZ{?3F9*E0&E^SO*|dERV`znbfkO4ac_#&EaC}3q#vH`$k~Lvs=32`)Q_p
zU)OxUbDGK2_dQ2feLsrB%=gtaPH0c~{!tQ#{YZi{0}d>-!)`ieqn=D(1p`0pGbA$H
zV{_R{B$~ptc&I!1*OkY7){|#fQs2g&$+S5TP!oB7=oAFe0#|@J5~(i&H3g|L4NNwC
z3I2bxVdOKet2}J`m+Puztmn|3TnGAS1j<1C@H*1?JG|te89>p4*UYP+{55T{F)tAj
zbL3Bgl6!@K&GJJ5G`B*416LZAzSYvDAPwW#E9-ik#z~q5Nhq}qPwR3dIW}qmO6amo
z%nR4GXQ?t4RpDu@Zg~GOM#=?A2u@SXnEColV`t|E;^H}{QA4s+6Fa&z>;Z2r(Wj8Q
zAt<-Nmv3zWU;i(B=ax>;x*45|1c5`j;TlBXzA_ELZjD@7<O+Kr<!%e1@8<}4z&;g}
zXW|R-gUz=a=Q+9^hZC7@t*&WtJQlt;wGH%fc`{I&+l<Kvl<91_=(npI-F_H!)6Lb9
zoIwEW+sek^0ER-&YJPZS;4O@F5mR21Fpe)Ji*w*-^Iu4=u%6^=$<I^}$_{QE&(>Bp
z&?OT)kmD^;SB4Gn8^-5@NGR?ZBez|-ejMyTfr};})Ero2jxi19Iru|B?>=heG}Z30
zRB7B#{5aX`YkT|K&v<&M*hV#vsa*?;ds$b*VSCc%e#`oO^g2FYJKcV=uS-1p+K!V<
zH!#V4sr>A8AX9ahp(?{}solI0=6kxv*R`Q_`9uZDKI%8@4KN)j?O^iF?W?$DDsEG1
zlFu|cnC_$<c_Kq&qPAj~fqx!!$sifWUGjyg_Te4e>qkdjxyf%S``Wc<Ma3$MDZyh3
il#5>(D}I)1^P6kAR^4jW3Mt;L;g?I9{dyti+W!Ksf!+}S

diff --git a/resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_constants.cpython-35.pyc
deleted file mode 100644
index 368544dcafdaf72965a51adee92899dd834cab69..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3099
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
Fe*pD4cI^NF

diff --git a/resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_file.cpython-35.pyc
deleted file mode 100644
index 2e8bbe118279fb735d7106993b1282ab526e22c2..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 7763
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_mp3util.cpython-35.pyc
deleted file mode 100644
index e242cafa64776176be78a6c7ef2f18a25b8a3e70..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8626
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_tags.cpython-35.pyc
deleted file mode 100644
index b49b1dfcbd2a2fce928e9ac92ebfbdbf514f2518..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2970
zcma)8OK;mo5MGM3Br9?aC#jRBuRSz?d5938=tW4<7;aFcP1GQ28q_e#tjLu_n-6ul
zvLPV_iYDj&kN%sUdF`b+_S#dwSyD7(yG3d5aJ4%-JCAQ>xV^E_*?a!iv*(u>`-9aU
z2lM+F*=>+71I7Z2y*5-b_R41a7FHW9u+6H?%2lj5Z0N4}I8{9j%<f}kPeH(lvk<rs
zUW@J93}W^jL)S<i1mL3mCJXShY33~!;J<C=Z5FJvpkwBkT*7VL{L&K<1Yt6Hn2b~L
z2>0!dGOdKK`K;LBA$C+ObeJYQ9UV$v_c|Sp=i6zR@slu$_(*~~%VfZH%Jo!oUqsPJ
z_(yy^PkeJyPld*ELcDWu8VL~0GjWXTxfYYGl-utY^68F1j(NQBR`Pa8nLe-S?DmZ$
z;-yDPJy(g*xi^v8!+9}}v{xFl<E~V%>6lH^c@(UUlKLV}=ZWU&xN-vQg4TTJ-d%p%
z<s(71rir=Q*qbI=;zSlMaXNt?AZ6u1h@qe3Fv8wg%azSNcn~YTo{Q+zC+F{u!^u?h
zW1$vs$4n*^E?j;pw1^Mi3FZ<3FiMFioFp-+B(>kGSzS7Whw&_u#83pbjEwMoY2rA~
zOi-rMNbAQ^Px~(}aOew8TWJ7~zZLkNIrj#rWxh=m54{P{dl=a^$V-d8WX!{)VI~IL
zL)RL(jl5NQFL%hNu8p^h#)@Pp2jn-Vnt1))+v^`ZSwMy4)6Whbg`<Nk)biF0*b@_(
z9mI10OC|@N!Pjh&w>>XRLhX4saXKSO%v!gWS8I^!typ}Ew(-!dG>qm$OpsL2{16kH
z-MID>6gQ^Of<p_&o<l6Jv7<GnzBb%ovDlHr)E8v%kOWwGVTxoOy6xrfgol3#(8)CP
zr(CO0W|U-b1`?4ApjKpQB1TaWDawKK2qHul0tzhoQ;|$0#cVuBzDRy7RfhatWgtoE
z7pc_eyXTUj4q_AMf?qFYvJauUS_>H*{H6nky$L|VKjB105Ez7PhcUNdLTQ5P+G-6O
zxibaYb2~~;OneK{qMa)jz|X}-_6`VZZ&_Q7P5e2Hmc9JA9?lES`J@Cr^sX@Z34S0%
zQ1z(I)OH!-R}4Wi;eCa0VbdVgQXAlg1xD+26bI!|u{*1E*Q4u8Fn^9C8NrrWU8~br
zUazZJXRMG%2y`$E@;D{{nueC>Bo@JkKvaOuW@DRe58cLKdF{^4%#@<LH-l9b!t|c&
z<PHUik{}VW^t_t?En~@9^}(s+OnKe|++C)NeJ))JmVw(*TVTsOo)@RVJi-Ne*Ykcv
zeJifZTb>uBK4bz2MINg*&B&K(lgMQv<f0m(@GJ7fI)({%_j0q*Tx;50r@P&4nmr$T
z)kEU%WBf<oM4YRRi5^;YOiWD2M6Y~}CW~f@pB-wK^wacH>YMGyQj0)nu{_*OxL7sK
zAoTUEC_IvHx5^bWN5`aIKn;ZZ5qc)49vF*cLEVP7P^ikY871iBqJS0kG;fvltilHw
zO5+<cmV*GPS(oaI`k=%wVYk741?DR3=1!D~ph`KRCRyG*I-&Pjw^MlN<D-my*II6^
zjJdc@Qk;x7l=gt?FD9XioXM{YQvGn%d1UZFsZ^C}sQ19HZV)*Qzlr>HtbK=(nWVLr
z8<hfN#o(=6rrY<3w{|<S)@>}W4aG5hZYrNCp~7_J-oUlg`#7;m<u>(cg6kPI(10M|
z`$`YHrU&i(Pc?piD&rK2P(R{hmBzf(<Mf;7VZ!4yqpzimiamX0#Zn|0J}egK00J$#
z4wlsym@<Nk4=XCjMmQmg3@Y$L%V+EbOwM^cZw6>DTK-TO+gff`PA~S{w#jvMg^ph(
z@-7jR%@i>8A(4-WoQBiPOiZc$e}w_8GS+pF%?;$Ty}5?6i}rwi-z>g0O>UdTi{#|(
PJ4L0rXG#MBWPA4?BbT<w

diff --git a/resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_toolsutil.cpython-35.pyc
deleted file mode 100644
index cabceb1e10e6403e5472a8689571666cd647a182..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 6102
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
UpK+#}b6(?2qc(M<(P&)xALiS7%>V!Z

diff --git a/resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_util.cpython-35.pyc
deleted file mode 100644
index 199c081ba21657517d7297d2389319f50067db88..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17420
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/_vorbis.cpython-35.pyc
deleted file mode 100644
index 5a667067893e573ea9145da73a1180c5996a0eb6..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 10304
zcmcgy&2Jn>cCViA84gK_6e)47SY^pHIRZJ7EicybT8?DOa<sN6N2DZsEP1v$QzV=0
z=_adt_%R`aWMyPGKoTT5<+!H=drU65CO|&^g`f|~VNV7EBtd`xQ7-wtSKU3s5w(CX
z8M}+!)%De{-uu1Zd!^~o(aN8#{l}xfeM^X6ih*Aq_qT8b|Bj+9L<6NI+8N<mqJbN8
zpAn6WXlF$`C)}*)<U}JUI(gB^i%vl_3ZhdKjiTt3M581+Wzi@Li+bjTTM?ZR(HIe(
zs%TVETM+G{_y+Tg3N<PkW8#x`NwmvUg;I@+=ULG>gW8H{k8tgXP-nRIENZKwJ<7G{
zZfYk`J0{xWTstmQMr=(Abxu6TBB$`~jA)<bcV~r~5^98BOw$*x)FyiW`%u|!X?tzC
zuboiI$G%>30@?CA9p#0xZHIQfQaM>0NLwqpu9aH%bw_Ti!)trC+f{PMc69JVs^BJ-
zmK~@?#|xAfIH9wr<n5KEd-r%Y?qCLb<T)+Btz-~t$J+?%@-~eiJNBX6wD(k{({)2<
z$5qnw!E#nN(RxR<oOP#VhmP+_EZGj_W*F|gf9=}-{(k+y+1af7dgIz2f6%qI-@3N9
z7=KoMGwita%D32YvHY!tvQ~(ST4~z}R8;)vFjT?42T?gx2VpZj+)+{CK-=Diin5=7
zd4q~~YMH2f$7zN4odd^<@+}fni$<bf23Ho>60TqqMJR-RLxh$%!hJ?O&+sGE$r8^k
z@e@lN+@e=^p4G)M8nMo8qwhn$&x-9_a-S31=sZuVwX!=eo`gB-h(T7MSu3?-?_VCO
zu&X^8YHWzJF6~&ZEFu!2(;nVrNE<?x_&t8bYqL>1*q_i|!%moeTvOq`Ql7ki@XmEP
zAMDsIRpVhuovt7J;EtT1JD#g8NOjP1yKQW)oVz&}Pqk`qtAN&atsC#Z<H^qt!%ghw
zI}7sV${Rd%Z^l!q9mvp^YpgQT^Fz5tJNCkoFRbGi*3G-iwY)Au>`}!Dpnjq4wN#Yn
zMp1bc`}wifzK(KhzVBiqvk*XJwSFP=Idpn)ed+zDW+6Ae|MW9w?J1O3E#m98?F|(?
z?Q}!@^`173X4Q8NqpA}$S+Y%x^gY`2Dhg4avZ|R;>rJba(bITtQDyQYUAcuTID<j~
z53uPdfil=jEGn;Pjmx!cRBARoyQ7-TsM2h9{C3x+`)aef+qGTuh8I3iOkqrY9>sN<
zfz+{T@$+|D!jv)lM=;><uEr8rY*>=MK;zF)L0>yCF$qxf?62YOU0liR^!%M>pER?7
z>;W90I^SjPi<s-Zfw}4V#N0Vl+@c2iD29aJfLUgQ%89MKI1gwys1JMl1`eSp41mX&
z4YvUBPLLX?pW<@^m1t~$868JN3dC>&7!%+dl^!n{oVEW6>TrA5E_ZAlD7_%<R!aqe
zPj@gZt-Q7ZR>gL7+jUd4_BGy+nE?)Wv_j`S)h0Ym<}o1PIzcG?b+#uc>>zlXDFPoe
z9VXVu%{vN~*_JKaBfIMcsx1k2!vr`^xD5jrw!3e`5$tPwX9vw;-hofU&;ng&TVW3Q
zU}GcxYF^En69N&G-I2bQw!Hh<?InpWHleFrbN$x#0tS&7Bap6d0|$)LU_Zruc$js^
zRf{}@zqX}X;R1t%EvcRiCJ|i-kF(YVbi8li@2cE`a544bBIaOtp|#AJ+BrGz)K%R$
zN?rS*Y^!w}zDdsAH;^!A5ULbE%H<YW9{q@9kj_286~NPzkps4yg7IC5?Xu9n%l^Si
z{KE|qX6Y@U9WKFpO%T$vV}SPvmPME+Ei&E%^;-q;T!>@#23tjHd6W@HS@Fk~u+rB@
zIdQ>E3#eJC<(4&}P?Uqj+r76^CDCfP>p*=BteUMzMOrIG`DWnslpaH0Jx&FET2$P$
z0~-?61ZYt%RIVH4Xp?IBC>JPqJ<8EGMmbw=1W{popa158&N>`G5CRW^JjZjwX7fIt
zf(aDDD(9-!IR96*s@9~{JDV)Hp0p+(zT?6Tt+r@M^dlgi$F+njn8$t`zOdkpx7f>W
zG1@|gBxDvZY9J&$ZZQf#hLARiEJr^4lwNsp(6z7^M|tXX=h=pJTzEy(qBy)n8$)vx
zhvzshi6`E?$c5-#6zdtWT@m^pgk@PF5C)o16w{ahRN+g{5?>r%6h|eo1;EJ>6~H2&
zX2fw>99J;RE{3V_0FNx|ctlve-y&@QauIhZTl-%N?|rT*a!t(woE;U!G=|KWLA>vY
za72VvafEVIY>kn2fuGKRomx-6B*?1>4E;MJ;tO8Sv^Xk@t#RV4Mrau4iFX+*Im0XY
z6MTt<@c@u$n0(6a*W!!ttk8d9QE!MG>=av37U2Xn!@d5R#1BS`$D;}f29*~2J%MuT
z|FObzu}Db@ypiC?GkCaFrMFWA!KjDao_KRu4aO>oa5|Cjl}s#Q@b12J@og)$GxlG@
zIzA^11{4`*lp^iA_B*IetwI{0iGhWv)TiojA9xI^ywN2ponQ>TyDHhaql%uA-yC6C
zE1X-9bFmuZ7GN~zY<Rv_b9K2&xM#1g1JVMS(B`%>4)VlU<R~`%edz<nG^n&oOdTNH
z^1YCFJ;ni-QyN|(go{Y~UIneUJA}}HK(I$*kqdI&c3i_HleYkB8iTkOI~p%E2uj~V
zRDnhyFy}}i#*;Sigkg0^mM56}Lb?<HGxw%B*x$f`utON?&Tb*-f5Nw9`n`NNTwi>z
zcL@%Q1|Wo!v$O)B{Q}sdFJ0`6(xYgs38tfh5wosQ_j1?U_WXTsw*4)=T#oW~&~lvK
zq~jS14Gt*DmB?AmAljQ66wSUJfWd+`^)8c&B%-oU<=?gQx<U&mcCjT*SLxSzIlQlk
zJdE?v1*#l<u&4C8>+drgibn3Mj;{~-f$3e84|ZH9j4B}VT|?J&79I6P>YWV^gQx^D
z4sc!fqk<x8rJ{*glbC76orpa%XN?Nvy}fo+Cehhi+lQK{9HV@#s;|(%Mfyls0EQ&e
z8lb}HKx5Rb9$)2cgqw(y{H_;9mJ=1?nu2lQQ5FJ?@{ny8K^8_C@(B7e=J^U&@Bs=j
zmbETeuj87;m9;M6?-c%Cv?kyh3b@W?7V$i7japOL0-Q$GI-hydI)j?tXzDh|Ikm_G
z(vQSMWOfNx@B#&(f{-V1$iWIZWc?=j;WTVJ1p06ra7B<tK#29kyTB+;`2CRaJC~yN
z!OyAroo9c}Xbqsth{I{Zdw2!(4St&jXQ(3JEHQBYJRXC5fRzF#VJ<B&6VrvD!K?d6
zULAX~7-A34Qlo+8?KYAD;j}X8fT;cg*Ao*(+HXTF&>;sNTgCA_5QVn~rk2|1(xz%{
zQ!Lg&G$(^D(gF$^ddLC1F<L;tNv#LtWR{r@5^f@NH{E!G*xLIY6QlzcK3cuI_+Cu<
z>hhl9umn29R}<UB3;_YBS*}S|K=0a^mLlBPFvuu5;0OGrYq^)kpf{uqX3awpn8ssK
zLL!bm?PM8g=iYQoABRHJ1dOI;^)M1({N4nrhkKlVxhbz;wN^H~ivl}Mw2}i|{Vv^I
zLy=nZ9dZk?HB<0u3^vOKtBFx*()1xSi%OV<H~kOsGPsIDR3^xxGFj^!tZ5`O3A?IV
z6|$_%INqGWo3p5!$qW#(WR%pp2t>2EXwHE6*a6ZZY+%s)JI^LL?ZK90Ou^b6g)VX=
z+>=W#fTzD5?0boxC)u23gv&XKQlf$eAnvJJ#(d^)V4~qK%(rN?zoK<8N6dmrj-`;m
zFH_oHLMxInulm1nL&lD53)m>u27~_wlp7`p>%|QWl({1K9X^WqWR^G$Od93~UT26U
zUg3YsyfantL%$&>bmE2wErS_fF=pfi=p{|VZ0DFw7|f04Lp_HC=ka|07??6NJ5$b+
zvvZZT+XI;b)B3l#3n*o>LNuU2A#Yp2gE+C2=skRD?D|a#&Ien-rVz&;FsR2ypOPG%
z2?=-L#F&q8G}nTm5nte_ZoTWZIHTu#sa+*@%~2qw1=(!npfL~3`01}CUWu0;q&LRH
zVZ7uev77loDULBsN{cY2mG}z!o5i()E08FRDw7@{HUl+>nk1pc3LcQpS&l&f$&-HE
z^aFIQS*IFev1z>X-_sQ2`vtt%sMWiasw(Ge>mMK}w%y>vdffiMXl*wGDy>>c-^E`J
z&olu>l;=){Zd5|Q88eP33vV2iHdM$UgML@2-%TohME$IF?yo<hnkA|!#>O{fUive<
z{2N>>7BN!ERP$x5{36zV5nh@?w=vwE1#;kP`i92~O9h<31qnHL2SsXbe;~w9g;<C6
zAzH|Z-)Eb=1ARr14Au|pGyt+vD=eqOo!Bq>v=jS<pe)%h^5Mvf^Oj|kEn`NQ>>EQa
z0qraIE#_^s@?_u6SiLj_Q{Uk<>cGaC4(Vd)o2la@jX>YN!Qc37zE2w3Li{ZGP`Y8j
zL1;Anrx~$JX<)0DpABx#2E02+A!h2=BWpXd_OR-Q7}D$-h1xGF9}se4H+Me#SDKMT
zUlzS9sh$n5x8FXpDm5$0ZTXHDBQC;zdn2ATo{8i6f*ItWX#y4s)MCc!jSo#y8y(6l
zM#W|mnR}!=i*edD8Uw!w04GgD9F1O*M`4O+6#gDPk|yc~73WSCoSytO)O~=9)34Rh
zLS{5GJ~r`ob+%d<e?!yT7OwP1Qn`gI_!@;enXNSmL+B&C0gi>Bl~6>w2OOuTJ-A7Z
zc_@jPrVLS;9O+Q{a15I_9#s>$&<TT02Nbtc{Q*pZF1fzHjT9JX(PSH~AQ4+9X?%_o
zO+<>9h!=GLY~zTv-<A?`;l3{^NmrM5adH!y;0stVA6s`#?mJ*XK}aP+MLt*n$a~$+
z8sbVoJ_80KDhjdy|BTuFw?v=nas^bxYrCL*NTh3c3kG}bcQ-cKWyC9`skpysr6fGL
zmh%{6!Aw&#2TW$NcxVTyeQBROFe0oTIy6GaNBKMo*m%W!x<t4SVv#ltoeq*-`<n=6
z2G)Vq#@=OxCmRYsX&p>4TSJFkV4Ep{)UjaT^xzMB2*kme5ymM^z07fniC|OiK`@Y9
zL4XHf7Hr-47=4L&Kgb1;3YCOeU=MVMDB^9(4I+xbqsa>PF_THl0A_+nz+r7u2x2;;
zn+M{8@u1Jh5cZC4L(4c_oKRqRZWbm!e8B6n%29R#tZWAxhAeHYLto8>PUxz+XLaLe
z=jUu`YGUnz>ZGn7DU<DfD1QPIbC3aJVjVXzCcfFnM8LkGX7~_Fb1_Yd@zB7a73Epv
zfO2x&dX<VL6ad8ux@VkVba;$M3uc#~;liLnBHaj22_@|`n|&DX!{RdhC;4~;<F3<E
z3?|XYKqn5Y4P{3L7}?HGm5Jf8vMaO@S`7k!3RO}t<rVVcY`pzXTqZAC8V3bO(3`1d
zX5duEGrbG1T5PIYB+68`{s<5MZ@TrmICzcJizz>=FVb;fs?b|&SA+5!t-E?hxkya0
z1sO3&8z&$w2jM7As-4gP2dQAHBLvZ@jx69I){)D}k*W|g!y00V)MIiM2bw@T0&hkd
z5?KeQtH#8yP>r@i0_N4K5r=O-#_*|Pq+Ny-L$6V*-$YT%^%bL02600d$S2CtRIzp&
z9cB%IgAzqrBWbkAr5a)x4FT~Q){%4xFX;#w2y!2+BZgW*?|?!;3NkrR4}?BAz^<Dx
z<uY3&a?u<Onb&AX7CB2SZe@2CyWGc@w}y_Z38t~FIQa*bAf`9FUth(UZgxmKgF@*W
z^MJP}S<pEtB|UAN12>y9Bowkuk!6zsBb~tMY;R`BWcoc?WGD0^)vDrRdHe!5uo-+<
zjK`Pzc#KLSWKd*4G9Cz$-;E)e`XF9Jzy{d;jP5r~76@5$9`5E7Zvv?w<mHh*+NMkm
zr~8o$(!a;@21no_N#t)BOPXO}1MDl*0>+di$$w@fUCZ|VZbc!PN`jpWVh5P!M+QI~
zt=Ktp#s>d<fE+LrbL<6tneWn}pMpnU#V+;zC=LYe6u}d3@rtAkr~+oTCC=qHkQg0q
zl@4RrUEdYxMTmU4He#ZuALAi4AEa50#SQ4kC)9w=r$l!YXz5=^aYAQWR7?9gq%(RG
zb#IZXT*Qqik6V+Bu;W%4iH;EjF8K^0?J0JYt|f&f48yuZ^8~~zp#xBwG@!x0Q}|;3
zU%_5oO5*H<JrGZWykmFT2fS=NG8t!Z8AS|u7fx}CCmo>yxj{Gh2;GOJKu+*uk^&(s
zLW0>7Mgm<5PV;`iP7*$n%_;<XUU_-oC`WxiN=6phe&UGuZk^t~BtH7sfChv3erf{1
zgv`L<BTM@{l_Lr9?L>%7M~J8nh|ruCL##HP3%Jam`(mZ9r6_NbW|Symu};T+1k=IH
zW?*E4LS^B_euI(t#J4jllRSTvd^O}be?p#i&?Ihtx=3+!igz0Bi^vLz!OR!mGtd>|
z5k5(#fMCy^HpOzSG8ATM%B*Xf3UVD9Uo6-Nct-8g<GGUse-Q;kzr@9mTCIYEOjjqW
zqt%PmJkIT~*7IiM?^EBWC=kz4q-lrdRDv+oBrz!{G?`4Y*Jw13_Ug?pLJwW0w-qX;
zQ8e-(q&O`o-h0qL`7nnPWUo=_e*7;6SP>Z}cr~YeLR9*|{EM0o&!THU!xNLLT6JqW
PTb(Rqr*rh*#lQSNYB63I

diff --git a/resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/aac.cpython-35.pyc
deleted file mode 100644
index c976f6f4650780c5745425975ff709a31937ca5d..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 10050
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
SUi`)48;ka0Z}9@N^#28zz&&mN

diff --git a/resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/aiff.cpython-35.pyc
deleted file mode 100644
index f9a42314d9662e22c2aa38fff07809f81db17f50..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 10216
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/apev2.cpython-35.pyc
deleted file mode 100644
index 943ab32c895bf467e60af7587acf090672c40d08..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 20902
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/easyid3.cpython-35.pyc
deleted file mode 100644
index a1031ba9cedbd41184ef7bbc08feb167385d2f96..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 17989
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
R_9DM4v$yzceW8!s{|97&yEgy;

diff --git a/resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/easymp4.cpython-35.pyc
deleted file mode 100644
index f970678fbfeb6d5c63c1baf98504447dddb60746..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 10727
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
QqHwsdw}#2fO7JZH7m7(;rT_o{

diff --git a/resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/flac.cpython-35.pyc
deleted file mode 100644
index 703d588251c17e036a3bfde0b1b41be9202d3afd..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 28214
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
Cy}z6Q

diff --git a/resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/m4a.cpython-35.pyc
deleted file mode 100644
index 43ede4e2c474159647db96d95d9be8b501954a47..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3561
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/monkeysaudio.cpython-35.pyc
deleted file mode 100644
index 8c719f0ede6e8b0bf573377a20e0fd3b1884bb06..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2904
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
W0eH@;<2yakLuvKgwZ__Dt@A&tI-YX?

diff --git a/resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/mp3.cpython-35.pyc
deleted file mode 100644
index 8a0be64316ee96d34ca96f91a8414a6ead76495b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 9514
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
RoN{S$GBf_4<41Fu{{bT;EF=H`

diff --git a/resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/musepack.cpython-35.pyc
deleted file mode 100644
index cf4ca9e5582986eebe0a4d9b7941a217ba4bd059..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8017
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/ogg.cpython-35.pyc
deleted file mode 100644
index 0eb57b0c6c9310509e2fb8e83ed55f48342796ce..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 16965
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
Q(WBPs^jvP{f%@_P0W}0OjQ{`u

diff --git a/resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggflac.cpython-35.pyc
deleted file mode 100644
index ab7dadf0f0e45087906650fa6558056a002dc4ab..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4862
zcmb7I-H+VV6+d_E@qFw|W_Oe9W+8wJ1Q;ny_$(Af6j%r#qJ^@7P&cjRweQS&cgCJv
zd-5^ev{FMU&q%0med|M2seh2ww?6G#pDR`E?;Ov^X6Z}4@tx~)zs^19cYf#IY_!|W
z@Am%r#UIy+{y`@m8})baW;I0=5qT&TMHPxH8d&6^Zc|jHVTC*h9g1o+v}k0LXOrT#
zIt{Bda>#Sa?*<KP@?ED<gS=)bZPIXsMlJGM<@X8=+cY{u-WeKo$m>YoRZ<^DEsEOo
zHPmR6UZsc7p+O%;XDI53v_rZf(ltm|DOwZh8fizQ>yWNfbXKHiNw-9L7SeOPF`ieE
z?uhgp>2u_rrwIQnZt^yGtGnHez=1VB*Foe4N#x!&ajxA@2Lt!o^>?@3FddC_l4reU
z^TvLhxuZ0i47IzDu0x%beflw1@0wsd){z@0m`5hqi}NfuIvDlb8~fVr$0p00GHWSz
z`{@KLV%ABfqn>*$HSY6CaHyl_W#a6zn`!NiCV4Q>Nv}T)!c%Hs=w5)aZrbm=Io8O=
zI*j{q802vZJ@V#$o{!(Uaz(~w>BNM(pPGU0B|5*74hH@RO2^5dx1W!Oy=GCv##zI{
z{&cL9!in@y=lYm2v~vtIQCAi9@V)!F&OZJW_2#Z@@Z+SPb}Oca4n<WPlNz+OzT7U2
z_kJAaT}$Tv3_B@ScoEp_#{IE2b&Lt|E*@Ux9lY5p3JpI2l_-Hn1#eYkzz(LX`+gFP
zwC@*9-!I*Qddv53Pl93D@)t54Od`d<UfO=^)@S$g{WN*?^;_5Dy;~U^{o*(X51~_b
zYi8wJFp3y!d|y_2k~OKIXsQ2O)na{S&5r;sZ9ZVTKy%Y#o!VTS;bNg;S>tV+H55<d
zoiEMbk64LSdWSn)M{&wZp2abZkma%}B|;fOTX0`v+#|R-2I?H*h-$7kC~CmM&0=k)
z_R_c0^AMF_pFuZ<$9u-c;vuWTPZRHXyxVxQU%`zz(UGEuiXK$x-g9(Rp#%6*(bfT+
zS>cB4wMRPL?$FKLq627do#+m|q4Do4wDqt;w`_V~(F2=~h<r?4r6bwRp+)8!%2BVe
zWCOGRPEqm#9bqb)`dD(ExYOUHQ)4Ngb!GJ&nbn~jb1bxtnhZc$9_u-Df0gn&9f5-I
zS)-fD%Vg&bI&6}8OsNNTQqwhF<EYL(pv29eV?6$$={I%yDpB;+y{i<hP)UC!-Ieq<
zsL}hse2N}4C~vYZN2sjO5!3+>!ekby+i$3wm~6Luc1P@=RblVna?nnlx8Hm3qt~)y
zmK-y{G8;wzdaymc5GQwnVH}lC90lf38~24vrfazK^vOwbn51_Tcg6?q9c?mD$Xo8E
zsCOwkX8d{WQJnaD!!$h1;;F`GaBulptYpDxJk-7kat)3ELneBdc^zg!AA#<d?Or=i
z^I$mp$_i(wlR>_Zz-C2cN8OfTEEP2lPMv2(Gs^>$XLsX#zi<G+NthSTB;jU>6bQA6
zgQ0PF*crCP61zo%za{vJTBh?L&rMMqlog=e&h)Tf)R@H6y@SFYGeuQL!9i;96DO(h
zY-R+nft5j(=;Sq6;ByQVN*6Xyc_NHCf|;J;B+h;R6?|pSp&->#TWVD~>N09))w;4!
zwyY;A7NqOysY*+2sOe)rh`!!@szs<-n2lcvwV$E_s2sq0ZHC!yt_Tr^&||5^xIq9i
zo^638go542If4Y3yIr-@t(gm$NGy;2#YyB31MK(-2#fkS1zQ3SMYTUngWPOD3MONU
zGS6`FEEh{A6B9Ij49k6Fuj~^vXH4~^8p>AZ)%4>3*yf^p$Vq@7{#|VJ96mrF<zBf|
z9M&613Omy$<^*-wz0`Z9kHi_KNrVh~DRMuW0$s8>QCpR2pHkKtkKx_?XIMj`hTxs)
zg&!(ecH8MTPtkZ$^Zh6deZO336Z43bOqXkyxp<L_<+4%}w#PY*7$F(wgO+kCj>Uhi
zt(IfBEyTNc*c|+1Jro*YInR%OK&T79f?L0V9E88%e4Eg^+pQLz*^cJ9Ih;?&xtrO7
zzq3E17=W-a9vo-Wy<O(eEI7f|!n^Q5$&-f_-WjNtfmdbTQ}_Y+#=oE(T#dR&q1qt`
z+$N!V5G}k2asx=AxkXz{^(VO&L<?$#uL0r%Fe3687@m`gQVjL3vvX}p9LKx?gt7?`
zw}bAAfVo7l!-umZ0x_k`QlKac#ACKFpk$3^p89jXsb&)pmbt=>>^{WC0_la#VeeI$
zCNlFXcX<{?QJb9<aqwyQeu&t~fQ6-xd7iKEX6#V19MC`mG_bDP>S^`N5^8hDEQ5aT
zp$5h=M{eWI?jSM{;Rt%M1}OynEZJI;Y{eC5+kz8j$@X13+3EI0x(UOxcV8CIN!EoU
z(^x*?V4J-LN%reUM}8tB2a<j7eIW_BiG>^*|5Z{aT(lxtzVs?Qp8fri85k%h<~QF8
zLXcDi2<rB?I0s;J@I0oH><@<{^>WZ78w&~^l0x=nL|~JAim@a|BV8WgRsr0gvBtOp
zbU4x0q<IsvO&&KNdX*Py^E|v3+shWl!~|$KBQ#p(a^zl2vfFJ>-@GZ81Q44W1$lrY
zf1b+mn=_63Woq_tFrVSUEkSjK@lqa`mI+&sD;W(Z<ZI?-ICf`RdHdsIcKPvt@R+My
z`!{Oc4reNTSYldm`xx51dZuqrbP{U8c?rnZ&|o<4nAcGhRk_oc$GML579M)7zDY+5
zxYvvxBcJJRV+nJwH4dT(QRMe0NhlEansS4|uz~g(=`fO7CNWglYzVKB=({uNlIJ8B
z)Q}AibeMy%DC2;kYP-yb6-Pa*Rx1tFm2g<AoVV81o2psaQq#>-Y}K31(yg9i-;!lH
zGL{6}Rpuuw5Q^nk{t~Znd#keEmgo*4Sv*W0{A3Y|lf(*CfJ`-R!BxI_dNtB@IWAWS
zF@Q@ipy%N<hY~omm};4b1-IXz1MarC(E_M7`Wj3INr(IFcI$@o+jQ;S^1dkO;d4lD
z+6g96oL;&2Vp?8DcEb57n|~P|{AQfbK%pGgB}Gcw#|o0sE%Rfv%NoE=a7QBzGLIE?
zUWBhma}zCT>V33-hc}b;)U-A8K##{Sk_9i$=Gx|-KSIG-U`|C~8+;yOzjBC2W%Jx_
z*WMA+W4c|b@MfjL$5ZIRV8)5!1N_K|qHy-**0l7eII^f_VQTdEyfAA(I5bV$3njU&
zyLuWNoGB%d7FD0Gs^WXE>fgZ0L=KRi<Ch0S#~;Nb@cJ7VQP_NPF>H~dg%fBVha-*q
zV`M($`4#}58&Pn^4*eRh(7dE_5Iv|~Qu=u-<aU1emrX1d<bw^|$UX$;fU(dH(BWV+
zz3{mnrFXQ;s<<4rZh-$t07Q1+OrFG8D4>EEZlO~NdT#m?-qM*?chj&X_@ojHhsaLC
z2sNKmBF=Pjc&^Rv!R1}Pb@Kln4Bv(25^SDA&Fjn?{X4>Vh7**xGM56mC0DJmxDFSI
ziN)UHj55E<9@rAHk)>73ns7(`YI(hXo4>M)D4a^C(yFXhR&iinRTr%d8-Fit^fopf
F>%R{d_Jsfd

diff --git a/resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggopus.cpython-35.pyc
deleted file mode 100644
index fa6e46fa18b4658ac155f038b62d93a06acf2aee..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4763
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggspeex.cpython-35.pyc
deleted file mode 100644
index 9cb1566580dc286aebb8d1aaf8e69c4600458c69..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4600
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
N;`i3-hpQ`=`9J>^?#=)J

diff --git a/resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggtheora.cpython-35.pyc
deleted file mode 100644
index a7a4e55736d3e214718eae3c2d039db2a76f609c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4781
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
Wc_iH{cs6n?dHmg2xw*3J<o*kMv;RQ=

diff --git a/resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/oggvorbis.cpython-35.pyc
deleted file mode 100644
index 9acf6dc536fafd567cf5ae3c705815bf35d5b8ea..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4602
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

diff --git a/resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/optimfrog.cpython-35.pyc
deleted file mode 100644
index 0bcbbb85864a5bceedba00e8f3b89b42776baef1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2545
zcmZuyTW=dh6h6DQ*Y?J)LurFbp$vs0T&1o^sVbGKN?VaOQk7JV5Rw&XwegPaP4{AE
z#tpHO7gGKJZ~TP*3H}6c`^p2#8_zuPo$)1U#Y%H_&UMb5+pIL3{%?E#{P1Uu=r20=
zm9byP(r*DGqAs9AkxNmD1|{lZU#6%+iA!A&J&LN7lqfAzw@eQ6YLrwc^{DGj@j4|{
zN^8`uQCg>Nol>8=J~>Y#pQ0stg9DaG*69S+m*{EKpr~o7O_H9aHbA|=Zm_IFvSz7Z
zH>nGHg?8Gl@OOu3dt~Bt<K^~K5ssrc7uqNprdk}tW+)!L`2O{pFyTOVeE&=`)*=*1
zu5}_cwpOo~ry~V4&NDGm`9OtfM{F7`(lE}%AP*A(HIw^FM&q6op#kUTI31_L45iTV
zM2fsWGuDq`c~u+?@fRbh(%6_d8;HHw_;DmN6Zc>b$C+G(hL7y$N`wi{$VOws2lHt<
zPU9q0)4T8~-OU&NIY(E;-WY;bb{G$a$)V6A*^9#j-b8R#%!OxuMWG2f5*Z|4OU0Mf
z;@i$$5oVEK3ld%^+-5u)<;uti!W-}`%|VPaIGI{^SA~{R42>DxzkU1Q;Gi=Prj2DM
zSA*MrQC|eSC~uEsR(O$2q>-nnGTJ&t?x^h)HJL@4lZCy%W0SXugW9E{ZnL+65Ed1w
zRIXGBDm~Voe{5qNOD_XRR4z&t5Jl>sK-}We!i8rtjHLRW&1}JTEeNtOl|fMWL6GLr
zIN^OG2!0xe$&~Y!)CE|z;NP_;_jg|$nqi*Zxx4#3-rLo&k+()+?^QUEdN&=T#$>je
zb20W&J)O~^@}Yf=opS*ij>X^cp6Tl>&;fYQ`)+C0fT(|T88=yld!bw52Ha^^i{|2P
z9IweA3v|{3=ktX$3Ve@@Q87wJ>ibi|P0<^MStb)LZi#H1?jiq+BA&CX>P?ZzY=D-!
zCAhff7+OQiUXB#5ijcdipX8yLY3MK=A+D%!3AI^!GL2ioQlb8AVx@Z=4E{W<3f2=W
z{WrjoL;H3HoH%ssl5yzBCG3&nS0&OvGP;k9TVeyaDbYT{Rbs|8Zoj&8<-|^|D^ZJH
zRY~>8Adnb94*lZHHGHc9`V!4Fb}MvTqT@0hSLlc+@GPv-5sc%A20gq7J~-ulOvNVM
zbEhKvIHDj^V?i7N0q_}C>C#m;RHv*?7;w`)YIdi+ymk7V?_37h-gqe{?t{%!MouxW
zN!eY4(H6kua+Vu(tvJg%r)A<j*nIZ*@#g6z78C%AV>!UGU)xp8QI`SQeo@A#jEWL6
zTlm_9%GgFMJj~E>&lKJ`;|jNh+pW#Zp;$VX*P>z*UU<{$XnRFj%cNga*_(WCzwm~(
z@w!cun=lEcs=9me9W>z4)1BKjhnSd))>C|4?$<brO%SXC(<=bvw4CeCS5CwI#<_rf
z9qR*kva$#WcSUEe*5gURAOA~Ow!AnNC?ie`>I~4KzDp~Z99xrX3qz~oMrW;mR_8Y&
z6fGk88MM`>0BuJJ-oNLwHyWupGv9$_lS=S$ayF5ye^ZgIwx^cZ0_RsX0g7r6M0qa=
z)E7+plEImIyW_Yho_p7^G`CHo=DCfQrS*U=KECfdmi`*x{G5?^e#<0L@%FiYLtLzQ
z!_hlC?Xu!n^;nrd{&NjJ!P0jCrcpZ~I>sI?Xgd=6#$^__qe~yn5Oe_$*xb@=2jy)e
z(#iX3zR{S<l_WpFr2iNqiZ8^2nhS{svih8<eAjb<u(eUqJv>j=FIFPA;a+hjtwsJi
zmcP}mz87N~sG<^ZB8mp;AFpyMG2<h}fjW20S-d$U>sZ$7hU2;Imbw9)^M&Ud`MNsA
z_~8wWy+u)-JrAnNyygt+gd_QSP1)wIgJEAc+iTV_8-3+6<9`xg)E-P<t%n~%LSF>%
U+!k)W>?}K<lvc_s*H--h0H%^zDF6Tf

diff --git a/resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/trueaudio.cpython-35.pyc
deleted file mode 100644
index 629b654b9c5709ab5e164296d91a430a126fcd4a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 2924
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
F_AgA^o~Hl+

diff --git a/resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc b/resources/lib/libraries/mutagen/__pycache__/wavpack.cpython-35.pyc
deleted file mode 100644
index 2b9be21492c00f302d9b204dced56a8dc8852b9b..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 3738
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
DmOdQc

diff --git a/resources/lib/libraries/mutagen/_compat.py b/resources/lib/libraries/mutagen/_compat.py
deleted file mode 100644
index 77c465f1..00000000
--- a/resources/lib/libraries/mutagen/_compat.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2013  Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-import sys
-
-
-PY2 = sys.version_info[0] == 2
-PY3 = not PY2
-
-if PY2:
-    from StringIO import StringIO
-    BytesIO = StringIO
-    from cStringIO import StringIO as cBytesIO
-    from itertools import izip
-
-    long_ = long
-    integer_types = (int, long)
-    string_types = (str, unicode)
-    text_type = unicode
-
-    xrange = xrange
-    cmp = cmp
-    chr_ = chr
-
-    def endswith(text, end):
-        return text.endswith(end)
-
-    iteritems = lambda d: d.iteritems()
-    itervalues = lambda d: d.itervalues()
-    iterkeys = lambda d: d.iterkeys()
-
-    iterbytes = lambda b: iter(b)
-
-    exec("def reraise(tp, value, tb):\n raise tp, value, tb")
-
-    def swap_to_string(cls):
-        if "__str__" in cls.__dict__:
-            cls.__unicode__ = cls.__str__
-
-        if "__bytes__" in cls.__dict__:
-            cls.__str__ = cls.__bytes__
-
-        return cls
-
-elif PY3:
-    from io import StringIO
-    StringIO = StringIO
-    from io import BytesIO
-    cBytesIO = BytesIO
-
-    long_ = int
-    integer_types = (int,)
-    string_types = (str,)
-    text_type = str
-
-    izip = zip
-    xrange = range
-    cmp = lambda a, b: (a > b) - (a < b)
-    chr_ = lambda x: bytes([x])
-
-    def endswith(text, end):
-        # usefull for paths which can be both, str and bytes
-        if isinstance(text, str):
-            if not isinstance(end, str):
-                end = end.decode("ascii")
-        else:
-            if not isinstance(end, bytes):
-                end = end.encode("ascii")
-        return text.endswith(end)
-
-    iteritems = lambda d: iter(d.items())
-    itervalues = lambda d: iter(d.values())
-    iterkeys = lambda d: iter(d.keys())
-
-    iterbytes = lambda b: (bytes([v]) for v in b)
-
-    def reraise(tp, value, tb):
-        raise tp(value).with_traceback(tb)
-
-    def swap_to_string(cls):
-        return cls
diff --git a/resources/lib/libraries/mutagen/_constants.py b/resources/lib/libraries/mutagen/_constants.py
deleted file mode 100644
index 62c1ce02..00000000
--- a/resources/lib/libraries/mutagen/_constants.py
+++ /dev/null
@@ -1,199 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""Constants used by Mutagen."""
-
-GENRES = [
-    u"Blues",
-    u"Classic Rock",
-    u"Country",
-    u"Dance",
-    u"Disco",
-    u"Funk",
-    u"Grunge",
-    u"Hip-Hop",
-    u"Jazz",
-    u"Metal",
-    u"New Age",
-    u"Oldies",
-    u"Other",
-    u"Pop",
-    u"R&B",
-    u"Rap",
-    u"Reggae",
-    u"Rock",
-    u"Techno",
-    u"Industrial",
-    u"Alternative",
-    u"Ska",
-    u"Death Metal",
-    u"Pranks",
-    u"Soundtrack",
-    u"Euro-Techno",
-    u"Ambient",
-    u"Trip-Hop",
-    u"Vocal",
-    u"Jazz+Funk",
-    u"Fusion",
-    u"Trance",
-    u"Classical",
-    u"Instrumental",
-    u"Acid",
-    u"House",
-    u"Game",
-    u"Sound Clip",
-    u"Gospel",
-    u"Noise",
-    u"Alt. Rock",
-    u"Bass",
-    u"Soul",
-    u"Punk",
-    u"Space",
-    u"Meditative",
-    u"Instrumental Pop",
-    u"Instrumental Rock",
-    u"Ethnic",
-    u"Gothic",
-    u"Darkwave",
-    u"Techno-Industrial",
-    u"Electronic",
-    u"Pop-Folk",
-    u"Eurodance",
-    u"Dream",
-    u"Southern Rock",
-    u"Comedy",
-    u"Cult",
-    u"Gangsta Rap",
-    u"Top 40",
-    u"Christian Rap",
-    u"Pop/Funk",
-    u"Jungle",
-    u"Native American",
-    u"Cabaret",
-    u"New Wave",
-    u"Psychedelic",
-    u"Rave",
-    u"Showtunes",
-    u"Trailer",
-    u"Lo-Fi",
-    u"Tribal",
-    u"Acid Punk",
-    u"Acid Jazz",
-    u"Polka",
-    u"Retro",
-    u"Musical",
-    u"Rock & Roll",
-    u"Hard Rock",
-    u"Folk",
-    u"Folk-Rock",
-    u"National Folk",
-    u"Swing",
-    u"Fast-Fusion",
-    u"Bebop",
-    u"Latin",
-    u"Revival",
-    u"Celtic",
-    u"Bluegrass",
-    u"Avantgarde",
-    u"Gothic Rock",
-    u"Progressive Rock",
-    u"Psychedelic Rock",
-    u"Symphonic Rock",
-    u"Slow Rock",
-    u"Big Band",
-    u"Chorus",
-    u"Easy Listening",
-    u"Acoustic",
-    u"Humour",
-    u"Speech",
-    u"Chanson",
-    u"Opera",
-    u"Chamber Music",
-    u"Sonata",
-    u"Symphony",
-    u"Booty Bass",
-    u"Primus",
-    u"Porn Groove",
-    u"Satire",
-    u"Slow Jam",
-    u"Club",
-    u"Tango",
-    u"Samba",
-    u"Folklore",
-    u"Ballad",
-    u"Power Ballad",
-    u"Rhythmic Soul",
-    u"Freestyle",
-    u"Duet",
-    u"Punk Rock",
-    u"Drum Solo",
-    u"A Cappella",
-    u"Euro-House",
-    u"Dance Hall",
-    u"Goa",
-    u"Drum & Bass",
-    u"Club-House",
-    u"Hardcore",
-    u"Terror",
-    u"Indie",
-    u"BritPop",
-    u"Afro-Punk",
-    u"Polsk Punk",
-    u"Beat",
-    u"Christian Gangsta Rap",
-    u"Heavy Metal",
-    u"Black Metal",
-    u"Crossover",
-    u"Contemporary Christian",
-    u"Christian Rock",
-    u"Merengue",
-    u"Salsa",
-    u"Thrash Metal",
-    u"Anime",
-    u"JPop",
-    u"Synthpop",
-    u"Abstract",
-    u"Art Rock",
-    u"Baroque",
-    u"Bhangra",
-    u"Big Beat",
-    u"Breakbeat",
-    u"Chillout",
-    u"Downtempo",
-    u"Dub",
-    u"EBM",
-    u"Eclectic",
-    u"Electro",
-    u"Electroclash",
-    u"Emo",
-    u"Experimental",
-    u"Garage",
-    u"Global",
-    u"IDM",
-    u"Illbient",
-    u"Industro-Goth",
-    u"Jam Band",
-    u"Krautrock",
-    u"Leftfield",
-    u"Lounge",
-    u"Math Rock",
-    u"New Romantic",
-    u"Nu-Breakz",
-    u"Post-Punk",
-    u"Post-Rock",
-    u"Psytrance",
-    u"Shoegaze",
-    u"Space Rock",
-    u"Trop Rock",
-    u"World Music",
-    u"Neoclassical",
-    u"Audiobook",
-    u"Audio Theatre",
-    u"Neue Deutsche Welle",
-    u"Podcast",
-    u"Indie Rock",
-    u"G-Funk",
-    u"Dubstep",
-    u"Garage Rock",
-    u"Psybient",
-]
-"""The ID3v1 genre list."""
diff --git a/resources/lib/libraries/mutagen/_file.py b/resources/lib/libraries/mutagen/_file.py
deleted file mode 100644
index 5daa2521..00000000
--- a/resources/lib/libraries/mutagen/_file.py
+++ /dev/null
@@ -1,253 +0,0 @@
-# Copyright (C) 2005  Michael Urman
-# -*- coding: utf-8 -*-
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-import warnings
-
-from mutagen._util import DictMixin
-from mutagen._compat import izip
-
-
-class FileType(DictMixin):
-    """An abstract object wrapping tags and audio stream information.
-
-    Attributes:
-
-    * info -- stream information (length, bitrate, sample rate)
-    * tags -- metadata tags, if any
-
-    Each file format has different potential tags and stream
-    information.
-
-    FileTypes implement an interface very similar to Metadata; the
-    dict interface, save, load, and delete calls on a FileType call
-    the appropriate methods on its tag data.
-    """
-
-    __module__ = "mutagen"
-
-    info = None
-    tags = None
-    filename = None
-    _mimes = ["application/octet-stream"]
-
-    def __init__(self, filename=None, *args, **kwargs):
-        if filename is None:
-            warnings.warn("FileType constructor requires a filename",
-                          DeprecationWarning)
-        else:
-            self.load(filename, *args, **kwargs)
-
-    def load(self, filename, *args, **kwargs):
-        raise NotImplementedError
-
-    def __getitem__(self, key):
-        """Look up a metadata tag key.
-
-        If the file has no tags at all, a KeyError is raised.
-        """
-
-        if self.tags is None:
-            raise KeyError(key)
-        else:
-            return self.tags[key]
-
-    def __setitem__(self, key, value):
-        """Set a metadata tag.
-
-        If the file has no tags, an appropriate format is added (but
-        not written until save is called).
-        """
-
-        if self.tags is None:
-            self.add_tags()
-        self.tags[key] = value
-
-    def __delitem__(self, key):
-        """Delete a metadata tag key.
-
-        If the file has no tags at all, a KeyError is raised.
-        """
-
-        if self.tags is None:
-            raise KeyError(key)
-        else:
-            del(self.tags[key])
-
-    def keys(self):
-        """Return a list of keys in the metadata tag.
-
-        If the file has no tags at all, an empty list is returned.
-        """
-
-        if self.tags is None:
-            return []
-        else:
-            return self.tags.keys()
-
-    def delete(self, filename=None):
-        """Remove tags from a file.
-
-        In cases where the tagging format is independent of the file type
-        (for example `mutagen.ID3`) all traces of the tagging format will
-        be removed.
-        In cases where the tag is part of the file type, all tags and
-        padding will be removed.
-
-        The tags attribute will be cleared as well if there is one.
-
-        Does nothing if the file has no tags.
-
-        :raises mutagen.MutagenError: if deleting wasn't possible
-        """
-
-        if self.tags is not None:
-            if filename is None:
-                filename = self.filename
-            else:
-                warnings.warn(
-                    "delete(filename=...) is deprecated, reload the file",
-                    DeprecationWarning)
-            return self.tags.delete(filename)
-
-    def save(self, filename=None, **kwargs):
-        """Save metadata tags.
-
-        :raises mutagen.MutagenError: if saving wasn't possible
-        """
-
-        if filename is None:
-            filename = self.filename
-        else:
-            warnings.warn(
-                "save(filename=...) is deprecated, reload the file",
-                DeprecationWarning)
-
-        if self.tags is not None:
-            return self.tags.save(filename, **kwargs)
-
-    def pprint(self):
-        """Print stream information and comment key=value pairs."""
-
-        stream = "%s (%s)" % (self.info.pprint(), self.mime[0])
-        try:
-            tags = self.tags.pprint()
-        except AttributeError:
-            return stream
-        else:
-            return stream + ((tags and "\n" + tags) or "")
-
-    def add_tags(self):
-        """Adds new tags to the file.
-
-        :raises mutagen.MutagenError: if tags already exist or adding is not
-            possible.
-        """
-
-        raise NotImplementedError
-
-    @property
-    def mime(self):
-        """A list of mime types"""
-
-        mimes = []
-        for Kind in type(self).__mro__:
-            for mime in getattr(Kind, '_mimes', []):
-                if mime not in mimes:
-                    mimes.append(mime)
-        return mimes
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        raise NotImplementedError
-
-
-class StreamInfo(object):
-    """Abstract stream information object.
-
-    Provides attributes for length, bitrate, sample rate etc.
-
-    See the implementations for details.
-    """
-
-    __module__ = "mutagen"
-
-    def pprint(self):
-        """Print stream information"""
-
-        raise NotImplementedError
-
-
-def File(filename, options=None, easy=False):
-    """Guess the type of the file and try to open it.
-
-    The file type is decided by several things, such as the first 128
-    bytes (which usually contains a file type identifier), the
-    filename extension, and the presence of existing tags.
-
-    If no appropriate type could be found, None is returned.
-
-    :param options: Sequence of :class:`FileType` implementations, defaults to
-                    all included ones.
-
-    :param easy: If the easy wrappers should be returnd if available.
-                 For example :class:`EasyMP3 <mp3.EasyMP3>` instead
-                 of :class:`MP3 <mp3.MP3>`.
-    """
-
-    if options is None:
-        from mutagen.asf import ASF
-        from mutagen.apev2 import APEv2File
-        from mutagen.flac import FLAC
-        if easy:
-            from mutagen.easyid3 import EasyID3FileType as ID3FileType
-        else:
-            from mutagen.id3 import ID3FileType
-        if easy:
-            from mutagen.mp3 import EasyMP3 as MP3
-        else:
-            from mutagen.mp3 import MP3
-        from mutagen.oggflac import OggFLAC
-        from mutagen.oggspeex import OggSpeex
-        from mutagen.oggtheora import OggTheora
-        from mutagen.oggvorbis import OggVorbis
-        from mutagen.oggopus import OggOpus
-        if easy:
-            from mutagen.trueaudio import EasyTrueAudio as TrueAudio
-        else:
-            from mutagen.trueaudio import TrueAudio
-        from mutagen.wavpack import WavPack
-        if easy:
-            from mutagen.easymp4 import EasyMP4 as MP4
-        else:
-            from mutagen.mp4 import MP4
-        from mutagen.musepack import Musepack
-        from mutagen.monkeysaudio import MonkeysAudio
-        from mutagen.optimfrog import OptimFROG
-        from mutagen.aiff import AIFF
-        from mutagen.aac import AAC
-        options = [MP3, TrueAudio, OggTheora, OggSpeex, OggVorbis, OggFLAC,
-                   FLAC, AIFF, APEv2File, MP4, ID3FileType, WavPack,
-                   Musepack, MonkeysAudio, OptimFROG, ASF, OggOpus, AAC]
-
-    if not options:
-        return None
-
-    with open(filename, "rb") as fileobj:
-        header = fileobj.read(128)
-        # Sort by name after score. Otherwise import order affects
-        # Kind sort order, which affects treatment of things with
-        # equals scores.
-        results = [(Kind.score(filename, fileobj, header), Kind.__name__)
-                   for Kind in options]
-
-    results = list(izip(results, options))
-    results.sort()
-    (score, name), Kind = results[-1]
-    if score > 0:
-        return Kind(filename)
-    else:
-        return None
diff --git a/resources/lib/libraries/mutagen/_mp3util.py b/resources/lib/libraries/mutagen/_mp3util.py
deleted file mode 100644
index 409cadcb..00000000
--- a/resources/lib/libraries/mutagen/_mp3util.py
+++ /dev/null
@@ -1,420 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2015 Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""
-http://www.codeproject.com/Articles/8295/MPEG-Audio-Frame-Header
-http://wiki.hydrogenaud.io/index.php?title=MP3
-"""
-
-from functools import partial
-
-from ._util import cdata, BitReader
-from ._compat import xrange, iterbytes, cBytesIO
-
-
-class LAMEError(Exception):
-    pass
-
-
-class LAMEHeader(object):
-    """http://gabriel.mp3-tech.org/mp3infotag.html"""
-
-    vbr_method = 0
-    """0: unknown, 1: CBR, 2: ABR, 3/4/5: VBR, others: see the docs"""
-
-    lowpass_filter = 0
-    """lowpass filter value in Hz. 0 means unknown"""
-
-    quality = -1
-    """Encoding quality: 0..9"""
-
-    vbr_quality = -1
-    """VBR quality: 0..9"""
-
-    track_peak = None
-    """Peak signal amplitude as float. None if unknown."""
-
-    track_gain_origin = 0
-    """see the docs"""
-
-    track_gain_adjustment = None
-    """Track gain adjustment as float (for 89db replay gain) or None"""
-
-    album_gain_origin = 0
-    """see the docs"""
-
-    album_gain_adjustment = None
-    """Album gain adjustment as float (for 89db replay gain) or None"""
-
-    encoding_flags = 0
-    """see docs"""
-
-    ath_type = -1
-    """see docs"""
-
-    bitrate = -1
-    """Bitrate in kbps. For VBR the minimum bitrate, for anything else
-    (CBR, ABR, ..) the target bitrate.
-    """
-
-    encoder_delay_start = 0
-    """Encoder delay in samples"""
-
-    encoder_padding_end = 0
-    """Padding in samples added at the end"""
-
-    source_sample_frequency_enum = -1
-    """see docs"""
-
-    unwise_setting_used = False
-    """see docs"""
-
-    stereo_mode = 0
-    """see docs"""
-
-    noise_shaping = 0
-    """see docs"""
-
-    mp3_gain = 0
-    """Applied MP3 gain -127..127. Factor is 2 ** (mp3_gain / 4)"""
-
-    surround_info = 0
-    """see docs"""
-
-    preset_used = 0
-    """lame preset"""
-
-    music_length = 0
-    """Length in bytes excluding any ID3 tags"""
-
-    music_crc = -1
-    """CRC16 of the data specified by music_length"""
-
-    header_crc = -1
-    """CRC16 of this header and everything before (not checked)"""
-
-    def __init__(self, xing, fileobj):
-        """Raises LAMEError if parsing fails"""
-
-        payload = fileobj.read(27)
-        if len(payload) != 27:
-            raise LAMEError("Not enough data")
-
-        # extended lame header
-        r = BitReader(cBytesIO(payload))
-        revision = r.bits(4)
-        if revision != 0:
-            raise LAMEError("unsupported header revision %d" % revision)
-
-        self.vbr_method = r.bits(4)
-        self.lowpass_filter = r.bits(8) * 100
-
-        # these have a different meaning for lame; expose them again here
-        self.quality = (100 - xing.vbr_scale) % 10
-        self.vbr_quality = (100 - xing.vbr_scale) // 10
-
-        track_peak_data = r.bytes(4)
-        if track_peak_data == b"\x00\x00\x00\x00":
-            self.track_peak = None
-        else:
-            # see PutLameVBR() in LAME's VbrTag.c
-            self.track_peak = (
-                cdata.uint32_be(track_peak_data) - 0.5) / 2 ** 23
-        track_gain_type = r.bits(3)
-        self.track_gain_origin = r.bits(3)
-        sign = r.bits(1)
-        gain_adj = r.bits(9) / 10.0
-        if sign:
-            gain_adj *= -1
-        if track_gain_type == 1:
-            self.track_gain_adjustment = gain_adj
-        else:
-            self.track_gain_adjustment = None
-        assert r.is_aligned()
-
-        album_gain_type = r.bits(3)
-        self.album_gain_origin = r.bits(3)
-        sign = r.bits(1)
-        album_gain_adj = r.bits(9) / 10.0
-        if album_gain_type == 2:
-            self.album_gain_adjustment = album_gain_adj
-        else:
-            self.album_gain_adjustment = None
-
-        self.encoding_flags = r.bits(4)
-        self.ath_type = r.bits(4)
-
-        self.bitrate = r.bits(8)
-
-        self.encoder_delay_start = r.bits(12)
-        self.encoder_padding_end = r.bits(12)
-
-        self.source_sample_frequency_enum = r.bits(2)
-        self.unwise_setting_used = r.bits(1)
-        self.stereo_mode = r.bits(3)
-        self.noise_shaping = r.bits(2)
-
-        sign = r.bits(1)
-        mp3_gain = r.bits(7)
-        if sign:
-            mp3_gain *= -1
-        self.mp3_gain = mp3_gain
-
-        r.skip(2)
-        self.surround_info = r.bits(3)
-        self.preset_used = r.bits(11)
-        self.music_length = r.bits(32)
-        self.music_crc = r.bits(16)
-
-        self.header_crc = r.bits(16)
-        assert r.is_aligned()
-
-    @classmethod
-    def parse_version(cls, fileobj):
-        """Returns a version string and True if a LAMEHeader follows.
-        The passed file object will be positioned right before the
-        lame header if True.
-
-        Raises LAMEError if there is no lame version info.
-        """
-
-        # http://wiki.hydrogenaud.io/index.php?title=LAME_version_string
-
-        data = fileobj.read(20)
-        if len(data) != 20:
-            raise LAMEError("Not a lame header")
-        if not data.startswith((b"LAME", b"L3.99")):
-            raise LAMEError("Not a lame header")
-
-        data = data.lstrip(b"EMAL")
-        major, data = data[0:1], data[1:].lstrip(b".")
-        minor = b""
-        for c in iterbytes(data):
-            if not c.isdigit():
-                break
-            minor += c
-        data = data[len(minor):]
-
-        try:
-            major = int(major.decode("ascii"))
-            minor = int(minor.decode("ascii"))
-        except ValueError:
-            raise LAMEError
-
-        # the extended header was added sometimes in the 3.90 cycle
-        # e.g. "LAME3.90 (alpha)" should still stop here.
-        # (I have seen such a file)
-        if (major, minor) < (3, 90) or (
-                (major, minor) == (3, 90) and data[-11:-10] == b"("):
-            flag = data.strip(b"\x00").rstrip().decode("ascii")
-            return u"%d.%d%s" % (major, minor, flag), False
-
-        if len(data) <= 11:
-            raise LAMEError("Invalid version: too long")
-
-        flag = data[:-11].rstrip(b"\x00")
-
-        flag_string = u""
-        patch = u""
-        if flag == b"a":
-            flag_string = u" (alpha)"
-        elif flag == b"b":
-            flag_string = u" (beta)"
-        elif flag == b"r":
-            patch = u".1+"
-        elif flag == b" ":
-            if (major, minor) > (3, 96):
-                patch = u".0"
-            else:
-                patch = u".0+"
-        elif flag == b"" or flag == b".":
-            patch = u".0+"
-        else:
-            flag_string = u" (?)"
-
-        # extended header, seek back to 9 bytes for the caller
-        fileobj.seek(-11, 1)
-
-        return u"%d.%d%s%s" % (major, minor, patch, flag_string), True
-
-
-class XingHeaderError(Exception):
-    pass
-
-
-class XingHeaderFlags(object):
-    FRAMES = 0x1
-    BYTES = 0x2
-    TOC = 0x4
-    VBR_SCALE = 0x8
-
-
-class XingHeader(object):
-
-    frames = -1
-    """Number of frames, -1 if unknown"""
-
-    bytes = -1
-    """Number of bytes, -1 if unknown"""
-
-    toc = []
-    """List of 100 file offsets in percent encoded as 0-255. E.g. entry
-    50 contains the file offset in percent at 50% play time.
-    Empty if unknown.
-    """
-
-    vbr_scale = -1
-    """VBR quality indicator 0-100. -1 if unknown"""
-
-    lame_header = None
-    """A LAMEHeader instance or None"""
-
-    lame_version = u""
-    """The version of the LAME encoder e.g. '3.99.0'. Empty if unknown"""
-
-    is_info = False
-    """If the header started with 'Info' and not 'Xing'"""
-
-    def __init__(self, fileobj):
-        """Parses the Xing header or raises XingHeaderError.
-
-        The file position after this returns is undefined.
-        """
-
-        data = fileobj.read(8)
-        if len(data) != 8 or data[:4] not in (b"Xing", b"Info"):
-            raise XingHeaderError("Not a Xing header")
-
-        self.is_info = (data[:4] == b"Info")
-
-        flags = cdata.uint32_be_from(data, 4)[0]
-
-        if flags & XingHeaderFlags.FRAMES:
-            data = fileobj.read(4)
-            if len(data) != 4:
-                raise XingHeaderError("Xing header truncated")
-            self.frames = cdata.uint32_be(data)
-
-        if flags & XingHeaderFlags.BYTES:
-            data = fileobj.read(4)
-            if len(data) != 4:
-                raise XingHeaderError("Xing header truncated")
-            self.bytes = cdata.uint32_be(data)
-
-        if flags & XingHeaderFlags.TOC:
-            data = fileobj.read(100)
-            if len(data) != 100:
-                raise XingHeaderError("Xing header truncated")
-            self.toc = list(bytearray(data))
-
-        if flags & XingHeaderFlags.VBR_SCALE:
-            data = fileobj.read(4)
-            if len(data) != 4:
-                raise XingHeaderError("Xing header truncated")
-            self.vbr_scale = cdata.uint32_be(data)
-
-        try:
-            self.lame_version, has_header = LAMEHeader.parse_version(fileobj)
-            if has_header:
-                self.lame_header = LAMEHeader(self, fileobj)
-        except LAMEError:
-            pass
-
-    @classmethod
-    def get_offset(cls, info):
-        """Calculate the offset to the Xing header from the start of the
-        MPEG header including sync based on the MPEG header's content.
-        """
-
-        assert info.layer == 3
-
-        if info.version == 1:
-            if info.mode != 3:
-                return 36
-            else:
-                return 21
-        else:
-            if info.mode != 3:
-                return 21
-            else:
-                return 13
-
-
-class VBRIHeaderError(Exception):
-    pass
-
-
-class VBRIHeader(object):
-
-    version = 0
-    """VBRI header version"""
-
-    quality = 0
-    """Quality indicator"""
-
-    bytes = 0
-    """Number of bytes"""
-
-    frames = 0
-    """Number of frames"""
-
-    toc_scale_factor = 0
-    """Scale factor of TOC entries"""
-
-    toc_frames = 0
-    """Number of frames per table entry"""
-
-    toc = []
-    """TOC"""
-
-    def __init__(self, fileobj):
-        """Reads the VBRI header or raises VBRIHeaderError.
-
-        The file position is undefined after this returns
-        """
-
-        data = fileobj.read(26)
-        if len(data) != 26 or not data.startswith(b"VBRI"):
-            raise VBRIHeaderError("Not a VBRI header")
-
-        offset = 4
-        self.version, offset = cdata.uint16_be_from(data, offset)
-        if self.version != 1:
-            raise VBRIHeaderError(
-                "Unsupported header version: %r" % self.version)
-
-        offset += 2  # float16.. can't do
-        self.quality, offset = cdata.uint16_be_from(data, offset)
-        self.bytes, offset = cdata.uint32_be_from(data, offset)
-        self.frames, offset = cdata.uint32_be_from(data, offset)
-
-        toc_num_entries, offset = cdata.uint16_be_from(data, offset)
-        self.toc_scale_factor, offset = cdata.uint16_be_from(data, offset)
-        toc_entry_size, offset = cdata.uint16_be_from(data, offset)
-        self.toc_frames, offset = cdata.uint16_be_from(data, offset)
-        toc_size = toc_entry_size * toc_num_entries
-        toc_data = fileobj.read(toc_size)
-        if len(toc_data) != toc_size:
-            raise VBRIHeaderError("VBRI header truncated")
-
-        self.toc = []
-        if toc_entry_size == 2:
-            unpack = partial(cdata.uint16_be_from, toc_data)
-        elif toc_entry_size == 4:
-            unpack = partial(cdata.uint32_be_from, toc_data)
-        else:
-            raise VBRIHeaderError("Invalid TOC entry size")
-
-        self.toc = [unpack(i)[0] for i in xrange(0, toc_size, toc_entry_size)]
-
-    @classmethod
-    def get_offset(cls, info):
-        """Offset in bytes from the start of the MPEG header including sync"""
-
-        assert info.layer == 3
-
-        return 36
diff --git a/resources/lib/libraries/mutagen/_tags.py b/resources/lib/libraries/mutagen/_tags.py
deleted file mode 100644
index ce250adf..00000000
--- a/resources/lib/libraries/mutagen/_tags.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2005  Michael Urman
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-
-class PaddingInfo(object):
-    """Abstract padding information object.
-
-    This will be passed to the callback function that can be used
-    for saving tags.
-
-    ::
-
-        def my_callback(info: PaddingInfo):
-            return info.get_default_padding()
-
-    The callback should return the amount of padding to use (>= 0) based on
-    the content size and the padding of the file after saving. The actual used
-    amount of padding might vary depending on the file format (due to
-    alignment etc.)
-
-    The default implementation can be accessed using the
-    :meth:`get_default_padding` method in the callback.
-    """
-
-    padding = 0
-    """The amount of padding left after saving in bytes (can be negative if
-    more data needs to be added as padding is available)
-    """
-
-    size = 0
-    """The amount of data following the padding"""
-
-    def __init__(self, padding, size):
-        self.padding = padding
-        self.size = size
-
-    def get_default_padding(self):
-        """The default implementation which tries to select a reasonable
-        amount of padding and which might change in future versions.
-
-        :return: Amount of padding after saving
-        :rtype: int
-        """
-
-        high = 1024 * 10 + self.size // 100  # 10 KiB + 1% of trailing data
-        low = 1024 + self.size // 1000  # 1 KiB + 0.1% of trailing data
-
-        if self.padding >= 0:
-            # enough padding left
-            if self.padding > high:
-                # padding too large, reduce
-                return low
-            # just use existing padding as is
-            return self.padding
-        else:
-            # not enough padding, add some
-            return low
-
-    def _get_padding(self, user_func):
-        if user_func is None:
-            return self.get_default_padding()
-        else:
-            return user_func(self)
-
-    def __repr__(self):
-        return "<%s size=%d padding=%d>" % (
-            type(self).__name__, self.size, self.padding)
-
-
-class Metadata(object):
-    """An abstract dict-like object.
-
-    Metadata is the base class for many of the tag objects in Mutagen.
-    """
-
-    __module__ = "mutagen"
-
-    def __init__(self, *args, **kwargs):
-        if args or kwargs:
-            self.load(*args, **kwargs)
-
-    def load(self, *args, **kwargs):
-        raise NotImplementedError
-
-    def save(self, filename=None):
-        """Save changes to a file."""
-
-        raise NotImplementedError
-
-    def delete(self, filename=None):
-        """Remove tags from a file.
-
-        In most cases this means any traces of the tag will be removed
-        from the file.
-        """
-
-        raise NotImplementedError
diff --git a/resources/lib/libraries/mutagen/_toolsutil.py b/resources/lib/libraries/mutagen/_toolsutil.py
deleted file mode 100644
index e9074b71..00000000
--- a/resources/lib/libraries/mutagen/_toolsutil.py
+++ /dev/null
@@ -1,231 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2015 Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-import os
-import sys
-import signal
-import locale
-import contextlib
-import optparse
-import ctypes
-
-from ._compat import text_type, PY2, PY3, iterbytes
-
-
-def split_escape(string, sep, maxsplit=None, escape_char="\\"):
-    """Like unicode/str/bytes.split but allows for the separator to be escaped
-
-    If passed unicode/str/bytes will only return list of unicode/str/bytes.
-    """
-
-    assert len(sep) == 1
-    assert len(escape_char) == 1
-
-    if isinstance(string, bytes):
-        if isinstance(escape_char, text_type):
-            escape_char = escape_char.encode("ascii")
-        iter_ = iterbytes
-    else:
-        iter_ = iter
-
-    if maxsplit is None:
-        maxsplit = len(string)
-
-    empty = string[:0]
-    result = []
-    current = empty
-    escaped = False
-    for char in iter_(string):
-        if escaped:
-            if char != escape_char and char != sep:
-                current += escape_char
-            current += char
-            escaped = False
-        else:
-            if char == escape_char:
-                escaped = True
-            elif char == sep and len(result) < maxsplit:
-                result.append(current)
-                current = empty
-            else:
-                current += char
-    result.append(current)
-    return result
-
-
-class SignalHandler(object):
-
-    def __init__(self):
-        self._interrupted = False
-        self._nosig = False
-        self._init = False
-
-    def init(self):
-        signal.signal(signal.SIGINT, self._handler)
-        signal.signal(signal.SIGTERM, self._handler)
-        if os.name != "nt":
-            signal.signal(signal.SIGHUP, self._handler)
-
-    def _handler(self, signum, frame):
-        self._interrupted = True
-        if not self._nosig:
-            raise SystemExit("Aborted...")
-
-    @contextlib.contextmanager
-    def block(self):
-        """While this context manager is active any signals for aborting
-        the process will be queued and exit the program once the context
-        is left.
-        """
-
-        self._nosig = True
-        yield
-        self._nosig = False
-        if self._interrupted:
-            raise SystemExit("Aborted...")
-
-
-def get_win32_unicode_argv():
-    """Returns a unicode argv under Windows and standard sys.argv otherwise"""
-
-    if os.name != "nt" or not PY2:
-        return sys.argv
-
-    import ctypes
-    from ctypes import cdll, windll, wintypes
-
-    GetCommandLineW = cdll.kernel32.GetCommandLineW
-    GetCommandLineW.argtypes = []
-    GetCommandLineW.restype = wintypes.LPCWSTR
-
-    CommandLineToArgvW = windll.shell32.CommandLineToArgvW
-    CommandLineToArgvW.argtypes = [
-        wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_int)]
-    CommandLineToArgvW.restype = ctypes.POINTER(wintypes.LPWSTR)
-
-    LocalFree = windll.kernel32.LocalFree
-    LocalFree.argtypes = [wintypes.HLOCAL]
-    LocalFree.restype = wintypes.HLOCAL
-
-    argc = ctypes.c_int()
-    argv = CommandLineToArgvW(GetCommandLineW(), ctypes.byref(argc))
-    if not argv:
-        return
-
-    res = argv[max(0, argc.value - len(sys.argv)):argc.value]
-
-    LocalFree(argv)
-
-    return res
-
-
-def fsencoding():
-    """The encoding used for paths, argv, environ, stdout and stdin"""
-
-    if os.name == "nt":
-        return ""
-
-    return locale.getpreferredencoding() or "utf-8"
-
-
-def fsnative(text=u""):
-    """Returns the passed text converted to the preferred path type
-    for each platform.
-    """
-
-    assert isinstance(text, text_type)
-
-    if os.name == "nt" or PY3:
-        return text
-    else:
-        return text.encode(fsencoding(), "replace")
-    return text
-
-
-def is_fsnative(arg):
-    """If the passed value is of the preferred path type for each platform.
-    Note that on Python3+linux, paths can be bytes or str but this returns
-    False for bytes there.
-    """
-
-    if PY3 or os.name == "nt":
-        return isinstance(arg, text_type)
-    else:
-        return isinstance(arg, bytes)
-
-
-def print_(*objects, **kwargs):
-    """A print which supports bytes and str+surrogates under python3.
-
-    Needed so we can print anything passed to us through argv and environ.
-    Under Windows only text_type is allowed.
-
-    Arguments:
-        objects: one or more bytes/text
-        linesep (bool): whether a line separator should be appended
-        sep (bool): whether objects should be printed separated by spaces
-    """
-
-    linesep = kwargs.pop("linesep", True)
-    sep = kwargs.pop("sep", True)
-    file_ = kwargs.pop("file", None)
-    if file_ is None:
-        file_ = sys.stdout
-
-    old_cp = None
-    if os.name == "nt":
-        # Try to force the output to cp65001 aka utf-8.
-        # If that fails use the current one (most likely cp850, so
-        # most of unicode will be replaced with '?')
-        encoding = "utf-8"
-        old_cp = ctypes.windll.kernel32.GetConsoleOutputCP()
-        if ctypes.windll.kernel32.SetConsoleOutputCP(65001) == 0:
-            encoding = getattr(sys.stdout, "encoding", None) or "utf-8"
-            old_cp = None
-    else:
-        encoding = fsencoding()
-
-    try:
-        if linesep:
-            objects = list(objects) + [os.linesep]
-
-        parts = []
-        for text in objects:
-            if isinstance(text, text_type):
-                if PY3:
-                    try:
-                        text = text.encode(encoding, 'surrogateescape')
-                    except UnicodeEncodeError:
-                        text = text.encode(encoding, 'replace')
-                else:
-                    text = text.encode(encoding, 'replace')
-            parts.append(text)
-
-        data = (b" " if sep else b"").join(parts)
-        try:
-            fileno = file_.fileno()
-        except (AttributeError, OSError, ValueError):
-            # for tests when stdout is replaced
-            try:
-                file_.write(data)
-            except TypeError:
-                file_.write(data.decode(encoding, "replace"))
-        else:
-            file_.flush()
-            os.write(fileno, data)
-    finally:
-        # reset the code page to what we had before
-        if old_cp is not None:
-            ctypes.windll.kernel32.SetConsoleOutputCP(old_cp)
-
-
-class OptionParser(optparse.OptionParser):
-    """OptionParser subclass which supports printing Unicode under Windows"""
-
-    def print_help(self, file=None):
-        print_(self.format_help(), file=file)
diff --git a/resources/lib/libraries/mutagen/_util.py b/resources/lib/libraries/mutagen/_util.py
deleted file mode 100644
index f05ff454..00000000
--- a/resources/lib/libraries/mutagen/_util.py
+++ /dev/null
@@ -1,550 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Utility classes for Mutagen.
-
-You should not rely on the interfaces here being stable. They are
-intended for internal use in Mutagen only.
-"""
-
-import struct
-import codecs
-
-from fnmatch import fnmatchcase
-
-from ._compat import chr_, PY2, iteritems, iterbytes, integer_types, xrange, \
-    izip
-
-
-class MutagenError(Exception):
-    """Base class for all custom exceptions in mutagen
-
-    .. versionadded:: 1.25
-    """
-
-    __module__ = "mutagen"
-
-
-def total_ordering(cls):
-    assert "__eq__" in cls.__dict__
-    assert "__lt__" in cls.__dict__
-
-    cls.__le__ = lambda self, other: self == other or self < other
-    cls.__gt__ = lambda self, other: not (self == other or self < other)
-    cls.__ge__ = lambda self, other: not self < other
-    cls.__ne__ = lambda self, other: not self.__eq__(other)
-
-    return cls
-
-
-def hashable(cls):
-    """Makes sure the class is hashable.
-
-    Needs a working __eq__ and __hash__ and will add a __ne__.
-    """
-
-    # py2
-    assert "__hash__" in cls.__dict__
-    # py3
-    assert cls.__dict__["__hash__"] is not None
-    assert "__eq__" in cls.__dict__
-
-    cls.__ne__ = lambda self, other: not self.__eq__(other)
-
-    return cls
-
-
-def enum(cls):
-    assert cls.__bases__ == (object,)
-
-    d = dict(cls.__dict__)
-    new_type = type(cls.__name__, (int,), d)
-    new_type.__module__ = cls.__module__
-
-    map_ = {}
-    for key, value in iteritems(d):
-        if key.upper() == key and isinstance(value, integer_types):
-            value_instance = new_type(value)
-            setattr(new_type, key, value_instance)
-            map_[value] = key
-
-    def str_(self):
-        if self in map_:
-            return "%s.%s" % (type(self).__name__, map_[self])
-        return "%d" % int(self)
-
-    def repr_(self):
-        if self in map_:
-            return "<%s.%s: %d>" % (type(self).__name__, map_[self], int(self))
-        return "%d" % int(self)
-
-    setattr(new_type, "__repr__", repr_)
-    setattr(new_type, "__str__", str_)
-
-    return new_type
-
-
-@total_ordering
-class DictMixin(object):
-    """Implement the dict API using keys() and __*item__ methods.
-
-    Similar to UserDict.DictMixin, this takes a class that defines
-    __getitem__, __setitem__, __delitem__, and keys(), and turns it
-    into a full dict-like object.
-
-    UserDict.DictMixin is not suitable for this purpose because it's
-    an old-style class.
-
-    This class is not optimized for very large dictionaries; many
-    functions have linear memory requirements. I recommend you
-    override some of these functions if speed is required.
-    """
-
-    def __iter__(self):
-        return iter(self.keys())
-
-    def __has_key(self, key):
-        try:
-            self[key]
-        except KeyError:
-            return False
-        else:
-            return True
-
-    if PY2:
-        has_key = __has_key
-
-    __contains__ = __has_key
-
-    if PY2:
-        iterkeys = lambda self: iter(self.keys())
-
-    def values(self):
-        return [self[k] for k in self.keys()]
-
-    if PY2:
-        itervalues = lambda self: iter(self.values())
-
-    def items(self):
-        return list(izip(self.keys(), self.values()))
-
-    if PY2:
-        iteritems = lambda s: iter(s.items())
-
-    def clear(self):
-        for key in list(self.keys()):
-            self.__delitem__(key)
-
-    def pop(self, key, *args):
-        if len(args) > 1:
-            raise TypeError("pop takes at most two arguments")
-        try:
-            value = self[key]
-        except KeyError:
-            if args:
-                return args[0]
-            else:
-                raise
-        del(self[key])
-        return value
-
-    def popitem(self):
-        for key in self.keys():
-            break
-        else:
-            raise KeyError("dictionary is empty")
-        return key, self.pop(key)
-
-    def update(self, other=None, **kwargs):
-        if other is None:
-            self.update(kwargs)
-            other = {}
-
-        try:
-            for key, value in other.items():
-                self.__setitem__(key, value)
-        except AttributeError:
-            for key, value in other:
-                self[key] = value
-
-    def setdefault(self, key, default=None):
-        try:
-            return self[key]
-        except KeyError:
-            self[key] = default
-            return default
-
-    def get(self, key, default=None):
-        try:
-            return self[key]
-        except KeyError:
-            return default
-
-    def __repr__(self):
-        return repr(dict(self.items()))
-
-    def __eq__(self, other):
-        return dict(self.items()) == other
-
-    def __lt__(self, other):
-        return dict(self.items()) < other
-
-    __hash__ = object.__hash__
-
-    def __len__(self):
-        return len(self.keys())
-
-
-class DictProxy(DictMixin):
-    def __init__(self, *args, **kwargs):
-        self.__dict = {}
-        super(DictProxy, self).__init__(*args, **kwargs)
-
-    def __getitem__(self, key):
-        return self.__dict[key]
-
-    def __setitem__(self, key, value):
-        self.__dict[key] = value
-
-    def __delitem__(self, key):
-        del(self.__dict[key])
-
-    def keys(self):
-        return self.__dict.keys()
-
-
-def _fill_cdata(cls):
-    """Add struct pack/unpack functions"""
-
-    funcs = {}
-    for key, name in [("b", "char"), ("h", "short"),
-                      ("i", "int"), ("q", "longlong")]:
-        for echar, esuffix in [("<", "le"), (">", "be")]:
-            esuffix = "_" + esuffix
-            for unsigned in [True, False]:
-                s = struct.Struct(echar + (key.upper() if unsigned else key))
-                get_wrapper = lambda f: lambda *a, **k: f(*a, **k)[0]
-                unpack = get_wrapper(s.unpack)
-                unpack_from = get_wrapper(s.unpack_from)
-
-                def get_unpack_from(s):
-                    def unpack_from(data, offset=0):
-                        return s.unpack_from(data, offset)[0], offset + s.size
-                    return unpack_from
-
-                unpack_from = get_unpack_from(s)
-                pack = s.pack
-
-                prefix = "u" if unsigned else ""
-                if s.size == 1:
-                    esuffix = ""
-                bits = str(s.size * 8)
-                funcs["%s%s%s" % (prefix, name, esuffix)] = unpack
-                funcs["%sint%s%s" % (prefix, bits, esuffix)] = unpack
-                funcs["%s%s%s_from" % (prefix, name, esuffix)] = unpack_from
-                funcs["%sint%s%s_from" % (prefix, bits, esuffix)] = unpack_from
-                funcs["to_%s%s%s" % (prefix, name, esuffix)] = pack
-                funcs["to_%sint%s%s" % (prefix, bits, esuffix)] = pack
-
-    for key, func in iteritems(funcs):
-        setattr(cls, key, staticmethod(func))
-
-
-class cdata(object):
-    """C character buffer to Python numeric type conversions.
-
-    For each size/sign/endianness:
-    uint32_le(data)/to_uint32_le(num)/uint32_le_from(data, offset=0)
-    """
-
-    from struct import error
-    error = error
-
-    bitswap = b''.join(
-        chr_(sum(((val >> i) & 1) << (7 - i) for i in xrange(8)))
-        for val in xrange(256))
-
-    test_bit = staticmethod(lambda value, n: bool((value >> n) & 1))
-
-
-_fill_cdata(cdata)
-
-
-def get_size(fileobj):
-    """Returns the size of the file object. The position when passed in will
-    be preserved if no error occurs.
-
-    In case of an error raises IOError.
-    """
-
-    old_pos = fileobj.tell()
-    try:
-        fileobj.seek(0, 2)
-        return fileobj.tell()
-    finally:
-        fileobj.seek(old_pos, 0)
-
-
-def insert_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
-    """Insert size bytes of empty space starting at offset.
-
-    fobj must be an open file object, open rb+ or
-    equivalent. Mutagen tries to use mmap to resize the file, but
-    falls back to a significantly slower method if mmap fails.
-    """
-
-    assert 0 < size
-    assert 0 <= offset
-
-    fobj.seek(0, 2)
-    filesize = fobj.tell()
-    movesize = filesize - offset
-    fobj.write(b'\x00' * size)
-    fobj.flush()
-
-    try:
-        import mmap
-        file_map = mmap.mmap(fobj.fileno(), filesize + size)
-        try:
-            file_map.move(offset + size, offset, movesize)
-        finally:
-            file_map.close()
-    except (ValueError, EnvironmentError, ImportError, AttributeError):
-        # handle broken mmap scenarios, BytesIO()
-        fobj.truncate(filesize)
-
-        fobj.seek(0, 2)
-        padsize = size
-        # Don't generate an enormous string if we need to pad
-        # the file out several megs.
-        while padsize:
-            addsize = min(BUFFER_SIZE, padsize)
-            fobj.write(b"\x00" * addsize)
-            padsize -= addsize
-
-        fobj.seek(filesize, 0)
-        while movesize:
-            # At the start of this loop, fobj is pointing at the end
-            # of the data we need to move, which is of movesize length.
-            thismove = min(BUFFER_SIZE, movesize)
-            # Seek back however much we're going to read this frame.
-            fobj.seek(-thismove, 1)
-            nextpos = fobj.tell()
-            # Read it, so we're back at the end.
-            data = fobj.read(thismove)
-            # Seek back to where we need to write it.
-            fobj.seek(-thismove + size, 1)
-            # Write it.
-            fobj.write(data)
-            # And seek back to the end of the unmoved data.
-            fobj.seek(nextpos)
-            movesize -= thismove
-
-        fobj.flush()
-
-
-def delete_bytes(fobj, size, offset, BUFFER_SIZE=2 ** 16):
-    """Delete size bytes of empty space starting at offset.
-
-    fobj must be an open file object, open rb+ or
-    equivalent. Mutagen tries to use mmap to resize the file, but
-    falls back to a significantly slower method if mmap fails.
-    """
-
-    assert 0 < size
-    assert 0 <= offset
-
-    fobj.seek(0, 2)
-    filesize = fobj.tell()
-    movesize = filesize - offset - size
-    assert 0 <= movesize
-
-    if movesize > 0:
-        fobj.flush()
-        try:
-            import mmap
-            file_map = mmap.mmap(fobj.fileno(), filesize)
-            try:
-                file_map.move(offset, offset + size, movesize)
-            finally:
-                file_map.close()
-        except (ValueError, EnvironmentError, ImportError, AttributeError):
-            # handle broken mmap scenarios, BytesIO()
-            fobj.seek(offset + size)
-            buf = fobj.read(BUFFER_SIZE)
-            while buf:
-                fobj.seek(offset)
-                fobj.write(buf)
-                offset += len(buf)
-                fobj.seek(offset + size)
-                buf = fobj.read(BUFFER_SIZE)
-    fobj.truncate(filesize - size)
-    fobj.flush()
-
-
-def resize_bytes(fobj, old_size, new_size, offset):
-    """Resize an area in a file adding and deleting at the end of it.
-    Does nothing if no resizing is needed.
-    """
-
-    if new_size < old_size:
-        delete_size = old_size - new_size
-        delete_at = offset + new_size
-        delete_bytes(fobj, delete_size, delete_at)
-    elif new_size > old_size:
-        insert_size = new_size - old_size
-        insert_at = offset + old_size
-        insert_bytes(fobj, insert_size, insert_at)
-
-
-def dict_match(d, key, default=None):
-    """Like __getitem__ but works as if the keys() are all filename patterns.
-    Returns the value of any dict key that matches the passed key.
-    """
-
-    if key in d and "[" not in key:
-        return d[key]
-    else:
-        for pattern, value in iteritems(d):
-            if fnmatchcase(key, pattern):
-                return value
-    return default
-
-
-def decode_terminated(data, encoding, strict=True):
-    """Returns the decoded data until the first NULL terminator
-    and all data after it.
-
-    In case the data can't be decoded raises UnicodeError.
-    In case the encoding is not found raises LookupError.
-    In case the data isn't null terminated (even if it is encoded correctly)
-    raises ValueError except if strict is False, then the decoded string
-    will be returned anyway.
-    """
-
-    codec_info = codecs.lookup(encoding)
-
-    # normalize encoding name so we can compare by name
-    encoding = codec_info.name
-
-    # fast path
-    if encoding in ("utf-8", "iso8859-1"):
-        index = data.find(b"\x00")
-        if index == -1:
-            # make sure we raise UnicodeError first, like in the slow path
-            res = data.decode(encoding), b""
-            if strict:
-                raise ValueError("not null terminated")
-            else:
-                return res
-        return data[:index].decode(encoding), data[index + 1:]
-
-    # slow path
-    decoder = codec_info.incrementaldecoder()
-    r = []
-    for i, b in enumerate(iterbytes(data)):
-        c = decoder.decode(b)
-        if c == u"\x00":
-            return u"".join(r), data[i + 1:]
-        r.append(c)
-    else:
-        # make sure the decoder is finished
-        r.append(decoder.decode(b"", True))
-        if strict:
-            raise ValueError("not null terminated")
-        return u"".join(r), b""
-
-
-class BitReaderError(Exception):
-    pass
-
-
-class BitReader(object):
-
-    def __init__(self, fileobj):
-        self._fileobj = fileobj
-        self._buffer = 0
-        self._bits = 0
-        self._pos = fileobj.tell()
-
-    def bits(self, count):
-        """Reads `count` bits and returns an uint, MSB read first.
-
-        May raise BitReaderError if not enough data could be read or
-        IOError by the underlying file object.
-        """
-
-        if count < 0:
-            raise ValueError
-
-        if count > self._bits:
-            n_bytes = (count - self._bits + 7) // 8
-            data = self._fileobj.read(n_bytes)
-            if len(data) != n_bytes:
-                raise BitReaderError("not enough data")
-            for b in bytearray(data):
-                self._buffer = (self._buffer << 8) | b
-            self._bits += n_bytes * 8
-
-        self._bits -= count
-        value = self._buffer >> self._bits
-        self._buffer &= (1 << self._bits) - 1
-        assert self._bits < 8
-        return value
-
-    def bytes(self, count):
-        """Returns a bytearray of length `count`. Works unaligned."""
-
-        if count < 0:
-            raise ValueError
-
-        # fast path
-        if self._bits == 0:
-            data = self._fileobj.read(count)
-            if len(data) != count:
-                raise BitReaderError("not enough data")
-            return data
-
-        return bytes(bytearray(self.bits(8) for _ in xrange(count)))
-
-    def skip(self, count):
-        """Skip `count` bits.
-
-        Might raise BitReaderError if there wasn't enough data to skip,
-        but might also fail on the next bits() instead.
-        """
-
-        if count < 0:
-            raise ValueError
-
-        if count <= self._bits:
-            self.bits(count)
-        else:
-            count -= self.align()
-            n_bytes = count // 8
-            self._fileobj.seek(n_bytes, 1)
-            count -= n_bytes * 8
-            self.bits(count)
-
-    def get_position(self):
-        """Returns the amount of bits read or skipped so far"""
-
-        return (self._fileobj.tell() - self._pos) * 8 - self._bits
-
-    def align(self):
-        """Align to the next byte, returns the amount of bits skipped"""
-
-        bits = self._bits
-        self._buffer = 0
-        self._bits = 0
-        return bits
-
-    def is_aligned(self):
-        """If we are currently aligned to bytes and nothing is buffered"""
-
-        return self._bits == 0
diff --git a/resources/lib/libraries/mutagen/_vorbis.py b/resources/lib/libraries/mutagen/_vorbis.py
deleted file mode 100644
index da202400..00000000
--- a/resources/lib/libraries/mutagen/_vorbis.py
+++ /dev/null
@@ -1,330 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005-2006  Joe Wreschnig
-#                    2013  Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""Read and write Vorbis comment data.
-
-Vorbis comments are freeform key/value pairs; keys are
-case-insensitive ASCII and values are Unicode strings. A key may have
-multiple values.
-
-The specification is at http://www.xiph.org/vorbis/doc/v-comment.html.
-"""
-
-import sys
-
-import mutagen
-from ._compat import reraise, BytesIO, text_type, xrange, PY3, PY2
-from mutagen._util import DictMixin, cdata
-
-
-def is_valid_key(key):
-    """Return true if a string is a valid Vorbis comment key.
-
-    Valid Vorbis comment keys are printable ASCII between 0x20 (space)
-    and 0x7D ('}'), excluding '='.
-
-    Takes str/unicode in Python 2, unicode in Python 3
-    """
-
-    if PY3 and isinstance(key, bytes):
-        raise TypeError("needs to be str not bytes")
-
-    for c in key:
-        if c < " " or c > "}" or c == "=":
-            return False
-    else:
-        return bool(key)
-
-
-istag = is_valid_key
-
-
-class error(IOError):
-    pass
-
-
-class VorbisUnsetFrameError(error):
-    pass
-
-
-class VorbisEncodingError(error):
-    pass
-
-
-class VComment(mutagen.Metadata, list):
-    """A Vorbis comment parser, accessor, and renderer.
-
-    All comment ordering is preserved. A VComment is a list of
-    key/value pairs, and so any Python list method can be used on it.
-
-    Vorbis comments are always wrapped in something like an Ogg Vorbis
-    bitstream or a FLAC metadata block, so this loads string data or a
-    file-like object, not a filename.
-
-    Attributes:
-
-    * vendor -- the stream 'vendor' (i.e. writer); default 'Mutagen'
-    """
-
-    vendor = u"Mutagen " + mutagen.version_string
-
-    def __init__(self, data=None, *args, **kwargs):
-        self._size = 0
-        # Collect the args to pass to load, this lets child classes
-        # override just load and get equivalent magic for the
-        # constructor.
-        if data is not None:
-            if isinstance(data, bytes):
-                data = BytesIO(data)
-            elif not hasattr(data, 'read'):
-                raise TypeError("VComment requires bytes or a file-like")
-            start = data.tell()
-            self.load(data, *args, **kwargs)
-            self._size = data.tell() - start
-
-    def load(self, fileobj, errors='replace', framing=True):
-        """Parse a Vorbis comment from a file-like object.
-
-        Keyword arguments:
-
-        * errors:
-            'strict', 'replace', or 'ignore'. This affects Unicode decoding
-            and how other malformed content is interpreted.
-        * framing -- if true, fail if a framing bit is not present
-
-        Framing bits are required by the Vorbis comment specification,
-        but are not used in FLAC Vorbis comment blocks.
-        """
-
-        try:
-            vendor_length = cdata.uint_le(fileobj.read(4))
-            self.vendor = fileobj.read(vendor_length).decode('utf-8', errors)
-            count = cdata.uint_le(fileobj.read(4))
-            for i in xrange(count):
-                length = cdata.uint_le(fileobj.read(4))
-                try:
-                    string = fileobj.read(length).decode('utf-8', errors)
-                except (OverflowError, MemoryError):
-                    raise error("cannot read %d bytes, too large" % length)
-                try:
-                    tag, value = string.split('=', 1)
-                except ValueError as err:
-                    if errors == "ignore":
-                        continue
-                    elif errors == "replace":
-                        tag, value = u"unknown%d" % i, string
-                    else:
-                        reraise(VorbisEncodingError, err, sys.exc_info()[2])
-                try:
-                    tag = tag.encode('ascii', errors)
-                except UnicodeEncodeError:
-                    raise VorbisEncodingError("invalid tag name %r" % tag)
-                else:
-                    # string keys in py3k
-                    if PY3:
-                        tag = tag.decode("ascii")
-                    if is_valid_key(tag):
-                        self.append((tag, value))
-
-            if framing and not bytearray(fileobj.read(1))[0] & 0x01:
-                raise VorbisUnsetFrameError("framing bit was unset")
-        except (cdata.error, TypeError):
-            raise error("file is not a valid Vorbis comment")
-
-    def validate(self):
-        """Validate keys and values.
-
-        Check to make sure every key used is a valid Vorbis key, and
-        that every value used is a valid Unicode or UTF-8 string. If
-        any invalid keys or values are found, a ValueError is raised.
-
-        In Python 3 all keys and values have to be a string.
-        """
-
-        if not isinstance(self.vendor, text_type):
-            if PY3:
-                raise ValueError("vendor needs to be str")
-
-            try:
-                self.vendor.decode('utf-8')
-            except UnicodeDecodeError:
-                raise ValueError
-
-        for key, value in self:
-            try:
-                if not is_valid_key(key):
-                    raise ValueError
-            except TypeError:
-                raise ValueError("%r is not a valid key" % key)
-
-            if not isinstance(value, text_type):
-                if PY3:
-                    raise ValueError("%r needs to be str" % key)
-
-                try:
-                    value.decode("utf-8")
-                except:
-                    raise ValueError("%r is not a valid value" % value)
-
-        return True
-
-    def clear(self):
-        """Clear all keys from the comment."""
-
-        for i in list(self):
-            self.remove(i)
-
-    def write(self, framing=True):
-        """Return a string representation of the data.
-
-        Validation is always performed, so calling this function on
-        invalid data may raise a ValueError.
-
-        Keyword arguments:
-
-        * framing -- if true, append a framing bit (see load)
-        """
-
-        self.validate()
-
-        def _encode(value):
-            if not isinstance(value, bytes):
-                return value.encode('utf-8')
-            return value
-
-        f = BytesIO()
-        vendor = _encode(self.vendor)
-        f.write(cdata.to_uint_le(len(vendor)))
-        f.write(vendor)
-        f.write(cdata.to_uint_le(len(self)))
-        for tag, value in self:
-            tag = _encode(tag)
-            value = _encode(value)
-            comment = tag + b"=" + value
-            f.write(cdata.to_uint_le(len(comment)))
-            f.write(comment)
-        if framing:
-            f.write(b"\x01")
-        return f.getvalue()
-
-    def pprint(self):
-
-        def _decode(value):
-            if not isinstance(value, text_type):
-                return value.decode('utf-8', 'replace')
-            return value
-
-        tags = [u"%s=%s" % (_decode(k), _decode(v)) for k, v in self]
-        return u"\n".join(tags)
-
-
-class VCommentDict(VComment, DictMixin):
-    """A VComment that looks like a dictionary.
-
-    This object differs from a dictionary in two ways. First,
-    len(comment) will still return the number of values, not the
-    number of keys. Secondly, iterating through the object will
-    iterate over (key, value) pairs, not keys. Since a key may have
-    multiple values, the same value may appear multiple times while
-    iterating.
-
-    Since Vorbis comment keys are case-insensitive, all keys are
-    normalized to lowercase ASCII.
-    """
-
-    def __getitem__(self, key):
-        """A list of values for the key.
-
-        This is a copy, so comment['title'].append('a title') will not
-        work.
-        """
-
-        # PY3 only
-        if isinstance(key, slice):
-            return VComment.__getitem__(self, key)
-
-        if not is_valid_key(key):
-            raise ValueError
-
-        key = key.lower()
-
-        values = [value for (k, value) in self if k.lower() == key]
-        if not values:
-            raise KeyError(key)
-        else:
-            return values
-
-    def __delitem__(self, key):
-        """Delete all values associated with the key."""
-
-        # PY3 only
-        if isinstance(key, slice):
-            return VComment.__delitem__(self, key)
-
-        if not is_valid_key(key):
-            raise ValueError
-
-        key = key.lower()
-        to_delete = [x for x in self if x[0].lower() == key]
-        if not to_delete:
-            raise KeyError(key)
-        else:
-            for item in to_delete:
-                self.remove(item)
-
-    def __contains__(self, key):
-        """Return true if the key has any values."""
-
-        if not is_valid_key(key):
-            raise ValueError
-
-        key = key.lower()
-        for k, value in self:
-            if k.lower() == key:
-                return True
-        else:
-            return False
-
-    def __setitem__(self, key, values):
-        """Set a key's value or values.
-
-        Setting a value overwrites all old ones. The value may be a
-        list of Unicode or UTF-8 strings, or a single Unicode or UTF-8
-        string.
-        """
-
-        # PY3 only
-        if isinstance(key, slice):
-            return VComment.__setitem__(self, key, values)
-
-        if not is_valid_key(key):
-            raise ValueError
-
-        if not isinstance(values, list):
-            values = [values]
-        try:
-            del(self[key])
-        except KeyError:
-            pass
-
-        if PY2:
-            key = key.encode('ascii')
-
-        for value in values:
-            self.append((key, value))
-
-    def keys(self):
-        """Return all keys in the comment."""
-
-        return list(set([k.lower() for k, v in self]))
-
-    def as_dict(self):
-        """Return a copy of the comment data in a real dict."""
-
-        return dict([(key, self[key]) for key in self.keys()])
diff --git a/resources/lib/libraries/mutagen/aac.py b/resources/lib/libraries/mutagen/aac.py
deleted file mode 100644
index 83968a05..00000000
--- a/resources/lib/libraries/mutagen/aac.py
+++ /dev/null
@@ -1,410 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2014 Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""
-* ADTS - Audio Data Transport Stream
-* ADIF - Audio Data Interchange Format
-* See ISO/IEC 13818-7 / 14496-03
-"""
-
-from mutagen import StreamInfo
-from mutagen._file import FileType
-from mutagen._util import BitReader, BitReaderError, MutagenError
-from mutagen._compat import endswith, xrange
-
-
-_FREQS = [
-    96000, 88200, 64000, 48000,
-    44100, 32000, 24000, 22050,
-    16000, 12000, 11025, 8000,
-    7350,
-]
-
-
-class _ADTSStream(object):
-    """Represents a series of frames belonging to the same stream"""
-
-    parsed_frames = 0
-    """Number of successfully parsed frames"""
-
-    offset = 0
-    """offset in bytes at which the stream starts (the first sync word)"""
-
-    @classmethod
-    def find_stream(cls, fileobj, max_bytes):
-        """Returns a possibly valid _ADTSStream or None.
-
-        Args:
-            max_bytes (int): maximum bytes to read
-        """
-
-        r = BitReader(fileobj)
-        stream = cls(r)
-        if stream.sync(max_bytes):
-            stream.offset = (r.get_position() - 12) // 8
-            return stream
-
-    def sync(self, max_bytes):
-        """Find the next sync.
-        Returns True if found."""
-
-        # at least 2 bytes for the sync
-        max_bytes = max(max_bytes, 2)
-
-        r = self._r
-        r.align()
-        while max_bytes > 0:
-            try:
-                b = r.bytes(1)
-                if b == b"\xff":
-                    if r.bits(4) == 0xf:
-                        return True
-                    r.align()
-                    max_bytes -= 2
-                else:
-                    max_bytes -= 1
-            except BitReaderError:
-                return False
-        return False
-
-    def __init__(self, r):
-        """Use _ADTSStream.find_stream to create a stream"""
-
-        self._fixed_header_key = None
-        self._r = r
-        self.offset = -1
-        self.parsed_frames = 0
-
-        self._samples = 0
-        self._payload = 0
-        self._start = r.get_position() / 8
-        self._last = self._start
-
-    @property
-    def bitrate(self):
-        """Bitrate of the raw aac blocks, excluding framing/crc"""
-
-        assert self.parsed_frames, "no frame parsed yet"
-
-        if self._samples == 0:
-            return 0
-
-        return (8 * self._payload * self.frequency) // self._samples
-
-    @property
-    def samples(self):
-        """samples so far"""
-
-        assert self.parsed_frames, "no frame parsed yet"
-
-        return self._samples
-
-    @property
-    def size(self):
-        """bytes read in the stream so far (including framing)"""
-
-        assert self.parsed_frames, "no frame parsed yet"
-
-        return self._last - self._start
-
-    @property
-    def channels(self):
-        """0 means unknown"""
-
-        assert self.parsed_frames, "no frame parsed yet"
-
-        b_index = self._fixed_header_key[6]
-        if b_index == 7:
-            return 8
-        elif b_index > 7:
-            return 0
-        else:
-            return b_index
-
-    @property
-    def frequency(self):
-        """0 means unknown"""
-
-        assert self.parsed_frames, "no frame parsed yet"
-
-        f_index = self._fixed_header_key[4]
-        try:
-            return _FREQS[f_index]
-        except IndexError:
-            return 0
-
-    def parse_frame(self):
-        """True if parsing was successful.
-        Fails either because the frame wasn't valid or the stream ended.
-        """
-
-        try:
-            return self._parse_frame()
-        except BitReaderError:
-            return False
-
-    def _parse_frame(self):
-        r = self._r
-        # start == position of sync word
-        start = r.get_position() - 12
-
-        # adts_fixed_header
-        id_ = r.bits(1)
-        layer = r.bits(2)
-        protection_absent = r.bits(1)
-
-        profile = r.bits(2)
-        sampling_frequency_index = r.bits(4)
-        private_bit = r.bits(1)
-        # TODO: if 0 we could parse program_config_element()
-        channel_configuration = r.bits(3)
-        original_copy = r.bits(1)
-        home = r.bits(1)
-
-        # the fixed header has to be the same for every frame in the stream
-        fixed_header_key = (
-            id_, layer, protection_absent, profile, sampling_frequency_index,
-            private_bit, channel_configuration, original_copy, home,
-        )
-
-        if self._fixed_header_key is None:
-            self._fixed_header_key = fixed_header_key
-        else:
-            if self._fixed_header_key != fixed_header_key:
-                return False
-
-        # adts_variable_header
-        r.skip(2)  # copyright_identification_bit/start
-        frame_length = r.bits(13)
-        r.skip(11)  # adts_buffer_fullness
-        nordbif = r.bits(2)
-        # adts_variable_header end
-
-        crc_overhead = 0
-        if not protection_absent:
-            crc_overhead += (nordbif + 1) * 16
-            if nordbif != 0:
-                crc_overhead *= 2
-
-        left = (frame_length * 8) - (r.get_position() - start)
-        if left < 0:
-            return False
-        r.skip(left)
-        assert r.is_aligned()
-
-        self._payload += (left - crc_overhead) / 8
-        self._samples += (nordbif + 1) * 1024
-        self._last = r.get_position() / 8
-
-        self.parsed_frames += 1
-        return True
-
-
-class ProgramConfigElement(object):
-
-    element_instance_tag = None
-    object_type = None
-    sampling_frequency_index = None
-    channels = None
-
-    def __init__(self, r):
-        """Reads the program_config_element()
-
-        Raises BitReaderError
-        """
-
-        self.element_instance_tag = r.bits(4)
-        self.object_type = r.bits(2)
-        self.sampling_frequency_index = r.bits(4)
-        num_front_channel_elements = r.bits(4)
-        num_side_channel_elements = r.bits(4)
-        num_back_channel_elements = r.bits(4)
-        num_lfe_channel_elements = r.bits(2)
-        num_assoc_data_elements = r.bits(3)
-        num_valid_cc_elements = r.bits(4)
-
-        mono_mixdown_present = r.bits(1)
-        if mono_mixdown_present == 1:
-            r.skip(4)
-        stereo_mixdown_present = r.bits(1)
-        if stereo_mixdown_present == 1:
-            r.skip(4)
-        matrix_mixdown_idx_present = r.bits(1)
-        if matrix_mixdown_idx_present == 1:
-            r.skip(3)
-
-        elms = num_front_channel_elements + num_side_channel_elements + \
-            num_back_channel_elements
-        channels = 0
-        for i in xrange(elms):
-            channels += 1
-            element_is_cpe = r.bits(1)
-            if element_is_cpe:
-                channels += 1
-            r.skip(4)
-        channels += num_lfe_channel_elements
-        self.channels = channels
-
-        r.skip(4 * num_lfe_channel_elements)
-        r.skip(4 * num_assoc_data_elements)
-        r.skip(5 * num_valid_cc_elements)
-        r.align()
-        comment_field_bytes = r.bits(8)
-        r.skip(8 * comment_field_bytes)
-
-
-class AACError(MutagenError):
-    pass
-
-
-class AACInfo(StreamInfo):
-    """AAC stream information.
-
-    Attributes:
-
-    * channels -- number of audio channels
-    * length -- file length in seconds, as a float
-    * sample_rate -- audio sampling rate in Hz
-    * bitrate -- audio bitrate, in bits per second
-
-    The length of the stream is just a guess and might not be correct.
-    """
-
-    channels = 0
-    length = 0
-    sample_rate = 0
-    bitrate = 0
-
-    def __init__(self, fileobj):
-        # skip id3v2 header
-        start_offset = 0
-        header = fileobj.read(10)
-        from mutagen.id3 import BitPaddedInt
-        if header.startswith(b"ID3"):
-            size = BitPaddedInt(header[6:])
-            start_offset = size + 10
-
-        fileobj.seek(start_offset)
-        adif = fileobj.read(4)
-        if adif == b"ADIF":
-            self._parse_adif(fileobj)
-            self._type = "ADIF"
-        else:
-            self._parse_adts(fileobj, start_offset)
-            self._type = "ADTS"
-
-    def _parse_adif(self, fileobj):
-        r = BitReader(fileobj)
-        try:
-            copyright_id_present = r.bits(1)
-            if copyright_id_present:
-                r.skip(72)  # copyright_id
-            r.skip(1 + 1)  # original_copy, home
-            bitstream_type = r.bits(1)
-            self.bitrate = r.bits(23)
-            npce = r.bits(4)
-            if bitstream_type == 0:
-                r.skip(20)  # adif_buffer_fullness
-
-            pce = ProgramConfigElement(r)
-            try:
-                self.sample_rate = _FREQS[pce.sampling_frequency_index]
-            except IndexError:
-                pass
-            self.channels = pce.channels
-
-            # other pces..
-            for i in xrange(npce):
-                ProgramConfigElement(r)
-            r.align()
-        except BitReaderError as e:
-            raise AACError(e)
-
-        # use bitrate + data size to guess length
-        start = fileobj.tell()
-        fileobj.seek(0, 2)
-        length = fileobj.tell() - start
-        if self.bitrate != 0:
-            self.length = (8.0 * length) / self.bitrate
-
-    def _parse_adts(self, fileobj, start_offset):
-        max_initial_read = 512
-        max_resync_read = 10
-        max_sync_tries = 10
-
-        frames_max = 100
-        frames_needed = 3
-
-        # Try up to X times to find a sync word and read up to Y frames.
-        # If more than Z frames are valid we assume a valid stream
-        offset = start_offset
-        for i in xrange(max_sync_tries):
-            fileobj.seek(offset)
-            s = _ADTSStream.find_stream(fileobj, max_initial_read)
-            if s is None:
-                raise AACError("sync not found")
-            # start right after the last found offset
-            offset += s.offset + 1
-
-            for i in xrange(frames_max):
-                if not s.parse_frame():
-                    break
-                if not s.sync(max_resync_read):
-                    break
-
-            if s.parsed_frames >= frames_needed:
-                break
-        else:
-            raise AACError(
-                "no valid stream found (only %d frames)" % s.parsed_frames)
-
-        self.sample_rate = s.frequency
-        self.channels = s.channels
-        self.bitrate = s.bitrate
-
-        # size from stream start to end of file
-        fileobj.seek(0, 2)
-        stream_size = fileobj.tell() - (offset + s.offset)
-        # approx
-        self.length = float(s.samples * stream_size) / (s.size * s.frequency)
-
-    def pprint(self):
-        return u"AAC (%s), %d Hz, %.2f seconds, %d channel(s), %d bps" % (
-            self._type, self.sample_rate, self.length, self.channels,
-            self.bitrate)
-
-
-class AAC(FileType):
-    """Load ADTS or ADIF streams containing AAC.
-
-    Tagging is not supported.
-    Use the ID3/APEv2 classes directly instead.
-    """
-
-    _mimes = ["audio/x-aac"]
-
-    def load(self, filename):
-        self.filename = filename
-        with open(filename, "rb") as h:
-            self.info = AACInfo(h)
-
-    def add_tags(self):
-        raise AACError("doesn't support tags")
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        filename = filename.lower()
-        s = endswith(filename, ".aac") or endswith(filename, ".adts") or \
-            endswith(filename, ".adif")
-        s += b"ADIF" in header
-        return s
-
-
-Open = AAC
-error = AACError
-
-__all__ = ["AAC", "Open"]
diff --git a/resources/lib/libraries/mutagen/aiff.py b/resources/lib/libraries/mutagen/aiff.py
deleted file mode 100644
index dc580063..00000000
--- a/resources/lib/libraries/mutagen/aiff.py
+++ /dev/null
@@ -1,357 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2014  Evan Purkhiser
-#               2014  Ben Ockmore
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""AIFF audio stream information and tags."""
-
-import sys
-import struct
-from struct import pack
-
-from ._compat import endswith, text_type, reraise
-from mutagen import StreamInfo, FileType
-
-from mutagen.id3 import ID3
-from mutagen.id3._util import ID3NoHeaderError, error as ID3Error
-from mutagen._util import resize_bytes, delete_bytes, MutagenError
-
-__all__ = ["AIFF", "Open", "delete"]
-
-
-class error(MutagenError, RuntimeError):
-    pass
-
-
-class InvalidChunk(error, IOError):
-    pass
-
-
-# based on stdlib's aifc
-_HUGE_VAL = 1.79769313486231e+308
-
-
-def is_valid_chunk_id(id):
-    assert isinstance(id, text_type)
-
-    return ((len(id) <= 4) and (min(id) >= u' ') and
-            (max(id) <= u'~'))
-
-
-def read_float(data):  # 10 bytes
-    expon, himant, lomant = struct.unpack('>hLL', data)
-    sign = 1
-    if expon < 0:
-        sign = -1
-        expon = expon + 0x8000
-    if expon == himant == lomant == 0:
-        f = 0.0
-    elif expon == 0x7FFF:
-        f = _HUGE_VAL
-    else:
-        expon = expon - 16383
-        f = (himant * 0x100000000 + lomant) * pow(2.0, expon - 63)
-    return sign * f
-
-
-class IFFChunk(object):
-    """Representation of a single IFF chunk"""
-
-    # Chunk headers are 8 bytes long (4 for ID and 4 for the size)
-    HEADER_SIZE = 8
-
-    def __init__(self, fileobj, parent_chunk=None):
-        self.__fileobj = fileobj
-        self.parent_chunk = parent_chunk
-        self.offset = fileobj.tell()
-
-        header = fileobj.read(self.HEADER_SIZE)
-        if len(header) < self.HEADER_SIZE:
-            raise InvalidChunk()
-
-        self.id, self.data_size = struct.unpack('>4si', header)
-
-        try:
-            self.id = self.id.decode('ascii')
-        except UnicodeDecodeError:
-            raise InvalidChunk()
-
-        if not is_valid_chunk_id(self.id):
-            raise InvalidChunk()
-
-        self.size = self.HEADER_SIZE + self.data_size
-        self.data_offset = fileobj.tell()
-
-    def read(self):
-        """Read the chunks data"""
-
-        self.__fileobj.seek(self.data_offset)
-        return self.__fileobj.read(self.data_size)
-
-    def write(self, data):
-        """Write the chunk data"""
-
-        if len(data) > self.data_size:
-            raise ValueError
-
-        self.__fileobj.seek(self.data_offset)
-        self.__fileobj.write(data)
-
-    def delete(self):
-        """Removes the chunk from the file"""
-
-        delete_bytes(self.__fileobj, self.size, self.offset)
-        if self.parent_chunk is not None:
-            self.parent_chunk._update_size(
-                self.parent_chunk.data_size - self.size)
-
-    def _update_size(self, data_size):
-        """Update the size of the chunk"""
-
-        self.__fileobj.seek(self.offset + 4)
-        self.__fileobj.write(pack('>I', data_size))
-        if self.parent_chunk is not None:
-            size_diff = self.data_size - data_size
-            self.parent_chunk._update_size(
-                self.parent_chunk.data_size - size_diff)
-        self.data_size = data_size
-        self.size = data_size + self.HEADER_SIZE
-
-    def resize(self, new_data_size):
-        """Resize the file and update the chunk sizes"""
-
-        resize_bytes(
-            self.__fileobj, self.data_size, new_data_size, self.data_offset)
-        self._update_size(new_data_size)
-
-
-class IFFFile(object):
-    """Representation of a IFF file"""
-
-    def __init__(self, fileobj):
-        self.__fileobj = fileobj
-        self.__chunks = {}
-
-        # AIFF Files always start with the FORM chunk which contains a 4 byte
-        # ID before the start of other chunks
-        fileobj.seek(0)
-        self.__chunks[u'FORM'] = IFFChunk(fileobj)
-
-        # Skip past the 4 byte FORM id
-        fileobj.seek(IFFChunk.HEADER_SIZE + 4)
-
-        # Where the next chunk can be located. We need to keep track of this
-        # since the size indicated in the FORM header may not match up with the
-        # offset determined from the size of the last chunk in the file
-        self.__next_offset = fileobj.tell()
-
-        # Load all of the chunks
-        while True:
-            try:
-                chunk = IFFChunk(fileobj, self[u'FORM'])
-            except InvalidChunk:
-                break
-            self.__chunks[chunk.id.strip()] = chunk
-
-            # Calculate the location of the next chunk,
-            # considering the pad byte
-            self.__next_offset = chunk.offset + chunk.size
-            self.__next_offset += self.__next_offset % 2
-            fileobj.seek(self.__next_offset)
-
-    def __contains__(self, id_):
-        """Check if the IFF file contains a specific chunk"""
-
-        assert isinstance(id_, text_type)
-
-        if not is_valid_chunk_id(id_):
-            raise KeyError("AIFF key must be four ASCII characters.")
-
-        return id_ in self.__chunks
-
-    def __getitem__(self, id_):
-        """Get a chunk from the IFF file"""
-
-        assert isinstance(id_, text_type)
-
-        if not is_valid_chunk_id(id_):
-            raise KeyError("AIFF key must be four ASCII characters.")
-
-        try:
-            return self.__chunks[id_]
-        except KeyError:
-            raise KeyError(
-                "%r has no %r chunk" % (self.__fileobj.name, id_))
-
-    def __delitem__(self, id_):
-        """Remove a chunk from the IFF file"""
-
-        assert isinstance(id_, text_type)
-
-        if not is_valid_chunk_id(id_):
-            raise KeyError("AIFF key must be four ASCII characters.")
-
-        self.__chunks.pop(id_).delete()
-
-    def insert_chunk(self, id_):
-        """Insert a new chunk at the end of the IFF file"""
-
-        assert isinstance(id_, text_type)
-
-        if not is_valid_chunk_id(id_):
-            raise KeyError("AIFF key must be four ASCII characters.")
-
-        self.__fileobj.seek(self.__next_offset)
-        self.__fileobj.write(pack('>4si', id_.ljust(4).encode('ascii'), 0))
-        self.__fileobj.seek(self.__next_offset)
-        chunk = IFFChunk(self.__fileobj, self[u'FORM'])
-        self[u'FORM']._update_size(self[u'FORM'].data_size + chunk.size)
-
-        self.__chunks[id_] = chunk
-        self.__next_offset = chunk.offset + chunk.size
-
-
-class AIFFInfo(StreamInfo):
-    """AIFF audio stream information.
-
-    Information is parsed from the COMM chunk of the AIFF file
-
-    Useful attributes:
-
-    * length -- audio length, in seconds
-    * bitrate -- audio bitrate, in bits per second
-    * channels -- The number of audio channels
-    * sample_rate -- audio sample rate, in Hz
-    * sample_size -- The audio sample size
-    """
-
-    length = 0
-    bitrate = 0
-    channels = 0
-    sample_rate = 0
-
-    def __init__(self, fileobj):
-        iff = IFFFile(fileobj)
-        try:
-            common_chunk = iff[u'COMM']
-        except KeyError as e:
-            raise error(str(e))
-
-        data = common_chunk.read()
-
-        info = struct.unpack('>hLh10s', data[:18])
-        channels, frame_count, sample_size, sample_rate = info
-
-        self.sample_rate = int(read_float(sample_rate))
-        self.sample_size = sample_size
-        self.channels = channels
-        self.bitrate = channels * sample_size * self.sample_rate
-        self.length = frame_count / float(self.sample_rate)
-
-    def pprint(self):
-        return u"%d channel AIFF @ %d bps, %s Hz, %.2f seconds" % (
-            self.channels, self.bitrate, self.sample_rate, self.length)
-
-
-class _IFFID3(ID3):
-    """A AIFF file with ID3v2 tags"""
-
-    def _pre_load_header(self, fileobj):
-        try:
-            fileobj.seek(IFFFile(fileobj)[u'ID3'].data_offset)
-        except (InvalidChunk, KeyError):
-            raise ID3NoHeaderError("No ID3 chunk")
-
-    def save(self, filename=None, v2_version=4, v23_sep='/', padding=None):
-        """Save ID3v2 data to the AIFF file"""
-
-        if filename is None:
-            filename = self.filename
-
-        # Unlike the parent ID3.save method, we won't save to a blank file
-        # since we would have to construct a empty AIFF file
-        with open(filename, 'rb+') as fileobj:
-            iff_file = IFFFile(fileobj)
-
-            if u'ID3' not in iff_file:
-                iff_file.insert_chunk(u'ID3')
-
-            chunk = iff_file[u'ID3']
-
-            try:
-                data = self._prepare_data(
-                    fileobj, chunk.data_offset, chunk.data_size, v2_version,
-                    v23_sep, padding)
-            except ID3Error as e:
-                reraise(error, e, sys.exc_info()[2])
-
-            new_size = len(data)
-            new_size += new_size % 2  # pad byte
-            assert new_size % 2 == 0
-            chunk.resize(new_size)
-            data += (new_size - len(data)) * b'\x00'
-            assert new_size == len(data)
-            chunk.write(data)
-
-    def delete(self, filename=None):
-        """Completely removes the ID3 chunk from the AIFF file"""
-
-        if filename is None:
-            filename = self.filename
-        delete(filename)
-        self.clear()
-
-
-def delete(filename):
-    """Completely removes the ID3 chunk from the AIFF file"""
-
-    with open(filename, "rb+") as file_:
-        try:
-            del IFFFile(file_)[u'ID3']
-        except KeyError:
-            pass
-
-
-class AIFF(FileType):
-    """An AIFF audio file.
-
-    :ivar info: :class:`AIFFInfo`
-    :ivar tags: :class:`ID3`
-    """
-
-    _mimes = ["audio/aiff", "audio/x-aiff"]
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        filename = filename.lower()
-
-        return (header.startswith(b"FORM") * 2 + endswith(filename, b".aif") +
-                endswith(filename, b".aiff") + endswith(filename, b".aifc"))
-
-    def add_tags(self):
-        """Add an empty ID3 tag to the file."""
-        if self.tags is None:
-            self.tags = _IFFID3()
-        else:
-            raise error("an ID3 tag already exists")
-
-    def load(self, filename, **kwargs):
-        """Load stream and tag information from a file."""
-        self.filename = filename
-
-        try:
-            self.tags = _IFFID3(filename, **kwargs)
-        except ID3NoHeaderError:
-            self.tags = None
-        except ID3Error as e:
-            raise error(e)
-
-        with open(filename, "rb") as fileobj:
-            self.info = AIFFInfo(fileobj)
-
-
-Open = AIFF
diff --git a/resources/lib/libraries/mutagen/apev2.py b/resources/lib/libraries/mutagen/apev2.py
deleted file mode 100644
index 3b79aba9..00000000
--- a/resources/lib/libraries/mutagen/apev2.py
+++ /dev/null
@@ -1,710 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""APEv2 reading and writing.
-
-The APEv2 format is most commonly used with Musepack files, but is
-also the format of choice for WavPack and other formats. Some MP3s
-also have APEv2 tags, but this can cause problems with many MP3
-decoders and taggers.
-
-APEv2 tags, like Vorbis comments, are freeform key=value pairs. APEv2
-keys can be any ASCII string with characters from 0x20 to 0x7E,
-between 2 and 255 characters long.  Keys are case-sensitive, but
-readers are recommended to be case insensitive, and it is forbidden to
-multiple keys which differ only in case.  Keys are usually stored
-title-cased (e.g. 'Artist' rather than 'artist').
-
-APEv2 values are slightly more structured than Vorbis comments; values
-are flagged as one of text, binary, or an external reference (usually
-a URI).
-
-Based off the format specification found at
-http://wiki.hydrogenaudio.org/index.php?title=APEv2_specification.
-"""
-
-__all__ = ["APEv2", "APEv2File", "Open", "delete"]
-
-import sys
-import struct
-from collections import MutableSequence
-
-from ._compat import (cBytesIO, PY3, text_type, PY2, reraise, swap_to_string,
-                      xrange)
-from mutagen import Metadata, FileType, StreamInfo
-from mutagen._util import (DictMixin, cdata, delete_bytes, total_ordering,
-                           MutagenError)
-
-
-def is_valid_apev2_key(key):
-    if not isinstance(key, text_type):
-        if PY3:
-            raise TypeError("APEv2 key must be str")
-
-        try:
-            key = key.decode('ascii')
-        except UnicodeDecodeError:
-            return False
-
-    # PY26 - Change to set literal syntax (since set is faster than list here)
-    return ((2 <= len(key) <= 255) and (min(key) >= u' ') and
-            (max(key) <= u'~') and
-            (key not in [u"OggS", u"TAG", u"ID3", u"MP+"]))
-
-# There are three different kinds of APE tag values.
-# "0: Item contains text information coded in UTF-8
-#  1: Item contains binary information
-#  2: Item is a locator of external stored information [e.g. URL]
-#  3: reserved"
-TEXT, BINARY, EXTERNAL = xrange(3)
-
-HAS_HEADER = 1 << 31
-HAS_NO_FOOTER = 1 << 30
-IS_HEADER = 1 << 29
-
-
-class error(IOError, MutagenError):
-    pass
-
-
-class APENoHeaderError(error, ValueError):
-    pass
-
-
-class APEUnsupportedVersionError(error, ValueError):
-    pass
-
-
-class APEBadItemError(error, ValueError):
-    pass
-
-
-class _APEv2Data(object):
-    # Store offsets of the important parts of the file.
-    start = header = data = footer = end = None
-    # Footer or header; seek here and read 32 to get version/size/items/flags
-    metadata = None
-    # Actual tag data
-    tag = None
-
-    version = None
-    size = None
-    items = None
-    flags = 0
-
-    # The tag is at the start rather than the end. A tag at both
-    # the start and end of the file (i.e. the tag is the whole file)
-    # is not considered to be at the start.
-    is_at_start = False
-
-    def __init__(self, fileobj):
-        self.__find_metadata(fileobj)
-
-        if self.header is None:
-            self.metadata = self.footer
-        elif self.footer is None:
-            self.metadata = self.header
-        else:
-            self.metadata = max(self.header, self.footer)
-
-        if self.metadata is None:
-            return
-
-        self.__fill_missing(fileobj)
-        self.__fix_brokenness(fileobj)
-        if self.data is not None:
-            fileobj.seek(self.data)
-            self.tag = fileobj.read(self.size)
-
-    def __find_metadata(self, fileobj):
-        # Try to find a header or footer.
-
-        # Check for a simple footer.
-        try:
-            fileobj.seek(-32, 2)
-        except IOError:
-            fileobj.seek(0, 2)
-            return
-        if fileobj.read(8) == b"APETAGEX":
-            fileobj.seek(-8, 1)
-            self.footer = self.metadata = fileobj.tell()
-            return
-
-        # Check for an APEv2 tag followed by an ID3v1 tag at the end.
-        try:
-            fileobj.seek(-128, 2)
-            if fileobj.read(3) == b"TAG":
-
-                fileobj.seek(-35, 1)  # "TAG" + header length
-                if fileobj.read(8) == b"APETAGEX":
-                    fileobj.seek(-8, 1)
-                    self.footer = fileobj.tell()
-                    return
-
-                # ID3v1 tag at the end, maybe preceded by Lyrics3v2.
-                # (http://www.id3.org/lyrics3200.html)
-                # (header length - "APETAGEX") - "LYRICS200"
-                fileobj.seek(15, 1)
-                if fileobj.read(9) == b'LYRICS200':
-                    fileobj.seek(-15, 1)  # "LYRICS200" + size tag
-                    try:
-                        offset = int(fileobj.read(6))
-                    except ValueError:
-                        raise IOError
-
-                    fileobj.seek(-32 - offset - 6, 1)
-                    if fileobj.read(8) == b"APETAGEX":
-                        fileobj.seek(-8, 1)
-                        self.footer = fileobj.tell()
-                        return
-
-        except IOError:
-            pass
-
-        # Check for a tag at the start.
-        fileobj.seek(0, 0)
-        if fileobj.read(8) == b"APETAGEX":
-            self.is_at_start = True
-            self.header = 0
-
-    def __fill_missing(self, fileobj):
-        fileobj.seek(self.metadata + 8)
-        self.version = fileobj.read(4)
-        self.size = cdata.uint_le(fileobj.read(4))
-        self.items = cdata.uint_le(fileobj.read(4))
-        self.flags = cdata.uint_le(fileobj.read(4))
-
-        if self.header is not None:
-            self.data = self.header + 32
-            # If we're reading the header, the size is the header
-            # offset + the size, which includes the footer.
-            self.end = self.data + self.size
-            fileobj.seek(self.end - 32, 0)
-            if fileobj.read(8) == b"APETAGEX":
-                self.footer = self.end - 32
-        elif self.footer is not None:
-            self.end = self.footer + 32
-            self.data = self.end - self.size
-            if self.flags & HAS_HEADER:
-                self.header = self.data - 32
-            else:
-                self.header = self.data
-        else:
-            raise APENoHeaderError("No APE tag found")
-
-        # exclude the footer from size
-        if self.footer is not None:
-            self.size -= 32
-
-    def __fix_brokenness(self, fileobj):
-        # Fix broken tags written with PyMusepack.
-        if self.header is not None:
-            start = self.header
-        else:
-            start = self.data
-        fileobj.seek(start)
-
-        while start > 0:
-            # Clean up broken writing from pre-Mutagen PyMusepack.
-            # It didn't remove the first 24 bytes of header.
-            try:
-                fileobj.seek(-24, 1)
-            except IOError:
-                break
-            else:
-                if fileobj.read(8) == b"APETAGEX":
-                    fileobj.seek(-8, 1)
-                    start = fileobj.tell()
-                else:
-                    break
-        self.start = start
-
-
-class _CIDictProxy(DictMixin):
-
-    def __init__(self, *args, **kwargs):
-        self.__casemap = {}
-        self.__dict = {}
-        super(_CIDictProxy, self).__init__(*args, **kwargs)
-        # Internally all names are stored as lowercase, but the case
-        # they were set with is remembered and used when saving.  This
-        # is roughly in line with the standard, which says that keys
-        # are case-sensitive but two keys differing only in case are
-        # not allowed, and recommends case-insensitive
-        # implementations.
-
-    def __getitem__(self, key):
-        return self.__dict[key.lower()]
-
-    def __setitem__(self, key, value):
-        lower = key.lower()
-        self.__casemap[lower] = key
-        self.__dict[lower] = value
-
-    def __delitem__(self, key):
-        lower = key.lower()
-        del(self.__casemap[lower])
-        del(self.__dict[lower])
-
-    def keys(self):
-        return [self.__casemap.get(key, key) for key in self.__dict.keys()]
-
-
-class APEv2(_CIDictProxy, Metadata):
-    """A file with an APEv2 tag.
-
-    ID3v1 tags are silently ignored and overwritten.
-    """
-
-    filename = None
-
-    def pprint(self):
-        """Return tag key=value pairs in a human-readable format."""
-
-        items = sorted(self.items())
-        return u"\n".join(u"%s=%s" % (k, v.pprint()) for k, v in items)
-
-    def load(self, filename):
-        """Load tags from a filename."""
-
-        self.filename = filename
-        with open(filename, "rb") as fileobj:
-            data = _APEv2Data(fileobj)
-
-        if data.tag:
-            self.clear()
-            self.__parse_tag(data.tag, data.items)
-        else:
-            raise APENoHeaderError("No APE tag found")
-
-    def __parse_tag(self, tag, count):
-        fileobj = cBytesIO(tag)
-
-        for i in xrange(count):
-            size_data = fileobj.read(4)
-            # someone writes wrong item counts
-            if not size_data:
-                break
-            size = cdata.uint_le(size_data)
-            flags = cdata.uint_le(fileobj.read(4))
-
-            # Bits 1 and 2 bits are flags, 0-3
-            # Bit 0 is read/write flag, ignored
-            kind = (flags & 6) >> 1
-            if kind == 3:
-                raise APEBadItemError("value type must be 0, 1, or 2")
-            key = value = fileobj.read(1)
-            while key[-1:] != b'\x00' and value:
-                value = fileobj.read(1)
-                key += value
-            if key[-1:] == b"\x00":
-                key = key[:-1]
-            if PY3:
-                try:
-                    key = key.decode("ascii")
-                except UnicodeError as err:
-                    reraise(APEBadItemError, err, sys.exc_info()[2])
-            value = fileobj.read(size)
-
-            value = _get_value_type(kind)._new(value)
-
-            self[key] = value
-
-    def __getitem__(self, key):
-        if not is_valid_apev2_key(key):
-            raise KeyError("%r is not a valid APEv2 key" % key)
-        if PY2:
-            key = key.encode('ascii')
-
-        return super(APEv2, self).__getitem__(key)
-
-    def __delitem__(self, key):
-        if not is_valid_apev2_key(key):
-            raise KeyError("%r is not a valid APEv2 key" % key)
-        if PY2:
-            key = key.encode('ascii')
-
-        super(APEv2, self).__delitem__(key)
-
-    def __setitem__(self, key, value):
-        """'Magic' value setter.
-
-        This function tries to guess at what kind of value you want to
-        store. If you pass in a valid UTF-8 or Unicode string, it
-        treats it as a text value. If you pass in a list, it treats it
-        as a list of string/Unicode values.  If you pass in a string
-        that is not valid UTF-8, it assumes it is a binary value.
-
-        Python 3: all bytes will be assumed to be a byte value, even
-        if they are valid utf-8.
-
-        If you need to force a specific type of value (e.g. binary
-        data that also happens to be valid UTF-8, or an external
-        reference), use the APEValue factory and set the value to the
-        result of that::
-
-            from mutagen.apev2 import APEValue, EXTERNAL
-            tag['Website'] = APEValue('http://example.org', EXTERNAL)
-        """
-
-        if not is_valid_apev2_key(key):
-            raise KeyError("%r is not a valid APEv2 key" % key)
-
-        if PY2:
-            key = key.encode('ascii')
-
-        if not isinstance(value, _APEValue):
-            # let's guess at the content if we're not already a value...
-            if isinstance(value, text_type):
-                # unicode? we've got to be text.
-                value = APEValue(value, TEXT)
-            elif isinstance(value, list):
-                items = []
-                for v in value:
-                    if not isinstance(v, text_type):
-                        if PY3:
-                            raise TypeError("item in list not str")
-                        v = v.decode("utf-8")
-                    items.append(v)
-
-                # list? text.
-                value = APEValue(u"\0".join(items), TEXT)
-            else:
-                if PY3:
-                    value = APEValue(value, BINARY)
-                else:
-                    try:
-                        value.decode("utf-8")
-                    except UnicodeError:
-                        # invalid UTF8 text, probably binary
-                        value = APEValue(value, BINARY)
-                    else:
-                        # valid UTF8, probably text
-                        value = APEValue(value, TEXT)
-
-        super(APEv2, self).__setitem__(key, value)
-
-    def save(self, filename=None):
-        """Save changes to a file.
-
-        If no filename is given, the one most recently loaded is used.
-
-        Tags are always written at the end of the file, and include
-        a header and a footer.
-        """
-
-        filename = filename or self.filename
-        try:
-            fileobj = open(filename, "r+b")
-        except IOError:
-            fileobj = open(filename, "w+b")
-        data = _APEv2Data(fileobj)
-
-        if data.is_at_start:
-            delete_bytes(fileobj, data.end - data.start, data.start)
-        elif data.start is not None:
-            fileobj.seek(data.start)
-            # Delete an ID3v1 tag if present, too.
-            fileobj.truncate()
-        fileobj.seek(0, 2)
-
-        tags = []
-        for key, value in self.items():
-            # Packed format for an item:
-            # 4B: Value length
-            # 4B: Value type
-            # Key name
-            # 1B: Null
-            # Key value
-            value_data = value._write()
-            if not isinstance(key, bytes):
-                key = key.encode("utf-8")
-            tag_data = bytearray()
-            tag_data += struct.pack("<2I", len(value_data), value.kind << 1)
-            tag_data += key + b"\0" + value_data
-            tags.append(bytes(tag_data))
-
-        # "APE tags items should be sorted ascending by size... This is
-        # not a MUST, but STRONGLY recommended. Actually the items should
-        # be sorted by importance/byte, but this is not feasible."
-        tags.sort(key=len)
-        num_tags = len(tags)
-        tags = b"".join(tags)
-
-        header = bytearray(b"APETAGEX")
-        # version, tag size, item count, flags
-        header += struct.pack("<4I", 2000, len(tags) + 32, num_tags,
-                              HAS_HEADER | IS_HEADER)
-        header += b"\0" * 8
-        fileobj.write(header)
-
-        fileobj.write(tags)
-
-        footer = bytearray(b"APETAGEX")
-        footer += struct.pack("<4I", 2000, len(tags) + 32, num_tags,
-                              HAS_HEADER)
-        footer += b"\0" * 8
-
-        fileobj.write(footer)
-        fileobj.close()
-
-    def delete(self, filename=None):
-        """Remove tags from a file."""
-
-        filename = filename or self.filename
-        with open(filename, "r+b") as fileobj:
-            data = _APEv2Data(fileobj)
-            if data.start is not None and data.size is not None:
-                delete_bytes(fileobj, data.end - data.start, data.start)
-
-        self.clear()
-
-
-Open = APEv2
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    try:
-        APEv2(filename).delete()
-    except APENoHeaderError:
-        pass
-
-
-def _get_value_type(kind):
-    """Returns a _APEValue subclass or raises ValueError"""
-
-    if kind == TEXT:
-        return APETextValue
-    elif kind == BINARY:
-        return APEBinaryValue
-    elif kind == EXTERNAL:
-        return APEExtValue
-    raise ValueError("unknown kind %r" % kind)
-
-
-def APEValue(value, kind):
-    """APEv2 tag value factory.
-
-    Use this if you need to specify the value's type manually.  Binary
-    and text data are automatically detected by APEv2.__setitem__.
-    """
-
-    try:
-        type_ = _get_value_type(kind)
-    except ValueError:
-        raise ValueError("kind must be TEXT, BINARY, or EXTERNAL")
-    else:
-        return type_(value)
-
-
-class _APEValue(object):
-
-    kind = None
-    value = None
-
-    def __init__(self, value, kind=None):
-        # kind kwarg is for backwards compat
-        if kind is not None and kind != self.kind:
-            raise ValueError
-        self.value = self._validate(value)
-
-    @classmethod
-    def _new(cls, data):
-        instance = cls.__new__(cls)
-        instance._parse(data)
-        return instance
-
-    def _parse(self, data):
-        """Sets value or raises APEBadItemError"""
-
-        raise NotImplementedError
-
-    def _write(self):
-        """Returns bytes"""
-
-        raise NotImplementedError
-
-    def _validate(self, value):
-        """Returns validated value or raises TypeError/ValueErrr"""
-
-        raise NotImplementedError
-
-    def __repr__(self):
-        return "%s(%r, %d)" % (type(self).__name__, self.value, self.kind)
-
-
-@swap_to_string
-@total_ordering
-class _APEUtf8Value(_APEValue):
-
-    def _parse(self, data):
-        try:
-            self.value = data.decode("utf-8")
-        except UnicodeDecodeError as e:
-            reraise(APEBadItemError, e, sys.exc_info()[2])
-
-    def _validate(self, value):
-        if not isinstance(value, text_type):
-            if PY3:
-                raise TypeError("value not str")
-            else:
-                value = value.decode("utf-8")
-        return value
-
-    def _write(self):
-        return self.value.encode("utf-8")
-
-    def __len__(self):
-        return len(self.value)
-
-    def __bytes__(self):
-        return self._write()
-
-    def __eq__(self, other):
-        return self.value == other
-
-    def __lt__(self, other):
-        return self.value < other
-
-    def __str__(self):
-        return self.value
-
-
-class APETextValue(_APEUtf8Value, MutableSequence):
-    """An APEv2 text value.
-
-    Text values are Unicode/UTF-8 strings. They can be accessed like
-    strings (with a null separating the values), or arrays of strings.
-    """
-
-    kind = TEXT
-
-    def __iter__(self):
-        """Iterate over the strings of the value (not the characters)"""
-
-        return iter(self.value.split(u"\0"))
-
-    def __getitem__(self, index):
-        return self.value.split(u"\0")[index]
-
-    def __len__(self):
-        return self.value.count(u"\0") + 1
-
-    def __setitem__(self, index, value):
-        if not isinstance(value, text_type):
-            if PY3:
-                raise TypeError("value not str")
-            else:
-                value = value.decode("utf-8")
-
-        values = list(self)
-        values[index] = value
-        self.value = u"\0".join(values)
-
-    def insert(self, index, value):
-        if not isinstance(value, text_type):
-            if PY3:
-                raise TypeError("value not str")
-            else:
-                value = value.decode("utf-8")
-
-        values = list(self)
-        values.insert(index, value)
-        self.value = u"\0".join(values)
-
-    def __delitem__(self, index):
-        values = list(self)
-        del values[index]
-        self.value = u"\0".join(values)
-
-    def pprint(self):
-        return u" / ".join(self)
-
-
-@swap_to_string
-@total_ordering
-class APEBinaryValue(_APEValue):
-    """An APEv2 binary value."""
-
-    kind = BINARY
-
-    def _parse(self, data):
-        self.value = data
-
-    def _write(self):
-        return self.value
-
-    def _validate(self, value):
-        if not isinstance(value, bytes):
-            raise TypeError("value not bytes")
-        return bytes(value)
-
-    def __len__(self):
-        return len(self.value)
-
-    def __bytes__(self):
-        return self._write()
-
-    def __eq__(self, other):
-        return self.value == other
-
-    def __lt__(self, other):
-        return self.value < other
-
-    def pprint(self):
-        return u"[%d bytes]" % len(self)
-
-
-class APEExtValue(_APEUtf8Value):
-    """An APEv2 external value.
-
-    External values are usually URI or IRI strings.
-    """
-
-    kind = EXTERNAL
-
-    def pprint(self):
-        return u"[External] %s" % self.value
-
-
-class APEv2File(FileType):
-    class _Info(StreamInfo):
-        length = 0
-        bitrate = 0
-
-        def __init__(self, fileobj):
-            pass
-
-        @staticmethod
-        def pprint():
-            return u"Unknown format with APEv2 tag."
-
-    def load(self, filename):
-        self.filename = filename
-        self.info = self._Info(open(filename, "rb"))
-        try:
-            self.tags = APEv2(filename)
-        except APENoHeaderError:
-            self.tags = None
-
-    def add_tags(self):
-        if self.tags is None:
-            self.tags = APEv2()
-        else:
-            raise error("%r already has tags: %r" % (self, self.tags))
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        try:
-            fileobj.seek(-160, 2)
-        except IOError:
-            fileobj.seek(0)
-        footer = fileobj.read()
-        return ((b"APETAGEX" in footer) - header.startswith(b"ID3"))
diff --git a/resources/lib/libraries/mutagen/asf/__init__.py b/resources/lib/libraries/mutagen/asf/__init__.py
deleted file mode 100644
index e667192d..00000000
--- a/resources/lib/libraries/mutagen/asf/__init__.py
+++ /dev/null
@@ -1,319 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2005-2006  Joe Wreschnig
-# Copyright (C) 2006-2007  Lukas Lalinsky
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write ASF (Window Media Audio) files."""
-
-__all__ = ["ASF", "Open"]
-
-from mutagen import FileType, Metadata, StreamInfo
-from mutagen._util import resize_bytes, DictMixin
-from mutagen._compat import string_types, long_, PY3, izip
-
-from ._util import error, ASFError, ASFHeaderError
-from ._objects import HeaderObject, MetadataLibraryObject, MetadataObject, \
-    ExtendedContentDescriptionObject, HeaderExtensionObject, \
-    ContentDescriptionObject
-from ._attrs import ASFGUIDAttribute, ASFWordAttribute, ASFQWordAttribute, \
-    ASFDWordAttribute, ASFBoolAttribute, ASFByteArrayAttribute, \
-    ASFUnicodeAttribute, ASFBaseAttribute, ASFValue
-
-
-# pyflakes
-error, ASFError, ASFHeaderError, ASFValue
-
-
-class ASFInfo(StreamInfo):
-    """ASF stream information."""
-
-    length = 0.0
-    """Length in seconds (`float`)"""
-
-    sample_rate = 0
-    """Sample rate in Hz (`int`)"""
-
-    bitrate = 0
-    """Bitrate in bps (`int`)"""
-
-    channels = 0
-    """Number of channels (`int`)"""
-
-    codec_type = u""
-    """Name of the codec type of the first audio stream or
-    an empty string if unknown. Example: ``Windows Media Audio 9 Standard``
-    (:class:`mutagen.text`)
-    """
-
-    codec_name = u""
-    """Name and maybe version of the codec used. Example:
-    ``Windows Media Audio 9.1`` (:class:`mutagen.text`)
-    """
-
-    codec_description = u""
-    """Further information on the codec used.
-    Example: ``64 kbps, 48 kHz, stereo 2-pass CBR`` (:class:`mutagen.text`)
-    """
-
-    def __init__(self):
-        self.length = 0.0
-        self.sample_rate = 0
-        self.bitrate = 0
-        self.channels = 0
-        self.codec_type = u""
-        self.codec_name = u""
-        self.codec_description = u""
-
-    def pprint(self):
-        """Returns a stream information text summary
-
-        :rtype: text
-        """
-
-        s = u"ASF (%s) %d bps, %s Hz, %d channels, %.2f seconds" % (
-            self.codec_type or self.codec_name or u"???", self.bitrate,
-            self.sample_rate, self.channels, self.length)
-        return s
-
-
-class ASFTags(list, DictMixin, Metadata):
-    """Dictionary containing ASF attributes."""
-
-    def __getitem__(self, key):
-        """A list of values for the key.
-
-        This is a copy, so comment['title'].append('a title') will not
-        work.
-
-        """
-
-        # PY3 only
-        if isinstance(key, slice):
-            return list.__getitem__(self, key)
-
-        values = [value for (k, value) in self if k == key]
-        if not values:
-            raise KeyError(key)
-        else:
-            return values
-
-    def __delitem__(self, key):
-        """Delete all values associated with the key."""
-
-        # PY3 only
-        if isinstance(key, slice):
-            return list.__delitem__(self, key)
-
-        to_delete = [x for x in self if x[0] == key]
-        if not to_delete:
-            raise KeyError(key)
-        else:
-            for k in to_delete:
-                self.remove(k)
-
-    def __contains__(self, key):
-        """Return true if the key has any values."""
-        for k, value in self:
-            if k == key:
-                return True
-        else:
-            return False
-
-    def __setitem__(self, key, values):
-        """Set a key's value or values.
-
-        Setting a value overwrites all old ones. The value may be a
-        list of Unicode or UTF-8 strings, or a single Unicode or UTF-8
-        string.
-        """
-
-        # PY3 only
-        if isinstance(key, slice):
-            return list.__setitem__(self, key, values)
-
-        if not isinstance(values, list):
-            values = [values]
-
-        to_append = []
-        for value in values:
-            if not isinstance(value, ASFBaseAttribute):
-                if isinstance(value, string_types):
-                    value = ASFUnicodeAttribute(value)
-                elif PY3 and isinstance(value, bytes):
-                    value = ASFByteArrayAttribute(value)
-                elif isinstance(value, bool):
-                    value = ASFBoolAttribute(value)
-                elif isinstance(value, int):
-                    value = ASFDWordAttribute(value)
-                elif isinstance(value, long_):
-                    value = ASFQWordAttribute(value)
-                else:
-                    raise TypeError("Invalid type %r" % type(value))
-            to_append.append((key, value))
-
-        try:
-            del(self[key])
-        except KeyError:
-            pass
-
-        self.extend(to_append)
-
-    def keys(self):
-        """Return a sequence of all keys in the comment."""
-
-        return self and set(next(izip(*self)))
-
-    def as_dict(self):
-        """Return a copy of the comment data in a real dict."""
-
-        d = {}
-        for key, value in self:
-            d.setdefault(key, []).append(value)
-        return d
-
-    def pprint(self):
-        """Returns a string containing all key, value pairs.
-
-        :rtype: text
-        """
-
-        return "\n".join("%s=%s" % (k, v) for k, v in self)
-
-
-UNICODE = ASFUnicodeAttribute.TYPE
-"""Unicode string type"""
-
-BYTEARRAY = ASFByteArrayAttribute.TYPE
-"""Byte array type"""
-
-BOOL = ASFBoolAttribute.TYPE
-"""Bool type"""
-
-DWORD = ASFDWordAttribute.TYPE
-""""DWord type (uint32)"""
-
-QWORD = ASFQWordAttribute.TYPE
-"""QWord type (uint64)"""
-
-WORD = ASFWordAttribute.TYPE
-"""Word type (uint16)"""
-
-GUID = ASFGUIDAttribute.TYPE
-"""GUID type"""
-
-
-class ASF(FileType):
-    """An ASF file, probably containing WMA or WMV.
-
-    :param filename: a filename to load
-    :raises mutagen.asf.error: In case loading fails
-    """
-
-    _mimes = ["audio/x-ms-wma", "audio/x-ms-wmv", "video/x-ms-asf",
-              "audio/x-wma", "video/x-wmv"]
-
-    info = None
-    """A `ASFInfo` instance"""
-
-    tags = None
-    """A `ASFTags` instance"""
-
-    def load(self, filename):
-        self.filename = filename
-        self.info = ASFInfo()
-        self.tags = ASFTags()
-
-        with open(filename, "rb") as fileobj:
-            self._tags = {}
-
-            self._header = HeaderObject.parse_full(self, fileobj)
-
-            for guid in [ContentDescriptionObject.GUID,
-                    ExtendedContentDescriptionObject.GUID, MetadataObject.GUID,
-                    MetadataLibraryObject.GUID]:
-                self.tags.extend(self._tags.pop(guid, []))
-
-            assert not self._tags
-
-    def save(self, filename=None, padding=None):
-        """Save tag changes back to the loaded file.
-
-        :param padding: A callback which returns the amount of padding to use.
-            See :class:`mutagen.PaddingInfo`
-
-        :raises mutagen.asf.error: In case saving fails
-        """
-
-        if filename is not None and filename != self.filename:
-            raise ValueError("saving to another file not supported atm")
-
-        # Move attributes to the right objects
-        self.to_content_description = {}
-        self.to_extended_content_description = {}
-        self.to_metadata = {}
-        self.to_metadata_library = []
-        for name, value in self.tags:
-            library_only = (value.data_size() > 0xFFFF or value.TYPE == GUID)
-            can_cont_desc = value.TYPE == UNICODE
-
-            if library_only or value.language is not None:
-                self.to_metadata_library.append((name, value))
-            elif value.stream is not None:
-                if name not in self.to_metadata:
-                    self.to_metadata[name] = value
-                else:
-                    self.to_metadata_library.append((name, value))
-            elif name in ContentDescriptionObject.NAMES:
-                if name not in self.to_content_description and can_cont_desc:
-                    self.to_content_description[name] = value
-                else:
-                    self.to_metadata_library.append((name, value))
-            else:
-                if name not in self.to_extended_content_description:
-                    self.to_extended_content_description[name] = value
-                else:
-                    self.to_metadata_library.append((name, value))
-
-        # Add missing objects
-        header = self._header
-        if header.get_child(ContentDescriptionObject.GUID) is None:
-            header.objects.append(ContentDescriptionObject())
-        if header.get_child(ExtendedContentDescriptionObject.GUID) is None:
-            header.objects.append(ExtendedContentDescriptionObject())
-        header_ext = header.get_child(HeaderExtensionObject.GUID)
-        if header_ext is None:
-            header_ext = HeaderExtensionObject()
-            header.objects.append(header_ext)
-        if header_ext.get_child(MetadataObject.GUID) is None:
-            header_ext.objects.append(MetadataObject())
-        if header_ext.get_child(MetadataLibraryObject.GUID) is None:
-            header_ext.objects.append(MetadataLibraryObject())
-
-        # Render to file
-        with open(self.filename, "rb+") as fileobj:
-            old_size = header.parse_size(fileobj)[0]
-            data = header.render_full(self, fileobj, old_size, padding)
-            size = len(data)
-            resize_bytes(fileobj, old_size, size, 0)
-            fileobj.seek(0)
-            fileobj.write(data)
-
-    def add_tags(self):
-        raise ASFError
-
-    def delete(self, filename=None):
-
-        if filename is not None and filename != self.filename:
-            raise ValueError("saving to another file not supported atm")
-
-        self.tags.clear()
-        self.save(padding=lambda x: 0)
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return header.startswith(HeaderObject.GUID) * 2
-
-Open = ASF
diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/__init__.cpython-35.pyc
deleted file mode 100644
index 277e2c5fe677f3fb5deae56dc081e4c55d092175..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 8567
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
GqW=Or25P<l

diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_attrs.cpython-35.pyc
deleted file mode 100644
index aa916eddf8713bbf7a8cf3defef0062f0f3221c9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 15131
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

diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_objects.cpython-35.pyc
deleted file mode 100644
index cb0810d8909b8a57de791aca68842bd2ba3565d6..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 15903
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
NVtv;%exDt){tt%Az9#?x

diff --git a/resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/asf/__pycache__/_util.cpython-35.pyc
deleted file mode 100644
index 661bff507dd00b7435a4f08bb07b58b7deed1cf4..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 10603
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
J|NUPB{{?fvP5S@<

diff --git a/resources/lib/libraries/mutagen/asf/_attrs.py b/resources/lib/libraries/mutagen/asf/_attrs.py
deleted file mode 100644
index 4621c9fa..00000000
--- a/resources/lib/libraries/mutagen/asf/_attrs.py
+++ /dev/null
@@ -1,438 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2005-2006  Joe Wreschnig
-# Copyright (C) 2006-2007  Lukas Lalinsky
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-import sys
-import struct
-
-from mutagen._compat import swap_to_string, text_type, PY2, reraise
-from mutagen._util import total_ordering
-
-from ._util import ASFError
-
-
-class ASFBaseAttribute(object):
-    """Generic attribute."""
-
-    TYPE = None
-
-    _TYPES = {}
-
-    value = None
-    """The Python value of this attribute (type depends on the class)"""
-
-    language = None
-    """Language"""
-
-    stream = None
-    """Stream"""
-
-    def __init__(self, value=None, data=None, language=None,
-                 stream=None, **kwargs):
-        self.language = language
-        self.stream = stream
-        if data:
-            self.value = self.parse(data, **kwargs)
-        else:
-            if value is None:
-                # we used to support not passing any args and instead assign
-                # them later, keep that working..
-                self.value = None
-            else:
-                self.value = self._validate(value)
-
-    @classmethod
-    def _register(cls, other):
-        cls._TYPES[other.TYPE] = other
-        return other
-
-    @classmethod
-    def _get_type(cls, type_):
-        """Raises KeyError"""
-
-        return cls._TYPES[type_]
-
-    def _validate(self, value):
-        """Raises TypeError or ValueError in case the user supplied value
-        isn't valid.
-        """
-
-        return value
-
-    def data_size(self):
-        raise NotImplementedError
-
-    def __repr__(self):
-        name = "%s(%r" % (type(self).__name__, self.value)
-        if self.language:
-            name += ", language=%d" % self.language
-        if self.stream:
-            name += ", stream=%d" % self.stream
-        name += ")"
-        return name
-
-    def render(self, name):
-        name = name.encode("utf-16-le") + b"\x00\x00"
-        data = self._render()
-        return (struct.pack("<H", len(name)) + name +
-                struct.pack("<HH", self.TYPE, len(data)) + data)
-
-    def render_m(self, name):
-        name = name.encode("utf-16-le") + b"\x00\x00"
-        if self.TYPE == 2:
-            data = self._render(dword=False)
-        else:
-            data = self._render()
-        return (struct.pack("<HHHHI", 0, self.stream or 0, len(name),
-                            self.TYPE, len(data)) + name + data)
-
-    def render_ml(self, name):
-        name = name.encode("utf-16-le") + b"\x00\x00"
-        if self.TYPE == 2:
-            data = self._render(dword=False)
-        else:
-            data = self._render()
-
-        return (struct.pack("<HHHHI", self.language or 0, self.stream or 0,
-                            len(name), self.TYPE, len(data)) + name + data)
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFUnicodeAttribute(ASFBaseAttribute):
-    """Unicode string attribute.
-
-    ::
-
-        ASFUnicodeAttribute(u'some text')
-    """
-
-    TYPE = 0x0000
-
-    def parse(self, data):
-        try:
-            return data.decode("utf-16-le").strip("\x00")
-        except UnicodeDecodeError as e:
-            reraise(ASFError, e, sys.exc_info()[2])
-
-    def _validate(self, value):
-        if not isinstance(value, text_type):
-            if PY2:
-                return value.decode("utf-8")
-            else:
-                raise TypeError("%r not str" % value)
-        return value
-
-    def _render(self):
-        return self.value.encode("utf-16-le") + b"\x00\x00"
-
-    def data_size(self):
-        return len(self._render())
-
-    def __bytes__(self):
-        return self.value.encode("utf-16-le")
-
-    def __str__(self):
-        return self.value
-
-    def __eq__(self, other):
-        return text_type(self) == other
-
-    def __lt__(self, other):
-        return text_type(self) < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFByteArrayAttribute(ASFBaseAttribute):
-    """Byte array attribute.
-
-    ::
-
-        ASFByteArrayAttribute(b'1234')
-    """
-    TYPE = 0x0001
-
-    def parse(self, data):
-        assert isinstance(data, bytes)
-        return data
-
-    def _render(self):
-        assert isinstance(self.value, bytes)
-        return self.value
-
-    def _validate(self, value):
-        if not isinstance(value, bytes):
-            raise TypeError("must be bytes/str: %r" % value)
-        return value
-
-    def data_size(self):
-        return len(self.value)
-
-    def __bytes__(self):
-        return self.value
-
-    def __str__(self):
-        return "[binary data (%d bytes)]" % len(self.value)
-
-    def __eq__(self, other):
-        return self.value == other
-
-    def __lt__(self, other):
-        return self.value < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFBoolAttribute(ASFBaseAttribute):
-    """Bool attribute.
-
-    ::
-
-        ASFBoolAttribute(True)
-    """
-
-    TYPE = 0x0002
-
-    def parse(self, data, dword=True):
-        if dword:
-            return struct.unpack("<I", data)[0] == 1
-        else:
-            return struct.unpack("<H", data)[0] == 1
-
-    def _render(self, dword=True):
-        if dword:
-            return struct.pack("<I", bool(self.value))
-        else:
-            return struct.pack("<H", bool(self.value))
-
-    def _validate(self, value):
-        return bool(value)
-
-    def data_size(self):
-        return 4
-
-    def __bool__(self):
-        return bool(self.value)
-
-    def __bytes__(self):
-        return text_type(self.value).encode('utf-8')
-
-    def __str__(self):
-        return text_type(self.value)
-
-    def __eq__(self, other):
-        return bool(self.value) == other
-
-    def __lt__(self, other):
-        return bool(self.value) < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFDWordAttribute(ASFBaseAttribute):
-    """DWORD attribute.
-
-    ::
-
-        ASFDWordAttribute(42)
-    """
-
-    TYPE = 0x0003
-
-    def parse(self, data):
-        return struct.unpack("<L", data)[0]
-
-    def _render(self):
-        return struct.pack("<L", self.value)
-
-    def _validate(self, value):
-        value = int(value)
-        if not 0 <= value <= 2 ** 32 - 1:
-            raise ValueError("Out of range")
-        return value
-
-    def data_size(self):
-        return 4
-
-    def __int__(self):
-        return self.value
-
-    def __bytes__(self):
-        return text_type(self.value).encode('utf-8')
-
-    def __str__(self):
-        return text_type(self.value)
-
-    def __eq__(self, other):
-        return int(self.value) == other
-
-    def __lt__(self, other):
-        return int(self.value) < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFQWordAttribute(ASFBaseAttribute):
-    """QWORD attribute.
-
-    ::
-
-        ASFQWordAttribute(42)
-    """
-
-    TYPE = 0x0004
-
-    def parse(self, data):
-        return struct.unpack("<Q", data)[0]
-
-    def _render(self):
-        return struct.pack("<Q", self.value)
-
-    def _validate(self, value):
-        value = int(value)
-        if not 0 <= value <= 2 ** 64 - 1:
-            raise ValueError("Out of range")
-        return value
-
-    def data_size(self):
-        return 8
-
-    def __int__(self):
-        return self.value
-
-    def __bytes__(self):
-        return text_type(self.value).encode('utf-8')
-
-    def __str__(self):
-        return text_type(self.value)
-
-    def __eq__(self, other):
-        return int(self.value) == other
-
-    def __lt__(self, other):
-        return int(self.value) < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFWordAttribute(ASFBaseAttribute):
-    """WORD attribute.
-
-    ::
-
-        ASFWordAttribute(42)
-    """
-
-    TYPE = 0x0005
-
-    def parse(self, data):
-        return struct.unpack("<H", data)[0]
-
-    def _render(self):
-        return struct.pack("<H", self.value)
-
-    def _validate(self, value):
-        value = int(value)
-        if not 0 <= value <= 2 ** 16 - 1:
-            raise ValueError("Out of range")
-        return value
-
-    def data_size(self):
-        return 2
-
-    def __int__(self):
-        return self.value
-
-    def __bytes__(self):
-        return text_type(self.value).encode('utf-8')
-
-    def __str__(self):
-        return text_type(self.value)
-
-    def __eq__(self, other):
-        return int(self.value) == other
-
-    def __lt__(self, other):
-        return int(self.value) < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-@ASFBaseAttribute._register
-@swap_to_string
-@total_ordering
-class ASFGUIDAttribute(ASFBaseAttribute):
-    """GUID attribute."""
-
-    TYPE = 0x0006
-
-    def parse(self, data):
-        assert isinstance(data, bytes)
-        return data
-
-    def _render(self):
-        assert isinstance(self.value, bytes)
-        return self.value
-
-    def _validate(self, value):
-        if not isinstance(value, bytes):
-            raise TypeError("must be bytes/str: %r" % value)
-        return value
-
-    def data_size(self):
-        return len(self.value)
-
-    def __bytes__(self):
-        return self.value
-
-    def __str__(self):
-        return repr(self.value)
-
-    def __eq__(self, other):
-        return self.value == other
-
-    def __lt__(self, other):
-        return self.value < other
-
-    __hash__ = ASFBaseAttribute.__hash__
-
-
-def ASFValue(value, kind, **kwargs):
-    """Create a tag value of a specific kind.
-
-    ::
-
-        ASFValue(u"My Value", UNICODE)
-
-    :rtype: ASFBaseAttribute
-    :raises TypeError: in case a wrong type was passed
-    :raises ValueError: in case the value can't be be represented as ASFValue.
-    """
-
-    try:
-        attr_type = ASFBaseAttribute._get_type(kind)
-    except KeyError:
-        raise ValueError("Unknown value type %r" % kind)
-    else:
-        return attr_type(value=value, **kwargs)
diff --git a/resources/lib/libraries/mutagen/asf/_objects.py b/resources/lib/libraries/mutagen/asf/_objects.py
deleted file mode 100644
index ed942679..00000000
--- a/resources/lib/libraries/mutagen/asf/_objects.py
+++ /dev/null
@@ -1,437 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2005-2006  Joe Wreschnig
-# Copyright (C) 2006-2007  Lukas Lalinsky
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-import struct
-
-from mutagen._util import cdata, get_size
-from mutagen._compat import text_type, xrange, izip
-from mutagen._tags import PaddingInfo
-
-from ._util import guid2bytes, bytes2guid, CODECS, ASFError, ASFHeaderError
-from ._attrs import ASFBaseAttribute, ASFUnicodeAttribute
-
-
-class BaseObject(object):
-    """Base ASF object."""
-
-    GUID = None
-    _TYPES = {}
-
-    def __init__(self):
-        self.objects = []
-        self.data = b""
-
-    def parse(self, asf, data):
-        self.data = data
-
-    def render(self, asf):
-        data = self.GUID + struct.pack("<Q", len(self.data) + 24) + self.data
-        return data
-
-    def get_child(self, guid):
-        for obj in self.objects:
-            if obj.GUID == guid:
-                return obj
-        return None
-
-    @classmethod
-    def _register(cls, other):
-        cls._TYPES[other.GUID] = other
-        return other
-
-    @classmethod
-    def _get_object(cls, guid):
-        if guid in cls._TYPES:
-            return cls._TYPES[guid]()
-        else:
-            return UnknownObject(guid)
-
-    def __repr__(self):
-        return "<%s GUID=%s objects=%r>" % (
-            type(self).__name__, bytes2guid(self.GUID), self.objects)
-
-    def pprint(self):
-        l = []
-        l.append("%s(%s)" % (type(self).__name__, bytes2guid(self.GUID)))
-        for o in self.objects:
-            for e in o.pprint().splitlines():
-                l.append("  " + e)
-        return "\n".join(l)
-
-
-class UnknownObject(BaseObject):
-    """Unknown ASF object."""
-
-    def __init__(self, guid):
-        super(UnknownObject, self).__init__()
-        assert isinstance(guid, bytes)
-        self.GUID = guid
-
-
-@BaseObject._register
-class HeaderObject(BaseObject):
-    """ASF header."""
-
-    GUID = guid2bytes("75B22630-668E-11CF-A6D9-00AA0062CE6C")
-
-    @classmethod
-    def parse_full(cls, asf, fileobj):
-        """Raises ASFHeaderError"""
-
-        header = cls()
-
-        size, num_objects = cls.parse_size(fileobj)
-        for i in xrange(num_objects):
-            guid, size = struct.unpack("<16sQ", fileobj.read(24))
-            obj = BaseObject._get_object(guid)
-            data = fileobj.read(size - 24)
-            obj.parse(asf, data)
-            header.objects.append(obj)
-
-        return header
-
-    @classmethod
-    def parse_size(cls, fileobj):
-        """Returns (size, num_objects)
-
-        Raises ASFHeaderError
-        """
-
-        header = fileobj.read(30)
-        if len(header) != 30 or header[:16] != HeaderObject.GUID:
-            raise ASFHeaderError("Not an ASF file.")
-
-        return struct.unpack("<QL", header[16:28])
-
-    def render_full(self, asf, fileobj, available, padding_func):
-        # Render everything except padding
-        num_objects = 0
-        data = bytearray()
-        for obj in self.objects:
-            if obj.GUID == PaddingObject.GUID:
-                continue
-            data += obj.render(asf)
-            num_objects += 1
-
-        # calculate how much space we need at least
-        padding_obj = PaddingObject()
-        header_size = len(HeaderObject.GUID) + 14
-        padding_overhead = len(padding_obj.render(asf))
-        needed_size = len(data) + header_size + padding_overhead
-
-        # ask the user for padding adjustments
-        file_size = get_size(fileobj)
-        content_size = file_size - available
-        assert content_size >= 0
-        info = PaddingInfo(available - needed_size, content_size)
-
-        # add padding
-        padding = info._get_padding(padding_func)
-        padding_obj.parse(asf, b"\x00" * padding)
-        data += padding_obj.render(asf)
-        num_objects += 1
-
-        data = (HeaderObject.GUID +
-                struct.pack("<QL", len(data) + 30, num_objects) +
-                b"\x01\x02" + data)
-
-        return data
-
-    def parse(self, asf, data):
-        raise NotImplementedError
-
-    def render(self, asf):
-        raise NotImplementedError
-
-
-@BaseObject._register
-class ContentDescriptionObject(BaseObject):
-    """Content description."""
-
-    GUID = guid2bytes("75B22633-668E-11CF-A6D9-00AA0062CE6C")
-
-    NAMES = [
-        u"Title",
-        u"Author",
-        u"Copyright",
-        u"Description",
-        u"Rating",
-    ]
-
-    def parse(self, asf, data):
-        super(ContentDescriptionObject, self).parse(asf, data)
-        lengths = struct.unpack("<HHHHH", data[:10])
-        texts = []
-        pos = 10
-        for length in lengths:
-            end = pos + length
-            if length > 0:
-                texts.append(data[pos:end].decode("utf-16-le").strip(u"\x00"))
-            else:
-                texts.append(None)
-            pos = end
-
-        for key, value in izip(self.NAMES, texts):
-            if value is not None:
-                value = ASFUnicodeAttribute(value=value)
-                asf._tags.setdefault(self.GUID, []).append((key, value))
-
-    def render(self, asf):
-        def render_text(name):
-            value = asf.to_content_description.get(name)
-            if value is not None:
-                return text_type(value).encode("utf-16-le") + b"\x00\x00"
-            else:
-                return b""
-
-        texts = [render_text(x) for x in self.NAMES]
-        data = struct.pack("<HHHHH", *map(len, texts)) + b"".join(texts)
-        return self.GUID + struct.pack("<Q", 24 + len(data)) + data
-
-
-@BaseObject._register
-class ExtendedContentDescriptionObject(BaseObject):
-    """Extended content description."""
-
-    GUID = guid2bytes("D2D0A440-E307-11D2-97F0-00A0C95EA850")
-
-    def parse(self, asf, data):
-        super(ExtendedContentDescriptionObject, self).parse(asf, data)
-        num_attributes, = struct.unpack("<H", data[0:2])
-        pos = 2
-        for i in xrange(num_attributes):
-            name_length, = struct.unpack("<H", data[pos:pos + 2])
-            pos += 2
-            name = data[pos:pos + name_length]
-            name = name.decode("utf-16-le").strip("\x00")
-            pos += name_length
-            value_type, value_length = struct.unpack("<HH", data[pos:pos + 4])
-            pos += 4
-            value = data[pos:pos + value_length]
-            pos += value_length
-            attr = ASFBaseAttribute._get_type(value_type)(data=value)
-            asf._tags.setdefault(self.GUID, []).append((name, attr))
-
-    def render(self, asf):
-        attrs = asf.to_extended_content_description.items()
-        data = b"".join(attr.render(name) for (name, attr) in attrs)
-        data = struct.pack("<QH", 26 + len(data), len(attrs)) + data
-        return self.GUID + data
-
-
-@BaseObject._register
-class FilePropertiesObject(BaseObject):
-    """File properties."""
-
-    GUID = guid2bytes("8CABDCA1-A947-11CF-8EE4-00C00C205365")
-
-    def parse(self, asf, data):
-        super(FilePropertiesObject, self).parse(asf, data)
-        length, _, preroll = struct.unpack("<QQQ", data[40:64])
-        # there are files where preroll is larger than length, limit to >= 0
-        asf.info.length = max((length / 10000000.0) - (preroll / 1000.0), 0.0)
-
-
-@BaseObject._register
-class StreamPropertiesObject(BaseObject):
-    """Stream properties."""
-
-    GUID = guid2bytes("B7DC0791-A9B7-11CF-8EE6-00C00C205365")
-
-    def parse(self, asf, data):
-        super(StreamPropertiesObject, self).parse(asf, data)
-        channels, sample_rate, bitrate = struct.unpack("<HII", data[56:66])
-        asf.info.channels = channels
-        asf.info.sample_rate = sample_rate
-        asf.info.bitrate = bitrate * 8
-
-
-@BaseObject._register
-class CodecListObject(BaseObject):
-    """Codec List"""
-
-    GUID = guid2bytes("86D15240-311D-11D0-A3A4-00A0C90348F6")
-
-    def _parse_entry(self, data, offset):
-        """can raise cdata.error"""
-
-        type_, offset = cdata.uint16_le_from(data, offset)
-
-        units, offset = cdata.uint16_le_from(data, offset)
-        # utf-16 code units, not characters..
-        next_offset = offset + units * 2
-        try:
-            name = data[offset:next_offset].decode("utf-16-le").strip("\x00")
-        except UnicodeDecodeError:
-            name = u""
-        offset = next_offset
-
-        units, offset = cdata.uint16_le_from(data, offset)
-        next_offset = offset + units * 2
-        try:
-            desc = data[offset:next_offset].decode("utf-16-le").strip("\x00")
-        except UnicodeDecodeError:
-            desc = u""
-        offset = next_offset
-
-        bytes_, offset = cdata.uint16_le_from(data, offset)
-        next_offset = offset + bytes_
-        codec = u""
-        if bytes_ == 2:
-            codec_id = cdata.uint16_le_from(data, offset)[0]
-            if codec_id in CODECS:
-                codec = CODECS[codec_id]
-        offset = next_offset
-
-        return offset, type_, name, desc, codec
-
-    def parse(self, asf, data):
-        super(CodecListObject, self).parse(asf, data)
-
-        offset = 16
-        count, offset = cdata.uint32_le_from(data, offset)
-        for i in xrange(count):
-            try:
-                offset, type_, name, desc, codec = \
-                    self._parse_entry(data, offset)
-            except cdata.error:
-                raise ASFError("invalid codec entry")
-
-            # go with the first audio entry
-            if type_ == 2:
-                name = name.strip()
-                desc = desc.strip()
-                asf.info.codec_type = codec
-                asf.info.codec_name = name
-                asf.info.codec_description = desc
-                return
-
-
-@BaseObject._register
-class PaddingObject(BaseObject):
-    """Padding object"""
-
-    GUID = guid2bytes("1806D474-CADF-4509-A4BA-9AABCB96AAE8")
-
-
-@BaseObject._register
-class StreamBitratePropertiesObject(BaseObject):
-    """Stream bitrate properties"""
-
-    GUID = guid2bytes("7BF875CE-468D-11D1-8D82-006097C9A2B2")
-
-
-@BaseObject._register
-class ContentEncryptionObject(BaseObject):
-    """Content encryption"""
-
-    GUID = guid2bytes("2211B3FB-BD23-11D2-B4B7-00A0C955FC6E")
-
-
-@BaseObject._register
-class ExtendedContentEncryptionObject(BaseObject):
-    """Extended content encryption"""
-
-    GUID = guid2bytes("298AE614-2622-4C17-B935-DAE07EE9289C")
-
-
-@BaseObject._register
-class HeaderExtensionObject(BaseObject):
-    """Header extension."""
-
-    GUID = guid2bytes("5FBF03B5-A92E-11CF-8EE3-00C00C205365")
-
-    def parse(self, asf, data):
-        super(HeaderExtensionObject, self).parse(asf, data)
-        datasize, = struct.unpack("<I", data[18:22])
-        datapos = 0
-        while datapos < datasize:
-            guid, size = struct.unpack(
-                "<16sQ", data[22 + datapos:22 + datapos + 24])
-            obj = BaseObject._get_object(guid)
-            obj.parse(asf, data[22 + datapos + 24:22 + datapos + size])
-            self.objects.append(obj)
-            datapos += size
-
-    def render(self, asf):
-        data = bytearray()
-        for obj in self.objects:
-            # some files have the padding in the extension header, but we
-            # want to add it at the end of the top level header. Just
-            # skip padding at this level.
-            if obj.GUID == PaddingObject.GUID:
-                continue
-            data += obj.render(asf)
-        return (self.GUID + struct.pack("<Q", 24 + 16 + 6 + len(data)) +
-                b"\x11\xD2\xD3\xAB\xBA\xA9\xcf\x11" +
-                b"\x8E\xE6\x00\xC0\x0C\x20\x53\x65" +
-                b"\x06\x00" + struct.pack("<I", len(data)) + data)
-
-
-@BaseObject._register
-class MetadataObject(BaseObject):
-    """Metadata description."""
-
-    GUID = guid2bytes("C5F8CBEA-5BAF-4877-8467-AA8C44FA4CCA")
-
-    def parse(self, asf, data):
-        super(MetadataObject, self).parse(asf, data)
-        num_attributes, = struct.unpack("<H", data[0:2])
-        pos = 2
-        for i in xrange(num_attributes):
-            (reserved, stream, name_length, value_type,
-             value_length) = struct.unpack("<HHHHI", data[pos:pos + 12])
-            pos += 12
-            name = data[pos:pos + name_length]
-            name = name.decode("utf-16-le").strip("\x00")
-            pos += name_length
-            value = data[pos:pos + value_length]
-            pos += value_length
-            args = {'data': value, 'stream': stream}
-            if value_type == 2:
-                args['dword'] = False
-            attr = ASFBaseAttribute._get_type(value_type)(**args)
-            asf._tags.setdefault(self.GUID, []).append((name, attr))
-
-    def render(self, asf):
-        attrs = asf.to_metadata.items()
-        data = b"".join([attr.render_m(name) for (name, attr) in attrs])
-        return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) +
-                data)
-
-
-@BaseObject._register
-class MetadataLibraryObject(BaseObject):
-    """Metadata library description."""
-
-    GUID = guid2bytes("44231C94-9498-49D1-A141-1D134E457054")
-
-    def parse(self, asf, data):
-        super(MetadataLibraryObject, self).parse(asf, data)
-        num_attributes, = struct.unpack("<H", data[0:2])
-        pos = 2
-        for i in xrange(num_attributes):
-            (language, stream, name_length, value_type,
-             value_length) = struct.unpack("<HHHHI", data[pos:pos + 12])
-            pos += 12
-            name = data[pos:pos + name_length]
-            name = name.decode("utf-16-le").strip("\x00")
-            pos += name_length
-            value = data[pos:pos + value_length]
-            pos += value_length
-            args = {'data': value, 'language': language, 'stream': stream}
-            if value_type == 2:
-                args['dword'] = False
-            attr = ASFBaseAttribute._get_type(value_type)(**args)
-            asf._tags.setdefault(self.GUID, []).append((name, attr))
-
-    def render(self, asf):
-        attrs = asf.to_metadata_library
-        data = b"".join([attr.render_ml(name) for (name, attr) in attrs])
-        return (self.GUID + struct.pack("<QH", 26 + len(data), len(attrs)) +
-                data)
diff --git a/resources/lib/libraries/mutagen/asf/_util.py b/resources/lib/libraries/mutagen/asf/_util.py
deleted file mode 100644
index 42154bff..00000000
--- a/resources/lib/libraries/mutagen/asf/_util.py
+++ /dev/null
@@ -1,315 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2005-2006  Joe Wreschnig
-# Copyright (C) 2006-2007  Lukas Lalinsky
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-import struct
-
-from mutagen._util import MutagenError
-
-
-class error(IOError, MutagenError):
-    """Error raised by :mod:`mutagen.asf`"""
-
-
-class ASFError(error):
-    pass
-
-
-class ASFHeaderError(error):
-    pass
-
-
-def guid2bytes(s):
-    """Converts a GUID to the serialized bytes representation"""
-
-    assert isinstance(s, str)
-    assert len(s) == 36
-
-    p = struct.pack
-    return b"".join([
-        p("<IHH", int(s[:8], 16), int(s[9:13], 16), int(s[14:18], 16)),
-        p(">H", int(s[19:23], 16)),
-        p(">Q", int(s[24:], 16))[2:],
-        ])
-
-
-def bytes2guid(s):
-    """Converts a serialized GUID to a text GUID"""
-
-    assert isinstance(s, bytes)
-
-    u = struct.unpack
-    v = []
-    v.extend(u("<IHH", s[:8]))
-    v.extend(u(">HQ", s[8:10] + b"\x00\x00" + s[10:]))
-    return "%08X-%04X-%04X-%04X-%012X" % tuple(v)
-
-
-# Names from http://windows.microsoft.com/en-za/windows7/c00d10d1-[0-9A-F]{1,4}
-CODECS = {
-    0x0000: u"Unknown Wave Format",
-    0x0001: u"Microsoft PCM Format",
-    0x0002: u"Microsoft ADPCM Format",
-    0x0003: u"IEEE Float",
-    0x0004: u"Compaq Computer VSELP",
-    0x0005: u"IBM CVSD",
-    0x0006: u"Microsoft CCITT A-Law",
-    0x0007: u"Microsoft CCITT u-Law",
-    0x0008: u"Microsoft DTS",
-    0x0009: u"Microsoft DRM",
-    0x000A: u"Windows Media Audio 9 Voice",
-    0x000B: u"Windows Media Audio 10 Voice",
-    0x000C: u"OGG Vorbis",
-    0x000D: u"FLAC",
-    0x000E: u"MOT AMR",
-    0x000F: u"Nice Systems IMBE",
-    0x0010: u"OKI ADPCM",
-    0x0011: u"Intel IMA ADPCM",
-    0x0012: u"Videologic MediaSpace ADPCM",
-    0x0013: u"Sierra Semiconductor ADPCM",
-    0x0014: u"Antex Electronics G.723 ADPCM",
-    0x0015: u"DSP Solutions DIGISTD",
-    0x0016: u"DSP Solutions DIGIFIX",
-    0x0017: u"Dialogic OKI ADPCM",
-    0x0018: u"MediaVision ADPCM",
-    0x0019: u"Hewlett-Packard CU codec",
-    0x001A: u"Hewlett-Packard Dynamic Voice",
-    0x0020: u"Yamaha ADPCM",
-    0x0021: u"Speech Compression SONARC",
-    0x0022: u"DSP Group True Speech",
-    0x0023: u"Echo Speech EchoSC1",
-    0x0024: u"Ahead Inc. Audiofile AF36",
-    0x0025: u"Audio Processing Technology APTX",
-    0x0026: u"Ahead Inc. AudioFile AF10",
-    0x0027: u"Aculab Prosody 1612",
-    0x0028: u"Merging Technologies S.A. LRC",
-    0x0030: u"Dolby Labs AC2",
-    0x0031: u"Microsoft GSM 6.10",
-    0x0032: u"Microsoft MSNAudio",
-    0x0033: u"Antex Electronics ADPCME",
-    0x0034: u"Control Resources VQLPC",
-    0x0035: u"DSP Solutions Digireal",
-    0x0036: u"DSP Solutions DigiADPCM",
-    0x0037: u"Control Resources CR10",
-    0x0038: u"Natural MicroSystems VBXADPCM",
-    0x0039: u"Crystal Semiconductor IMA ADPCM",
-    0x003A: u"Echo Speech EchoSC3",
-    0x003B: u"Rockwell ADPCM",
-    0x003C: u"Rockwell DigiTalk",
-    0x003D: u"Xebec Multimedia Solutions",
-    0x0040: u"Antex Electronics G.721 ADPCM",
-    0x0041: u"Antex Electronics G.728 CELP",
-    0x0042: u"Intel G.723",
-    0x0043: u"Intel G.723.1",
-    0x0044: u"Intel G.729 Audio",
-    0x0045: u"Sharp G.726 Audio",
-    0x0050: u"Microsoft MPEG-1",
-    0x0052: u"InSoft RT24",
-    0x0053: u"InSoft PAC",
-    0x0055: u"MP3 - MPEG Layer III",
-    0x0059: u"Lucent G.723",
-    0x0060: u"Cirrus Logic",
-    0x0061: u"ESS Technology ESPCM",
-    0x0062: u"Voxware File-Mode",
-    0x0063: u"Canopus Atrac",
-    0x0064: u"APICOM G.726 ADPCM",
-    0x0065: u"APICOM G.722 ADPCM",
-    0x0066: u"Microsoft DSAT",
-    0x0067: u"Microsoft DSAT Display",
-    0x0069: u"Voxware Byte Aligned",
-    0x0070: u"Voxware AC8",
-    0x0071: u"Voxware AC10",
-    0x0072: u"Voxware AC16",
-    0x0073: u"Voxware AC20",
-    0x0074: u"Voxware RT24 MetaVoice",
-    0x0075: u"Voxware RT29 MetaSound",
-    0x0076: u"Voxware RT29HW",
-    0x0077: u"Voxware VR12",
-    0x0078: u"Voxware VR18",
-    0x0079: u"Voxware TQ40",
-    0x007A: u"Voxware SC3",
-    0x007B: u"Voxware SC3",
-    0x0080: u"Softsound",
-    0x0081: u"Voxware TQ60",
-    0x0082: u"Microsoft MSRT24",
-    0x0083: u"AT&T Labs G.729A",
-    0x0084: u"Motion Pixels MVI MV12",
-    0x0085: u"DataFusion Systems G.726",
-    0x0086: u"DataFusion Systems GSM610",
-    0x0088: u"Iterated Systems ISIAudio",
-    0x0089: u"Onlive",
-    0x008A: u"Multitude FT SX20",
-    0x008B: u"Infocom ITS ACM G.721",
-    0x008C: u"Convedia G.729",
-    0x008D: u"Congruency Audio",
-    0x0091: u"Siemens Business Communications SBC24",
-    0x0092: u"Sonic Foundry Dolby AC3 SPDIF",
-    0x0093: u"MediaSonic G.723",
-    0x0094: u"Aculab Prosody 8KBPS",
-    0x0097: u"ZyXEL ADPCM",
-    0x0098: u"Philips LPCBB",
-    0x0099: u"Studer Professional Audio AG Packed",
-    0x00A0: u"Malden Electronics PHONYTALK",
-    0x00A1: u"Racal Recorder GSM",
-    0x00A2: u"Racal Recorder G720.a",
-    0x00A3: u"Racal Recorder G723.1",
-    0x00A4: u"Racal Recorder Tetra ACELP",
-    0x00B0: u"NEC AAC",
-    0x00FF: u"CoreAAC Audio",
-    0x0100: u"Rhetorex ADPCM",
-    0x0101: u"BeCubed Software IRAT",
-    0x0111: u"Vivo G.723",
-    0x0112: u"Vivo Siren",
-    0x0120: u"Philips CELP",
-    0x0121: u"Philips Grundig",
-    0x0123: u"Digital G.723",
-    0x0125: u"Sanyo ADPCM",
-    0x0130: u"Sipro Lab Telecom ACELP.net",
-    0x0131: u"Sipro Lab Telecom ACELP.4800",
-    0x0132: u"Sipro Lab Telecom ACELP.8V3",
-    0x0133: u"Sipro Lab Telecom ACELP.G.729",
-    0x0134: u"Sipro Lab Telecom ACELP.G.729A",
-    0x0135: u"Sipro Lab Telecom ACELP.KELVIN",
-    0x0136: u"VoiceAge AMR",
-    0x0140: u"Dictaphone G.726 ADPCM",
-    0x0141: u"Dictaphone CELP68",
-    0x0142: u"Dictaphone CELP54",
-    0x0150: u"Qualcomm PUREVOICE",
-    0x0151: u"Qualcomm HALFRATE",
-    0x0155: u"Ring Zero Systems TUBGSM",
-    0x0160: u"Windows Media Audio Standard",
-    0x0161: u"Windows Media Audio 9 Standard",
-    0x0162: u"Windows Media Audio 9 Professional",
-    0x0163: u"Windows Media Audio 9 Lossless",
-    0x0164: u"Windows Media Audio Pro over SPDIF",
-    0x0170: u"Unisys NAP ADPCM",
-    0x0171: u"Unisys NAP ULAW",
-    0x0172: u"Unisys NAP ALAW",
-    0x0173: u"Unisys NAP 16K",
-    0x0174: u"Sycom ACM SYC008",
-    0x0175: u"Sycom ACM SYC701 G725",
-    0x0176: u"Sycom ACM SYC701 CELP54",
-    0x0177: u"Sycom ACM SYC701 CELP68",
-    0x0178: u"Knowledge Adventure ADPCM",
-    0x0180: u"Fraunhofer IIS MPEG-2 AAC",
-    0x0190: u"Digital Theater Systems DTS",
-    0x0200: u"Creative Labs ADPCM",
-    0x0202: u"Creative Labs FastSpeech8",
-    0x0203: u"Creative Labs FastSpeech10",
-    0x0210: u"UHER informatic GmbH ADPCM",
-    0x0215: u"Ulead DV Audio",
-    0x0216: u"Ulead DV Audio",
-    0x0220: u"Quarterdeck",
-    0x0230: u"I-link Worldwide ILINK VC",
-    0x0240: u"Aureal Semiconductor RAW SPORT",
-    0x0249: u"Generic Passthru",
-    0x0250: u"Interactive Products HSX",
-    0x0251: u"Interactive Products RPELP",
-    0x0260: u"Consistent Software CS2",
-    0x0270: u"Sony SCX",
-    0x0271: u"Sony SCY",
-    0x0272: u"Sony ATRAC3",
-    0x0273: u"Sony SPC",
-    0x0280: u"Telum Audio",
-    0x0281: u"Telum IA Audio",
-    0x0285: u"Norcom Voice Systems ADPCM",
-    0x0300: u"Fujitsu TOWNS SND",
-    0x0350: u"Micronas SC4 Speech",
-    0x0351: u"Micronas CELP833",
-    0x0400: u"Brooktree BTV Digital",
-    0x0401: u"Intel Music Coder",
-    0x0402: u"Intel Audio",
-    0x0450: u"QDesign Music",
-    0x0500: u"On2 AVC0 Audio",
-    0x0501: u"On2 AVC1 Audio",
-    0x0680: u"AT&T Labs VME VMPCM",
-    0x0681: u"AT&T Labs TPC",
-    0x08AE: u"ClearJump Lightwave Lossless",
-    0x1000: u"Olivetti GSM",
-    0x1001: u"Olivetti ADPCM",
-    0x1002: u"Olivetti CELP",
-    0x1003: u"Olivetti SBC",
-    0x1004: u"Olivetti OPR",
-    0x1100: u"Lernout & Hauspie",
-    0x1101: u"Lernout & Hauspie CELP",
-    0x1102: u"Lernout & Hauspie SBC8",
-    0x1103: u"Lernout & Hauspie SBC12",
-    0x1104: u"Lernout & Hauspie SBC16",
-    0x1400: u"Norris Communication",
-    0x1401: u"ISIAudio",
-    0x1500: u"AT&T Labs Soundspace Music Compression",
-    0x1600: u"Microsoft MPEG ADTS AAC",
-    0x1601: u"Microsoft MPEG RAW AAC",
-    0x1608: u"Nokia MPEG ADTS AAC",
-    0x1609: u"Nokia MPEG RAW AAC",
-    0x181C: u"VoxWare MetaVoice RT24",
-    0x1971: u"Sonic Foundry Lossless",
-    0x1979: u"Innings Telecom ADPCM",
-    0x1FC4: u"NTCSoft ALF2CD ACM",
-    0x2000: u"Dolby AC3",
-    0x2001: u"DTS",
-    0x4143: u"Divio AAC",
-    0x4201: u"Nokia Adaptive Multi-Rate",
-    0x4243: u"Divio G.726",
-    0x4261: u"ITU-T H.261",
-    0x4263: u"ITU-T H.263",
-    0x4264: u"ITU-T H.264",
-    0x674F: u"Ogg Vorbis Mode 1",
-    0x6750: u"Ogg Vorbis Mode 2",
-    0x6751: u"Ogg Vorbis Mode 3",
-    0x676F: u"Ogg Vorbis Mode 1+",
-    0x6770: u"Ogg Vorbis Mode 2+",
-    0x6771: u"Ogg Vorbis Mode 3+",
-    0x7000: u"3COM NBX Audio",
-    0x706D: u"FAAD AAC Audio",
-    0x77A1: u"True Audio Lossless Audio",
-    0x7A21: u"GSM-AMR CBR 3GPP Audio",
-    0x7A22: u"GSM-AMR VBR 3GPP Audio",
-    0xA100: u"Comverse Infosys G723.1",
-    0xA101: u"Comverse Infosys AVQSBC",
-    0xA102: u"Comverse Infosys SBC",
-    0xA103: u"Symbol Technologies G729a",
-    0xA104: u"VoiceAge AMR WB",
-    0xA105: u"Ingenient Technologies G.726",
-    0xA106: u"ISO/MPEG-4 Advanced Audio Coding (AAC)",
-    0xA107: u"Encore Software Ltd's G.726",
-    0xA108: u"ZOLL Medical Corporation ASAO",
-    0xA109: u"Speex Voice",
-    0xA10A: u"Vianix MASC Speech Compression",
-    0xA10B: u"Windows Media 9 Spectrum Analyzer Output",
-    0xA10C: u"Media Foundation Spectrum Analyzer Output",
-    0xA10D: u"GSM 6.10 (Full-Rate) Speech",
-    0xA10E: u"GSM 6.20 (Half-Rate) Speech",
-    0xA10F: u"GSM 6.60 (Enchanced Full-Rate) Speech",
-    0xA110: u"GSM 6.90 (Adaptive Multi-Rate) Speech",
-    0xA111: u"GSM Adaptive Multi-Rate WideBand Speech",
-    0xA112: u"Polycom G.722",
-    0xA113: u"Polycom G.728",
-    0xA114: u"Polycom G.729a",
-    0xA115: u"Polycom Siren",
-    0xA116: u"Global IP Sound ILBC",
-    0xA117: u"Radio Time Time Shifted Radio",
-    0xA118: u"Nice Systems ACA",
-    0xA119: u"Nice Systems ADPCM",
-    0xA11A: u"Vocord Group ITU-T G.721",
-    0xA11B: u"Vocord Group ITU-T G.726",
-    0xA11C: u"Vocord Group ITU-T G.722.1",
-    0xA11D: u"Vocord Group ITU-T G.728",
-    0xA11E: u"Vocord Group ITU-T G.729",
-    0xA11F: u"Vocord Group ITU-T G.729a",
-    0xA120: u"Vocord Group ITU-T G.723.1",
-    0xA121: u"Vocord Group LBC",
-    0xA122: u"Nice G.728",
-    0xA123: u"France Telecom G.729 ACM Audio",
-    0xA124: u"CODIAN Audio",
-    0xCC12: u"Intel YUV12 Codec",
-    0xCFCC: u"Digital Processing Systems Perception Motion JPEG",
-    0xD261: u"DEC H.261",
-    0xD263: u"DEC H.263",
-    0xFFFE: u"Extensible Wave Format",
-    0xFFFF: u"Unregistered",
-}
diff --git a/resources/lib/libraries/mutagen/easyid3.py b/resources/lib/libraries/mutagen/easyid3.py
deleted file mode 100644
index f8dd2de0..00000000
--- a/resources/lib/libraries/mutagen/easyid3.py
+++ /dev/null
@@ -1,534 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""Easier access to ID3 tags.
-
-EasyID3 is a wrapper around mutagen.id3.ID3 to make ID3 tags appear
-more like Vorbis or APEv2 tags.
-"""
-
-import mutagen.id3
-
-from ._compat import iteritems, text_type, PY2
-from mutagen import Metadata
-from mutagen._util import DictMixin, dict_match
-from mutagen.id3 import ID3, error, delete, ID3FileType
-
-
-__all__ = ['EasyID3', 'Open', 'delete']
-
-
-class EasyID3KeyError(KeyError, ValueError, error):
-    """Raised when trying to get/set an invalid key.
-
-    Subclasses both KeyError and ValueError for API compatibility,
-    catching KeyError is preferred.
-    """
-
-
-class EasyID3(DictMixin, Metadata):
-    """A file with an ID3 tag.
-
-    Like Vorbis comments, EasyID3 keys are case-insensitive ASCII
-    strings. Only a subset of ID3 frames are supported by default. Use
-    EasyID3.RegisterKey and its wrappers to support more.
-
-    You can also set the GetFallback, SetFallback, and DeleteFallback
-    to generic key getter/setter/deleter functions, which are called
-    if no specific handler is registered for a key. Additionally,
-    ListFallback can be used to supply an arbitrary list of extra
-    keys. These can be set on EasyID3 or on individual instances after
-    creation.
-
-    To use an EasyID3 class with mutagen.mp3.MP3::
-
-        from mutagen.mp3 import EasyMP3 as MP3
-        MP3(filename)
-
-    Because many of the attributes are constructed on the fly, things
-    like the following will not work::
-
-        ezid3["performer"].append("Joe")
-
-    Instead, you must do::
-
-        values = ezid3["performer"]
-        values.append("Joe")
-        ezid3["performer"] = values
-
-    """
-
-    Set = {}
-    Get = {}
-    Delete = {}
-    List = {}
-
-    # For compatibility.
-    valid_keys = Get
-
-    GetFallback = None
-    SetFallback = None
-    DeleteFallback = None
-    ListFallback = None
-
-    @classmethod
-    def RegisterKey(cls, key,
-                    getter=None, setter=None, deleter=None, lister=None):
-        """Register a new key mapping.
-
-        A key mapping is four functions, a getter, setter, deleter,
-        and lister. The key may be either a string or a glob pattern.
-
-        The getter, deleted, and lister receive an ID3 instance and
-        the requested key name. The setter also receives the desired
-        value, which will be a list of strings.
-
-        The getter, setter, and deleter are used to implement __getitem__,
-        __setitem__, and __delitem__.
-
-        The lister is used to implement keys(). It should return a
-        list of keys that are actually in the ID3 instance, provided
-        by its associated getter.
-        """
-        key = key.lower()
-        if getter is not None:
-            cls.Get[key] = getter
-        if setter is not None:
-            cls.Set[key] = setter
-        if deleter is not None:
-            cls.Delete[key] = deleter
-        if lister is not None:
-            cls.List[key] = lister
-
-    @classmethod
-    def RegisterTextKey(cls, key, frameid):
-        """Register a text key.
-
-        If the key you need to register is a simple one-to-one mapping
-        of ID3 frame name to EasyID3 key, then you can use this
-        function::
-
-            EasyID3.RegisterTextKey("title", "TIT2")
-        """
-        def getter(id3, key):
-            return list(id3[frameid])
-
-        def setter(id3, key, value):
-            try:
-                frame = id3[frameid]
-            except KeyError:
-                id3.add(mutagen.id3.Frames[frameid](encoding=3, text=value))
-            else:
-                frame.encoding = 3
-                frame.text = value
-
-        def deleter(id3, key):
-            del(id3[frameid])
-
-        cls.RegisterKey(key, getter, setter, deleter)
-
-    @classmethod
-    def RegisterTXXXKey(cls, key, desc):
-        """Register a user-defined text frame key.
-
-        Some ID3 tags are stored in TXXX frames, which allow a
-        freeform 'description' which acts as a subkey,
-        e.g. TXXX:BARCODE.::
-
-            EasyID3.RegisterTXXXKey('barcode', 'BARCODE').
-        """
-        frameid = "TXXX:" + desc
-
-        def getter(id3, key):
-            return list(id3[frameid])
-
-        def setter(id3, key, value):
-            try:
-                frame = id3[frameid]
-            except KeyError:
-                enc = 0
-                # Store 8859-1 if we can, per MusicBrainz spec.
-                for v in value:
-                    if v and max(v) > u'\x7f':
-                        enc = 3
-                        break
-
-                id3.add(mutagen.id3.TXXX(encoding=enc, text=value, desc=desc))
-            else:
-                frame.text = value
-
-        def deleter(id3, key):
-            del(id3[frameid])
-
-        cls.RegisterKey(key, getter, setter, deleter)
-
-    def __init__(self, filename=None):
-        self.__id3 = ID3()
-        if filename is not None:
-            self.load(filename)
-
-    load = property(lambda s: s.__id3.load,
-                    lambda s, v: setattr(s.__id3, 'load', v))
-
-    def save(self, *args, **kwargs):
-        # ignore v2_version until we support 2.3 here
-        kwargs.pop("v2_version", None)
-        self.__id3.save(*args, **kwargs)
-
-    delete = property(lambda s: s.__id3.delete,
-                      lambda s, v: setattr(s.__id3, 'delete', v))
-
-    filename = property(lambda s: s.__id3.filename,
-                        lambda s, fn: setattr(s.__id3, 'filename', fn))
-
-    size = property(lambda s: s.__id3.size,
-                    lambda s, fn: setattr(s.__id3, 'size', s))
-
-    def __getitem__(self, key):
-        key = key.lower()
-        func = dict_match(self.Get, key, self.GetFallback)
-        if func is not None:
-            return func(self.__id3, key)
-        else:
-            raise EasyID3KeyError("%r is not a valid key" % key)
-
-    def __setitem__(self, key, value):
-        key = key.lower()
-        if PY2:
-            if isinstance(value, basestring):
-                value = [value]
-        else:
-            if isinstance(value, text_type):
-                value = [value]
-        func = dict_match(self.Set, key, self.SetFallback)
-        if func is not None:
-            return func(self.__id3, key, value)
-        else:
-            raise EasyID3KeyError("%r is not a valid key" % key)
-
-    def __delitem__(self, key):
-        key = key.lower()
-        func = dict_match(self.Delete, key, self.DeleteFallback)
-        if func is not None:
-            return func(self.__id3, key)
-        else:
-            raise EasyID3KeyError("%r is not a valid key" % key)
-
-    def keys(self):
-        keys = []
-        for key in self.Get.keys():
-            if key in self.List:
-                keys.extend(self.List[key](self.__id3, key))
-            elif key in self:
-                keys.append(key)
-        if self.ListFallback is not None:
-            keys.extend(self.ListFallback(self.__id3, ""))
-        return keys
-
-    def pprint(self):
-        """Print tag key=value pairs."""
-        strings = []
-        for key in sorted(self.keys()):
-            values = self[key]
-            for value in values:
-                strings.append("%s=%s" % (key, value))
-        return "\n".join(strings)
-
-
-Open = EasyID3
-
-
-def genre_get(id3, key):
-    return id3["TCON"].genres
-
-
-def genre_set(id3, key, value):
-    try:
-        frame = id3["TCON"]
-    except KeyError:
-        id3.add(mutagen.id3.TCON(encoding=3, text=value))
-    else:
-        frame.encoding = 3
-        frame.genres = value
-
-
-def genre_delete(id3, key):
-    del(id3["TCON"])
-
-
-def date_get(id3, key):
-    return [stamp.text for stamp in id3["TDRC"].text]
-
-
-def date_set(id3, key, value):
-    id3.add(mutagen.id3.TDRC(encoding=3, text=value))
-
-
-def date_delete(id3, key):
-    del(id3["TDRC"])
-
-
-def original_date_get(id3, key):
-    return [stamp.text for stamp in id3["TDOR"].text]
-
-
-def original_date_set(id3, key, value):
-    id3.add(mutagen.id3.TDOR(encoding=3, text=value))
-
-
-def original_date_delete(id3, key):
-    del(id3["TDOR"])
-
-
-def performer_get(id3, key):
-    people = []
-    wanted_role = key.split(":", 1)[1]
-    try:
-        mcl = id3["TMCL"]
-    except KeyError:
-        raise KeyError(key)
-    for role, person in mcl.people:
-        if role == wanted_role:
-            people.append(person)
-    if people:
-        return people
-    else:
-        raise KeyError(key)
-
-
-def performer_set(id3, key, value):
-    wanted_role = key.split(":", 1)[1]
-    try:
-        mcl = id3["TMCL"]
-    except KeyError:
-        mcl = mutagen.id3.TMCL(encoding=3, people=[])
-        id3.add(mcl)
-    mcl.encoding = 3
-    people = [p for p in mcl.people if p[0] != wanted_role]
-    for v in value:
-        people.append((wanted_role, v))
-    mcl.people = people
-
-
-def performer_delete(id3, key):
-    wanted_role = key.split(":", 1)[1]
-    try:
-        mcl = id3["TMCL"]
-    except KeyError:
-        raise KeyError(key)
-    people = [p for p in mcl.people if p[0] != wanted_role]
-    if people == mcl.people:
-        raise KeyError(key)
-    elif people:
-        mcl.people = people
-    else:
-        del(id3["TMCL"])
-
-
-def performer_list(id3, key):
-    try:
-        mcl = id3["TMCL"]
-    except KeyError:
-        return []
-    else:
-        return list(set("performer:" + p[0] for p in mcl.people))
-
-
-def musicbrainz_trackid_get(id3, key):
-    return [id3["UFID:http://musicbrainz.org"].data.decode('ascii')]
-
-
-def musicbrainz_trackid_set(id3, key, value):
-    if len(value) != 1:
-        raise ValueError("only one track ID may be set per song")
-    value = value[0].encode('ascii')
-    try:
-        frame = id3["UFID:http://musicbrainz.org"]
-    except KeyError:
-        frame = mutagen.id3.UFID(owner="http://musicbrainz.org", data=value)
-        id3.add(frame)
-    else:
-        frame.data = value
-
-
-def musicbrainz_trackid_delete(id3, key):
-    del(id3["UFID:http://musicbrainz.org"])
-
-
-def website_get(id3, key):
-    urls = [frame.url for frame in id3.getall("WOAR")]
-    if urls:
-        return urls
-    else:
-        raise EasyID3KeyError(key)
-
-
-def website_set(id3, key, value):
-    id3.delall("WOAR")
-    for v in value:
-        id3.add(mutagen.id3.WOAR(url=v))
-
-
-def website_delete(id3, key):
-    id3.delall("WOAR")
-
-
-def gain_get(id3, key):
-    try:
-        frame = id3["RVA2:" + key[11:-5]]
-    except KeyError:
-        raise EasyID3KeyError(key)
-    else:
-        return [u"%+f dB" % frame.gain]
-
-
-def gain_set(id3, key, value):
-    if len(value) != 1:
-        raise ValueError(
-            "there must be exactly one gain value, not %r.", value)
-    gain = float(value[0].split()[0])
-    try:
-        frame = id3["RVA2:" + key[11:-5]]
-    except KeyError:
-        frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1)
-        id3.add(frame)
-    frame.gain = gain
-
-
-def gain_delete(id3, key):
-    try:
-        frame = id3["RVA2:" + key[11:-5]]
-    except KeyError:
-        pass
-    else:
-        if frame.peak:
-            frame.gain = 0.0
-        else:
-            del(id3["RVA2:" + key[11:-5]])
-
-
-def peak_get(id3, key):
-    try:
-        frame = id3["RVA2:" + key[11:-5]]
-    except KeyError:
-        raise EasyID3KeyError(key)
-    else:
-        return [u"%f" % frame.peak]
-
-
-def peak_set(id3, key, value):
-    if len(value) != 1:
-        raise ValueError(
-            "there must be exactly one peak value, not %r.", value)
-    peak = float(value[0])
-    if peak >= 2 or peak < 0:
-        raise ValueError("peak must be => 0 and < 2.")
-    try:
-        frame = id3["RVA2:" + key[11:-5]]
-    except KeyError:
-        frame = mutagen.id3.RVA2(desc=key[11:-5], gain=0, peak=0, channel=1)
-        id3.add(frame)
-    frame.peak = peak
-
-
-def peak_delete(id3, key):
-    try:
-        frame = id3["RVA2:" + key[11:-5]]
-    except KeyError:
-        pass
-    else:
-        if frame.gain:
-            frame.peak = 0.0
-        else:
-            del(id3["RVA2:" + key[11:-5]])
-
-
-def peakgain_list(id3, key):
-    keys = []
-    for frame in id3.getall("RVA2"):
-        keys.append("replaygain_%s_gain" % frame.desc)
-        keys.append("replaygain_%s_peak" % frame.desc)
-    return keys
-
-for frameid, key in iteritems({
-    "TALB": "album",
-    "TBPM": "bpm",
-    "TCMP": "compilation",  # iTunes extension
-    "TCOM": "composer",
-    "TCOP": "copyright",
-    "TENC": "encodedby",
-    "TEXT": "lyricist",
-    "TLEN": "length",
-    "TMED": "media",
-    "TMOO": "mood",
-    "TIT2": "title",
-    "TIT3": "version",
-    "TPE1": "artist",
-    "TPE2": "performer",
-    "TPE3": "conductor",
-    "TPE4": "arranger",
-    "TPOS": "discnumber",
-    "TPUB": "organization",
-    "TRCK": "tracknumber",
-    "TOLY": "author",
-    "TSO2": "albumartistsort",  # iTunes extension
-    "TSOA": "albumsort",
-    "TSOC": "composersort",  # iTunes extension
-    "TSOP": "artistsort",
-    "TSOT": "titlesort",
-    "TSRC": "isrc",
-    "TSST": "discsubtitle",
-    "TLAN": "language",
-}):
-    EasyID3.RegisterTextKey(key, frameid)
-
-EasyID3.RegisterKey("genre", genre_get, genre_set, genre_delete)
-EasyID3.RegisterKey("date", date_get, date_set, date_delete)
-EasyID3.RegisterKey("originaldate", original_date_get, original_date_set,
-                    original_date_delete)
-EasyID3.RegisterKey(
-    "performer:*", performer_get, performer_set, performer_delete,
-    performer_list)
-EasyID3.RegisterKey("musicbrainz_trackid", musicbrainz_trackid_get,
-                    musicbrainz_trackid_set, musicbrainz_trackid_delete)
-EasyID3.RegisterKey("website", website_get, website_set, website_delete)
-EasyID3.RegisterKey(
-    "replaygain_*_gain", gain_get, gain_set, gain_delete, peakgain_list)
-EasyID3.RegisterKey("replaygain_*_peak", peak_get, peak_set, peak_delete)
-
-# At various times, information for this came from
-# http://musicbrainz.org/docs/specs/metadata_tags.html
-# http://bugs.musicbrainz.org/ticket/1383
-# http://musicbrainz.org/doc/MusicBrainzTag
-for desc, key in iteritems({
-    u"MusicBrainz Artist Id": "musicbrainz_artistid",
-    u"MusicBrainz Album Id": "musicbrainz_albumid",
-    u"MusicBrainz Album Artist Id": "musicbrainz_albumartistid",
-    u"MusicBrainz TRM Id": "musicbrainz_trmid",
-    u"MusicIP PUID": "musicip_puid",
-    u"MusicMagic Fingerprint": "musicip_fingerprint",
-    u"MusicBrainz Album Status": "musicbrainz_albumstatus",
-    u"MusicBrainz Album Type": "musicbrainz_albumtype",
-    u"MusicBrainz Album Release Country": "releasecountry",
-    u"MusicBrainz Disc Id": "musicbrainz_discid",
-    u"ASIN": "asin",
-    u"ALBUMARTISTSORT": "albumartistsort",
-    u"BARCODE": "barcode",
-    u"CATALOGNUMBER": "catalognumber",
-    u"MusicBrainz Release Track Id": "musicbrainz_releasetrackid",
-    u"MusicBrainz Release Group Id": "musicbrainz_releasegroupid",
-    u"MusicBrainz Work Id": "musicbrainz_workid",
-    u"Acoustid Fingerprint": "acoustid_fingerprint",
-    u"Acoustid Id": "acoustid_id",
-}):
-    EasyID3.RegisterTXXXKey(key, desc)
-
-
-class EasyID3FileType(ID3FileType):
-    """Like ID3FileType, but uses EasyID3 for tags."""
-    ID3 = EasyID3
diff --git a/resources/lib/libraries/mutagen/easymp4.py b/resources/lib/libraries/mutagen/easymp4.py
deleted file mode 100644
index b965f37d..00000000
--- a/resources/lib/libraries/mutagen/easymp4.py
+++ /dev/null
@@ -1,285 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2009  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-from mutagen import Metadata
-from mutagen._util import DictMixin, dict_match
-from mutagen.mp4 import MP4, MP4Tags, error, delete
-from ._compat import PY2, text_type, PY3
-
-
-__all__ = ["EasyMP4Tags", "EasyMP4", "delete", "error"]
-
-
-class EasyMP4KeyError(error, KeyError, ValueError):
-    pass
-
-
-class EasyMP4Tags(DictMixin, Metadata):
-    """A file with MPEG-4 iTunes metadata.
-
-    Like Vorbis comments, EasyMP4Tags keys are case-insensitive ASCII
-    strings, and values are a list of Unicode strings (and these lists
-    are always of length 0 or 1).
-
-    If you need access to the full MP4 metadata feature set, you should use
-    MP4, not EasyMP4.
-    """
-
-    Set = {}
-    Get = {}
-    Delete = {}
-    List = {}
-
-    def __init__(self, *args, **kwargs):
-        self.__mp4 = MP4Tags(*args, **kwargs)
-        self.load = self.__mp4.load
-        self.save = self.__mp4.save
-        self.delete = self.__mp4.delete
-        self._padding = self.__mp4._padding
-
-    filename = property(lambda s: s.__mp4.filename,
-                        lambda s, fn: setattr(s.__mp4, 'filename', fn))
-
-    @classmethod
-    def RegisterKey(cls, key,
-                    getter=None, setter=None, deleter=None, lister=None):
-        """Register a new key mapping.
-
-        A key mapping is four functions, a getter, setter, deleter,
-        and lister. The key may be either a string or a glob pattern.
-
-        The getter, deleted, and lister receive an MP4Tags instance
-        and the requested key name. The setter also receives the
-        desired value, which will be a list of strings.
-
-        The getter, setter, and deleter are used to implement __getitem__,
-        __setitem__, and __delitem__.
-
-        The lister is used to implement keys(). It should return a
-        list of keys that are actually in the MP4 instance, provided
-        by its associated getter.
-        """
-        key = key.lower()
-        if getter is not None:
-            cls.Get[key] = getter
-        if setter is not None:
-            cls.Set[key] = setter
-        if deleter is not None:
-            cls.Delete[key] = deleter
-        if lister is not None:
-            cls.List[key] = lister
-
-    @classmethod
-    def RegisterTextKey(cls, key, atomid):
-        """Register a text key.
-
-        If the key you need to register is a simple one-to-one mapping
-        of MP4 atom name to EasyMP4Tags key, then you can use this
-        function::
-
-            EasyMP4Tags.RegisterTextKey("artist", "\xa9ART")
-        """
-        def getter(tags, key):
-            return tags[atomid]
-
-        def setter(tags, key, value):
-            tags[atomid] = value
-
-        def deleter(tags, key):
-            del(tags[atomid])
-
-        cls.RegisterKey(key, getter, setter, deleter)
-
-    @classmethod
-    def RegisterIntKey(cls, key, atomid, min_value=0, max_value=(2 ** 16) - 1):
-        """Register a scalar integer key.
-        """
-
-        def getter(tags, key):
-            return list(map(text_type, tags[atomid]))
-
-        def setter(tags, key, value):
-            clamp = lambda x: int(min(max(min_value, x), max_value))
-            tags[atomid] = [clamp(v) for v in map(int, value)]
-
-        def deleter(tags, key):
-            del(tags[atomid])
-
-        cls.RegisterKey(key, getter, setter, deleter)
-
-    @classmethod
-    def RegisterIntPairKey(cls, key, atomid, min_value=0,
-                           max_value=(2 ** 16) - 1):
-        def getter(tags, key):
-            ret = []
-            for (track, total) in tags[atomid]:
-                if total:
-                    ret.append(u"%d/%d" % (track, total))
-                else:
-                    ret.append(text_type(track))
-            return ret
-
-        def setter(tags, key, value):
-            clamp = lambda x: int(min(max(min_value, x), max_value))
-            data = []
-            for v in value:
-                try:
-                    tracks, total = v.split("/")
-                    tracks = clamp(int(tracks))
-                    total = clamp(int(total))
-                except (ValueError, TypeError):
-                    tracks = clamp(int(v))
-                    total = min_value
-                data.append((tracks, total))
-            tags[atomid] = data
-
-        def deleter(tags, key):
-            del(tags[atomid])
-
-        cls.RegisterKey(key, getter, setter, deleter)
-
-    @classmethod
-    def RegisterFreeformKey(cls, key, name, mean="com.apple.iTunes"):
-        """Register a text key.
-
-        If the key you need to register is a simple one-to-one mapping
-        of MP4 freeform atom (----) and name to EasyMP4Tags key, then
-        you can use this function::
-
-            EasyMP4Tags.RegisterFreeformKey(
-                "musicbrainz_artistid", "MusicBrainz Artist Id")
-        """
-        atomid = "----:" + mean + ":" + name
-
-        def getter(tags, key):
-            return [s.decode("utf-8", "replace") for s in tags[atomid]]
-
-        def setter(tags, key, value):
-            encoded = []
-            for v in value:
-                if not isinstance(v, text_type):
-                    if PY3:
-                        raise TypeError("%r not str" % v)
-                    v = v.decode("utf-8")
-                encoded.append(v.encode("utf-8"))
-            tags[atomid] = encoded
-
-        def deleter(tags, key):
-            del(tags[atomid])
-
-        cls.RegisterKey(key, getter, setter, deleter)
-
-    def __getitem__(self, key):
-        key = key.lower()
-        func = dict_match(self.Get, key)
-        if func is not None:
-            return func(self.__mp4, key)
-        else:
-            raise EasyMP4KeyError("%r is not a valid key" % key)
-
-    def __setitem__(self, key, value):
-        key = key.lower()
-
-        if PY2:
-            if isinstance(value, basestring):
-                value = [value]
-        else:
-            if isinstance(value, text_type):
-                value = [value]
-
-        func = dict_match(self.Set, key)
-        if func is not None:
-            return func(self.__mp4, key, value)
-        else:
-            raise EasyMP4KeyError("%r is not a valid key" % key)
-
-    def __delitem__(self, key):
-        key = key.lower()
-        func = dict_match(self.Delete, key)
-        if func is not None:
-            return func(self.__mp4, key)
-        else:
-            raise EasyMP4KeyError("%r is not a valid key" % key)
-
-    def keys(self):
-        keys = []
-        for key in self.Get.keys():
-            if key in self.List:
-                keys.extend(self.List[key](self.__mp4, key))
-            elif key in self:
-                keys.append(key)
-        return keys
-
-    def pprint(self):
-        """Print tag key=value pairs."""
-        strings = []
-        for key in sorted(self.keys()):
-            values = self[key]
-            for value in values:
-                strings.append("%s=%s" % (key, value))
-        return "\n".join(strings)
-
-for atomid, key in {
-    '\xa9nam': 'title',
-    '\xa9alb': 'album',
-    '\xa9ART': 'artist',
-    'aART': 'albumartist',
-    '\xa9day': 'date',
-    '\xa9cmt': 'comment',
-    'desc': 'description',
-    '\xa9grp': 'grouping',
-    '\xa9gen': 'genre',
-    'cprt': 'copyright',
-    'soal': 'albumsort',
-    'soaa': 'albumartistsort',
-    'soar': 'artistsort',
-    'sonm': 'titlesort',
-    'soco': 'composersort',
-}.items():
-    EasyMP4Tags.RegisterTextKey(key, atomid)
-
-for name, key in {
-    'MusicBrainz Artist Id': 'musicbrainz_artistid',
-    'MusicBrainz Track Id': 'musicbrainz_trackid',
-    'MusicBrainz Album Id': 'musicbrainz_albumid',
-    'MusicBrainz Album Artist Id': 'musicbrainz_albumartistid',
-    'MusicIP PUID': 'musicip_puid',
-    'MusicBrainz Album Status': 'musicbrainz_albumstatus',
-    'MusicBrainz Album Type': 'musicbrainz_albumtype',
-    'MusicBrainz Release Country': 'releasecountry',
-}.items():
-    EasyMP4Tags.RegisterFreeformKey(key, name)
-
-for name, key in {
-    "tmpo": "bpm",
-}.items():
-    EasyMP4Tags.RegisterIntKey(key, name)
-
-for name, key in {
-    "trkn": "tracknumber",
-    "disk": "discnumber",
-}.items():
-    EasyMP4Tags.RegisterIntPairKey(key, name)
-
-
-class EasyMP4(MP4):
-    """Like :class:`MP4 <mutagen.mp4.MP4>`,
-    but uses :class:`EasyMP4Tags` for tags.
-
-    :ivar info: :class:`MP4Info <mutagen.mp4.MP4Info>`
-    :ivar tags: :class:`EasyMP4Tags`
-    """
-
-    MP4Tags = EasyMP4Tags
-
-    Get = EasyMP4Tags.Get
-    Set = EasyMP4Tags.Set
-    Delete = EasyMP4Tags.Delete
-    List = EasyMP4Tags.List
-    RegisterTextKey = EasyMP4Tags.RegisterTextKey
-    RegisterKey = EasyMP4Tags.RegisterKey
diff --git a/resources/lib/libraries/mutagen/flac.py b/resources/lib/libraries/mutagen/flac.py
deleted file mode 100644
index e6cd1cf7..00000000
--- a/resources/lib/libraries/mutagen/flac.py
+++ /dev/null
@@ -1,876 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""Read and write FLAC Vorbis comments and stream information.
-
-Read more about FLAC at http://flac.sourceforge.net.
-
-FLAC supports arbitrary metadata blocks. The two most interesting ones
-are the FLAC stream information block, and the Vorbis comment block;
-these are also the only ones Mutagen can currently read.
-
-This module does not handle Ogg FLAC files.
-
-Based off documentation available at
-http://flac.sourceforge.net/format.html
-"""
-
-__all__ = ["FLAC", "Open", "delete"]
-
-import struct
-from ._vorbis import VCommentDict
-import mutagen
-
-from ._compat import cBytesIO, endswith, chr_, xrange
-from mutagen._util import resize_bytes, MutagenError, get_size
-from mutagen._tags import PaddingInfo
-from mutagen.id3 import BitPaddedInt
-from functools import reduce
-
-
-class error(IOError, MutagenError):
-    pass
-
-
-class FLACNoHeaderError(error):
-    pass
-
-
-class FLACVorbisError(ValueError, error):
-    pass
-
-
-def to_int_be(data):
-    """Convert an arbitrarily-long string to a long using big-endian
-    byte order."""
-    return reduce(lambda a, b: (a << 8) + b, bytearray(data), 0)
-
-
-class StrictFileObject(object):
-    """Wraps a file-like object and raises an exception if the requested
-    amount of data to read isn't returned."""
-
-    def __init__(self, fileobj):
-        self._fileobj = fileobj
-        for m in ["close", "tell", "seek", "write", "name"]:
-            if hasattr(fileobj, m):
-                setattr(self, m, getattr(fileobj, m))
-
-    def read(self, size=-1):
-        data = self._fileobj.read(size)
-        if size >= 0 and len(data) != size:
-            raise error("file said %d bytes, read %d bytes" % (
-                        size, len(data)))
-        return data
-
-    def tryread(self, *args):
-        return self._fileobj.read(*args)
-
-
-class MetadataBlock(object):
-    """A generic block of FLAC metadata.
-
-    This class is extended by specific used as an ancestor for more specific
-    blocks, and also as a container for data blobs of unknown blocks.
-
-    Attributes:
-
-    * data -- raw binary data for this block
-    """
-
-    _distrust_size = False
-    """For block types setting this, we don't trust the size field and
-    use the size of the content instead."""
-
-    _invalid_overflow_size = -1
-    """In case the real size was bigger than what is representable by the
-    24 bit size field, we save the wrong specified size here. This can
-    only be set if _distrust_size is True"""
-
-    _MAX_SIZE = 2 ** 24 - 1
-
-    def __init__(self, data):
-        """Parse the given data string or file-like as a metadata block.
-        The metadata header should not be included."""
-        if data is not None:
-            if not isinstance(data, StrictFileObject):
-                if isinstance(data, bytes):
-                    data = cBytesIO(data)
-                elif not hasattr(data, 'read'):
-                    raise TypeError(
-                        "StreamInfo requires string data or a file-like")
-                data = StrictFileObject(data)
-            self.load(data)
-
-    def load(self, data):
-        self.data = data.read()
-
-    def write(self):
-        return self.data
-
-    @classmethod
-    def _writeblock(cls, block, is_last=False):
-        """Returns the block content + header.
-
-        Raises error.
-        """
-
-        data = bytearray()
-        code = (block.code | 128) if is_last else block.code
-        datum = block.write()
-        size = len(datum)
-        if size > cls._MAX_SIZE:
-            if block._distrust_size and block._invalid_overflow_size != -1:
-                # The original size of this block was (1) wrong and (2)
-                # the real size doesn't allow us to save the file
-                # according to the spec (too big for 24 bit uint). Instead
-                # simply write back the original wrong size.. at least
-                # we don't make the file more "broken" as it is.
-                size = block._invalid_overflow_size
-            else:
-                raise error("block is too long to write")
-        assert not size > cls._MAX_SIZE
-        length = struct.pack(">I", size)[-3:]
-        data.append(code)
-        data += length
-        data += datum
-        return data
-
-    @classmethod
-    def _writeblocks(cls, blocks, available, cont_size, padding_func):
-        """Render metadata block as a byte string."""
-
-        # write everything except padding
-        data = bytearray()
-        for block in blocks:
-            if isinstance(block, Padding):
-                continue
-            data += cls._writeblock(block)
-        blockssize = len(data)
-
-        # take the padding overhead into account. we always add one
-        # to make things simple.
-        padding_block = Padding()
-        blockssize += len(cls._writeblock(padding_block))
-
-        # finally add a padding block
-        info = PaddingInfo(available - blockssize, cont_size)
-        padding_block.length = min(info._get_padding(padding_func),
-                                   cls._MAX_SIZE)
-        data += cls._writeblock(padding_block, is_last=True)
-
-        return data
-
-
-class StreamInfo(MetadataBlock, mutagen.StreamInfo):
-    """FLAC stream information.
-
-    This contains information about the audio data in the FLAC file.
-    Unlike most stream information objects in Mutagen, changes to this
-    one will rewritten to the file when it is saved. Unless you are
-    actually changing the audio stream itself, don't change any
-    attributes of this block.
-
-    Attributes:
-
-    * min_blocksize -- minimum audio block size
-    * max_blocksize -- maximum audio block size
-    * sample_rate -- audio sample rate in Hz
-    * channels -- audio channels (1 for mono, 2 for stereo)
-    * bits_per_sample -- bits per sample
-    * total_samples -- total samples in file
-    * length -- audio length in seconds
-    """
-
-    code = 0
-
-    def __eq__(self, other):
-        try:
-            return (self.min_blocksize == other.min_blocksize and
-                    self.max_blocksize == other.max_blocksize and
-                    self.sample_rate == other.sample_rate and
-                    self.channels == other.channels and
-                    self.bits_per_sample == other.bits_per_sample and
-                    self.total_samples == other.total_samples)
-        except:
-            return False
-
-    __hash__ = MetadataBlock.__hash__
-
-    def load(self, data):
-        self.min_blocksize = int(to_int_be(data.read(2)))
-        self.max_blocksize = int(to_int_be(data.read(2)))
-        self.min_framesize = int(to_int_be(data.read(3)))
-        self.max_framesize = int(to_int_be(data.read(3)))
-        # first 16 bits of sample rate
-        sample_first = to_int_be(data.read(2))
-        # last 4 bits of sample rate, 3 of channels, first 1 of bits/sample
-        sample_channels_bps = to_int_be(data.read(1))
-        # last 4 of bits/sample, 36 of total samples
-        bps_total = to_int_be(data.read(5))
-
-        sample_tail = sample_channels_bps >> 4
-        self.sample_rate = int((sample_first << 4) + sample_tail)
-        if not self.sample_rate:
-            raise error("A sample rate value of 0 is invalid")
-        self.channels = int(((sample_channels_bps >> 1) & 7) + 1)
-        bps_tail = bps_total >> 36
-        bps_head = (sample_channels_bps & 1) << 4
-        self.bits_per_sample = int(bps_head + bps_tail + 1)
-        self.total_samples = bps_total & 0xFFFFFFFFF
-        self.length = self.total_samples / float(self.sample_rate)
-
-        self.md5_signature = to_int_be(data.read(16))
-
-    def write(self):
-        f = cBytesIO()
-        f.write(struct.pack(">I", self.min_blocksize)[-2:])
-        f.write(struct.pack(">I", self.max_blocksize)[-2:])
-        f.write(struct.pack(">I", self.min_framesize)[-3:])
-        f.write(struct.pack(">I", self.max_framesize)[-3:])
-
-        # first 16 bits of sample rate
-        f.write(struct.pack(">I", self.sample_rate >> 4)[-2:])
-        # 4 bits sample, 3 channel, 1 bps
-        byte = (self.sample_rate & 0xF) << 4
-        byte += ((self.channels - 1) & 7) << 1
-        byte += ((self.bits_per_sample - 1) >> 4) & 1
-        f.write(chr_(byte))
-        # 4 bits of bps, 4 of sample count
-        byte = ((self.bits_per_sample - 1) & 0xF) << 4
-        byte += (self.total_samples >> 32) & 0xF
-        f.write(chr_(byte))
-        # last 32 of sample count
-        f.write(struct.pack(">I", self.total_samples & 0xFFFFFFFF))
-        # MD5 signature
-        sig = self.md5_signature
-        f.write(struct.pack(
-            ">4I", (sig >> 96) & 0xFFFFFFFF, (sig >> 64) & 0xFFFFFFFF,
-            (sig >> 32) & 0xFFFFFFFF, sig & 0xFFFFFFFF))
-        return f.getvalue()
-
-    def pprint(self):
-        return u"FLAC, %.2f seconds, %d Hz" % (self.length, self.sample_rate)
-
-
-class SeekPoint(tuple):
-    """A single seek point in a FLAC file.
-
-    Placeholder seek points have first_sample of 0xFFFFFFFFFFFFFFFFL,
-    and byte_offset and num_samples undefined. Seek points must be
-    sorted in ascending order by first_sample number. Seek points must
-    be unique by first_sample number, except for placeholder
-    points. Placeholder points must occur last in the table and there
-    may be any number of them.
-
-    Attributes:
-
-    * first_sample -- sample number of first sample in the target frame
-    * byte_offset -- offset from first frame to target frame
-    * num_samples -- number of samples in target frame
-    """
-
-    def __new__(cls, first_sample, byte_offset, num_samples):
-        return super(cls, SeekPoint).__new__(
-            cls, (first_sample, byte_offset, num_samples))
-
-    first_sample = property(lambda self: self[0])
-    byte_offset = property(lambda self: self[1])
-    num_samples = property(lambda self: self[2])
-
-
-class SeekTable(MetadataBlock):
-    """Read and write FLAC seek tables.
-
-    Attributes:
-
-    * seekpoints -- list of SeekPoint objects
-    """
-
-    __SEEKPOINT_FORMAT = '>QQH'
-    __SEEKPOINT_SIZE = struct.calcsize(__SEEKPOINT_FORMAT)
-
-    code = 3
-
-    def __init__(self, data):
-        self.seekpoints = []
-        super(SeekTable, self).__init__(data)
-
-    def __eq__(self, other):
-        try:
-            return (self.seekpoints == other.seekpoints)
-        except (AttributeError, TypeError):
-            return False
-
-    __hash__ = MetadataBlock.__hash__
-
-    def load(self, data):
-        self.seekpoints = []
-        sp = data.tryread(self.__SEEKPOINT_SIZE)
-        while len(sp) == self.__SEEKPOINT_SIZE:
-            self.seekpoints.append(SeekPoint(
-                *struct.unpack(self.__SEEKPOINT_FORMAT, sp)))
-            sp = data.tryread(self.__SEEKPOINT_SIZE)
-
-    def write(self):
-        f = cBytesIO()
-        for seekpoint in self.seekpoints:
-            packed = struct.pack(
-                self.__SEEKPOINT_FORMAT,
-                seekpoint.first_sample, seekpoint.byte_offset,
-                seekpoint.num_samples)
-            f.write(packed)
-        return f.getvalue()
-
-    def __repr__(self):
-        return "<%s seekpoints=%r>" % (type(self).__name__, self.seekpoints)
-
-
-class VCFLACDict(VCommentDict):
-    """Read and write FLAC Vorbis comments.
-
-    FLACs don't use the framing bit at the end of the comment block.
-    So this extends VCommentDict to not use the framing bit.
-    """
-
-    code = 4
-    _distrust_size = True
-
-    def load(self, data, errors='replace', framing=False):
-        super(VCFLACDict, self).load(data, errors=errors, framing=framing)
-
-    def write(self, framing=False):
-        return super(VCFLACDict, self).write(framing=framing)
-
-
-class CueSheetTrackIndex(tuple):
-    """Index for a track in a cuesheet.
-
-    For CD-DA, an index_number of 0 corresponds to the track
-    pre-gap. The first index in a track must have a number of 0 or 1,
-    and subsequently, index_numbers must increase by 1. Index_numbers
-    must be unique within a track. And index_offset must be evenly
-    divisible by 588 samples.
-
-    Attributes:
-
-    * index_number -- index point number
-    * index_offset -- offset in samples from track start
-    """
-
-    def __new__(cls, index_number, index_offset):
-        return super(cls, CueSheetTrackIndex).__new__(
-            cls, (index_number, index_offset))
-
-    index_number = property(lambda self: self[0])
-    index_offset = property(lambda self: self[1])
-
-
-class CueSheetTrack(object):
-    """A track in a cuesheet.
-
-    For CD-DA, track_numbers must be 1-99, or 170 for the
-    lead-out. Track_numbers must be unique within a cue sheet. There
-    must be atleast one index in every track except the lead-out track
-    which must have none.
-
-    Attributes:
-
-    * track_number -- track number
-    * start_offset -- track offset in samples from start of FLAC stream
-    * isrc -- ISRC code
-    * type -- 0 for audio, 1 for digital data
-    * pre_emphasis -- true if the track is recorded with pre-emphasis
-    * indexes -- list of CueSheetTrackIndex objects
-    """
-
-    def __init__(self, track_number, start_offset, isrc='', type_=0,
-                 pre_emphasis=False):
-        self.track_number = track_number
-        self.start_offset = start_offset
-        self.isrc = isrc
-        self.type = type_
-        self.pre_emphasis = pre_emphasis
-        self.indexes = []
-
-    def __eq__(self, other):
-        try:
-            return (self.track_number == other.track_number and
-                    self.start_offset == other.start_offset and
-                    self.isrc == other.isrc and
-                    self.type == other.type and
-                    self.pre_emphasis == other.pre_emphasis and
-                    self.indexes == other.indexes)
-        except (AttributeError, TypeError):
-            return False
-
-    __hash__ = object.__hash__
-
-    def __repr__(self):
-        return (("<%s number=%r, offset=%d, isrc=%r, type=%r, "
-                "pre_emphasis=%r, indexes=%r)>") %
-                (type(self).__name__, self.track_number, self.start_offset,
-                 self.isrc, self.type, self.pre_emphasis, self.indexes))
-
-
-class CueSheet(MetadataBlock):
-    """Read and write FLAC embedded cue sheets.
-
-    Number of tracks should be from 1 to 100. There should always be
-    exactly one lead-out track and that track must be the last track
-    in the cue sheet.
-
-    Attributes:
-
-    * media_catalog_number -- media catalog number in ASCII
-    * lead_in_samples -- number of lead-in samples
-    * compact_disc -- true if the cuesheet corresponds to a compact disc
-    * tracks -- list of CueSheetTrack objects
-    * lead_out -- lead-out as CueSheetTrack or None if lead-out was not found
-    """
-
-    __CUESHEET_FORMAT = '>128sQB258xB'
-    __CUESHEET_SIZE = struct.calcsize(__CUESHEET_FORMAT)
-    __CUESHEET_TRACK_FORMAT = '>QB12sB13xB'
-    __CUESHEET_TRACK_SIZE = struct.calcsize(__CUESHEET_TRACK_FORMAT)
-    __CUESHEET_TRACKINDEX_FORMAT = '>QB3x'
-    __CUESHEET_TRACKINDEX_SIZE = struct.calcsize(__CUESHEET_TRACKINDEX_FORMAT)
-
-    code = 5
-
-    media_catalog_number = b''
-    lead_in_samples = 88200
-    compact_disc = True
-
-    def __init__(self, data):
-        self.tracks = []
-        super(CueSheet, self).__init__(data)
-
-    def __eq__(self, other):
-        try:
-            return (self.media_catalog_number == other.media_catalog_number and
-                    self.lead_in_samples == other.lead_in_samples and
-                    self.compact_disc == other.compact_disc and
-                    self.tracks == other.tracks)
-        except (AttributeError, TypeError):
-            return False
-
-    __hash__ = MetadataBlock.__hash__
-
-    def load(self, data):
-        header = data.read(self.__CUESHEET_SIZE)
-        media_catalog_number, lead_in_samples, flags, num_tracks = \
-            struct.unpack(self.__CUESHEET_FORMAT, header)
-        self.media_catalog_number = media_catalog_number.rstrip(b'\0')
-        self.lead_in_samples = lead_in_samples
-        self.compact_disc = bool(flags & 0x80)
-        self.tracks = []
-        for i in xrange(num_tracks):
-            track = data.read(self.__CUESHEET_TRACK_SIZE)
-            start_offset, track_number, isrc_padded, flags, num_indexes = \
-                struct.unpack(self.__CUESHEET_TRACK_FORMAT, track)
-            isrc = isrc_padded.rstrip(b'\0')
-            type_ = (flags & 0x80) >> 7
-            pre_emphasis = bool(flags & 0x40)
-            val = CueSheetTrack(
-                track_number, start_offset, isrc, type_, pre_emphasis)
-            for j in xrange(num_indexes):
-                index = data.read(self.__CUESHEET_TRACKINDEX_SIZE)
-                index_offset, index_number = struct.unpack(
-                    self.__CUESHEET_TRACKINDEX_FORMAT, index)
-                val.indexes.append(
-                    CueSheetTrackIndex(index_number, index_offset))
-            self.tracks.append(val)
-
-    def write(self):
-        f = cBytesIO()
-        flags = 0
-        if self.compact_disc:
-            flags |= 0x80
-        packed = struct.pack(
-            self.__CUESHEET_FORMAT, self.media_catalog_number,
-            self.lead_in_samples, flags, len(self.tracks))
-        f.write(packed)
-        for track in self.tracks:
-            track_flags = 0
-            track_flags |= (track.type & 1) << 7
-            if track.pre_emphasis:
-                track_flags |= 0x40
-            track_packed = struct.pack(
-                self.__CUESHEET_TRACK_FORMAT, track.start_offset,
-                track.track_number, track.isrc, track_flags,
-                len(track.indexes))
-            f.write(track_packed)
-            for index in track.indexes:
-                index_packed = struct.pack(
-                    self.__CUESHEET_TRACKINDEX_FORMAT,
-                    index.index_offset, index.index_number)
-                f.write(index_packed)
-        return f.getvalue()
-
-    def __repr__(self):
-        return (("<%s media_catalog_number=%r, lead_in=%r, compact_disc=%r, "
-                 "tracks=%r>") %
-                (type(self).__name__, self.media_catalog_number,
-                 self.lead_in_samples, self.compact_disc, self.tracks))
-
-
-class Picture(MetadataBlock):
-    """Read and write FLAC embed pictures.
-
-    Attributes:
-
-    * type -- picture type (same as types for ID3 APIC frames)
-    * mime -- MIME type of the picture
-    * desc -- picture's description
-    * width -- width in pixels
-    * height -- height in pixels
-    * depth -- color depth in bits-per-pixel
-    * colors -- number of colors for indexed palettes (like GIF),
-      0 for non-indexed
-    * data -- picture data
-
-    To create a picture from file (in order to add to a FLAC file),
-    instantiate this object without passing anything to the constructor and
-    then set the properties manually::
-
-        p = Picture()
-
-        with open("Folder.jpg", "rb") as f:
-            pic.data = f.read()
-
-        pic.type = id3.PictureType.COVER_FRONT
-        pic.mime = u"image/jpeg"
-        pic.width = 500
-        pic.height = 500
-        pic.depth = 16 # color depth
-    """
-
-    code = 6
-    _distrust_size = True
-
-    def __init__(self, data=None):
-        self.type = 0
-        self.mime = u''
-        self.desc = u''
-        self.width = 0
-        self.height = 0
-        self.depth = 0
-        self.colors = 0
-        self.data = b''
-        super(Picture, self).__init__(data)
-
-    def __eq__(self, other):
-        try:
-            return (self.type == other.type and
-                    self.mime == other.mime and
-                    self.desc == other.desc and
-                    self.width == other.width and
-                    self.height == other.height and
-                    self.depth == other.depth and
-                    self.colors == other.colors and
-                    self.data == other.data)
-        except (AttributeError, TypeError):
-            return False
-
-    __hash__ = MetadataBlock.__hash__
-
-    def load(self, data):
-        self.type, length = struct.unpack('>2I', data.read(8))
-        self.mime = data.read(length).decode('UTF-8', 'replace')
-        length, = struct.unpack('>I', data.read(4))
-        self.desc = data.read(length).decode('UTF-8', 'replace')
-        (self.width, self.height, self.depth,
-         self.colors, length) = struct.unpack('>5I', data.read(20))
-        self.data = data.read(length)
-
-    def write(self):
-        f = cBytesIO()
-        mime = self.mime.encode('UTF-8')
-        f.write(struct.pack('>2I', self.type, len(mime)))
-        f.write(mime)
-        desc = self.desc.encode('UTF-8')
-        f.write(struct.pack('>I', len(desc)))
-        f.write(desc)
-        f.write(struct.pack('>5I', self.width, self.height, self.depth,
-                            self.colors, len(self.data)))
-        f.write(self.data)
-        return f.getvalue()
-
-    def __repr__(self):
-        return "<%s '%s' (%d bytes)>" % (type(self).__name__, self.mime,
-                                         len(self.data))
-
-
-class Padding(MetadataBlock):
-    """Empty padding space for metadata blocks.
-
-    To avoid rewriting the entire FLAC file when editing comments,
-    metadata is often padded. Padding should occur at the end, and no
-    more than one padding block should be in any FLAC file.
-    """
-
-    code = 1
-
-    def __init__(self, data=b""):
-        super(Padding, self).__init__(data)
-
-    def load(self, data):
-        self.length = len(data.read())
-
-    def write(self):
-        try:
-            return b"\x00" * self.length
-        # On some 64 bit platforms this won't generate a MemoryError
-        # or OverflowError since you might have enough RAM, but it
-        # still generates a ValueError. On other 64 bit platforms,
-        # this will still succeed for extremely large values.
-        # Those should never happen in the real world, and if they
-        # do, writeblocks will catch it.
-        except (OverflowError, ValueError, MemoryError):
-            raise error("cannot write %d bytes" % self.length)
-
-    def __eq__(self, other):
-        return isinstance(other, Padding) and self.length == other.length
-
-    __hash__ = MetadataBlock.__hash__
-
-    def __repr__(self):
-        return "<%s (%d bytes)>" % (type(self).__name__, self.length)
-
-
-class FLAC(mutagen.FileType):
-    """A FLAC audio file.
-
-    Attributes:
-
-    * cuesheet -- CueSheet object, if any
-    * seektable -- SeekTable object, if any
-    * pictures -- list of embedded pictures
-    """
-
-    _mimes = ["audio/x-flac", "application/x-flac"]
-
-    info = None
-    """A `StreamInfo`"""
-
-    tags = None
-    """A `VCommentDict`"""
-
-    METADATA_BLOCKS = [StreamInfo, Padding, None, SeekTable, VCFLACDict,
-                       CueSheet, Picture]
-    """Known metadata block types, indexed by ID."""
-
-    @staticmethod
-    def score(filename, fileobj, header_data):
-        return (header_data.startswith(b"fLaC") +
-                endswith(filename.lower(), ".flac") * 3)
-
-    def __read_metadata_block(self, fileobj):
-        byte = ord(fileobj.read(1))
-        size = to_int_be(fileobj.read(3))
-        code = byte & 0x7F
-        last_block = bool(byte & 0x80)
-
-        try:
-            block_type = self.METADATA_BLOCKS[code] or MetadataBlock
-        except IndexError:
-            block_type = MetadataBlock
-
-        if block_type._distrust_size:
-            # Some jackass is writing broken Metadata block length
-            # for Vorbis comment blocks, and the FLAC reference
-            # implementaton can parse them (mostly by accident),
-            # so we have to too.  Instead of parsing the size
-            # given, parse an actual Vorbis comment, leaving
-            # fileobj in the right position.
-            # http://code.google.com/p/mutagen/issues/detail?id=52
-            # ..same for the Picture block:
-            # http://code.google.com/p/mutagen/issues/detail?id=106
-            start = fileobj.tell()
-            block = block_type(fileobj)
-            real_size = fileobj.tell() - start
-            if real_size > MetadataBlock._MAX_SIZE:
-                block._invalid_overflow_size = size
-        else:
-            data = fileobj.read(size)
-            block = block_type(data)
-        block.code = code
-
-        if block.code == VCFLACDict.code:
-            if self.tags is None:
-                self.tags = block
-            else:
-                raise FLACVorbisError("> 1 Vorbis comment block found")
-        elif block.code == CueSheet.code:
-            if self.cuesheet is None:
-                self.cuesheet = block
-            else:
-                raise error("> 1 CueSheet block found")
-        elif block.code == SeekTable.code:
-            if self.seektable is None:
-                self.seektable = block
-            else:
-                raise error("> 1 SeekTable block found")
-        self.metadata_blocks.append(block)
-        return not last_block
-
-    def add_tags(self):
-        """Add a Vorbis comment block to the file."""
-        if self.tags is None:
-            self.tags = VCFLACDict()
-            self.metadata_blocks.append(self.tags)
-        else:
-            raise FLACVorbisError("a Vorbis comment already exists")
-
-    add_vorbiscomment = add_tags
-
-    def delete(self, filename=None):
-        """Remove Vorbis comments from a file.
-
-        If no filename is given, the one most recently loaded is used.
-        """
-        if filename is None:
-            filename = self.filename
-
-        if self.tags is not None:
-            self.metadata_blocks.remove(self.tags)
-            self.save(padding=lambda x: 0)
-            self.metadata_blocks.append(self.tags)
-            self.tags.clear()
-
-    vc = property(lambda s: s.tags, doc="Alias for tags; don't use this.")
-
-    def load(self, filename):
-        """Load file information from a filename."""
-
-        self.metadata_blocks = []
-        self.tags = None
-        self.cuesheet = None
-        self.seektable = None
-        self.filename = filename
-        fileobj = StrictFileObject(open(filename, "rb"))
-        try:
-            self.__check_header(fileobj)
-            while self.__read_metadata_block(fileobj):
-                pass
-        finally:
-            fileobj.close()
-
-        try:
-            self.metadata_blocks[0].length
-        except (AttributeError, IndexError):
-            raise FLACNoHeaderError("Stream info block not found")
-
-    @property
-    def info(self):
-        return self.metadata_blocks[0]
-
-    def add_picture(self, picture):
-        """Add a new picture to the file."""
-        self.metadata_blocks.append(picture)
-
-    def clear_pictures(self):
-        """Delete all pictures from the file."""
-
-        blocks = [b for b in self.metadata_blocks if b.code != Picture.code]
-        self.metadata_blocks = blocks
-
-    @property
-    def pictures(self):
-        """List of embedded pictures"""
-
-        return [b for b in self.metadata_blocks if b.code == Picture.code]
-
-    def save(self, filename=None, deleteid3=False, padding=None):
-        """Save metadata blocks to a file.
-
-        If no filename is given, the one most recently loaded is used.
-        """
-
-        if filename is None:
-            filename = self.filename
-
-        with open(filename, 'rb+') as f:
-            header = self.__check_header(f)
-            audio_offset = self.__find_audio_offset(f)
-            # "fLaC" and maybe ID3
-            available = audio_offset - header
-
-            # Delete ID3v2
-            if deleteid3 and header > 4:
-                available += header - 4
-                header = 4
-
-            content_size = get_size(f) - audio_offset
-            assert content_size >= 0
-            data = MetadataBlock._writeblocks(
-                self.metadata_blocks, available, content_size, padding)
-            data_size = len(data)
-
-            resize_bytes(f, available, data_size, header)
-            f.seek(header - 4)
-            f.write(b"fLaC")
-            f.write(data)
-
-            # Delete ID3v1
-            if deleteid3:
-                try:
-                    f.seek(-128, 2)
-                except IOError:
-                    pass
-                else:
-                    if f.read(3) == b"TAG":
-                        f.seek(-128, 2)
-                        f.truncate()
-
-    def __find_audio_offset(self, fileobj):
-        byte = 0x00
-        while not (byte & 0x80):
-            byte = ord(fileobj.read(1))
-            size = to_int_be(fileobj.read(3))
-            try:
-                block_type = self.METADATA_BLOCKS[byte & 0x7F]
-            except IndexError:
-                block_type = None
-
-            if block_type and block_type._distrust_size:
-                # See comments in read_metadata_block; the size can't
-                # be trusted for Vorbis comment blocks and Picture block
-                block_type(fileobj)
-            else:
-                fileobj.read(size)
-        return fileobj.tell()
-
-    def __check_header(self, fileobj):
-        """Returns the offset of the flac block start
-        (skipping id3 tags if found). The passed fileobj will be advanced to
-        that offset as well.
-        """
-
-        size = 4
-        header = fileobj.read(4)
-        if header != b"fLaC":
-            size = None
-            if header[:3] == b"ID3":
-                size = 14 + BitPaddedInt(fileobj.read(6)[2:])
-                fileobj.seek(size - 4)
-                if fileobj.read(4) != b"fLaC":
-                    size = None
-        if size is None:
-            raise FLACNoHeaderError(
-                "%r is not a valid FLAC file" % fileobj.name)
-        return size
-
-
-Open = FLAC
-
-
-def delete(filename):
-    """Remove tags from a file."""
-    FLAC(filename).delete()
diff --git a/resources/lib/libraries/mutagen/id3/__init__.py b/resources/lib/libraries/mutagen/id3/__init__.py
deleted file mode 100644
index 9aef865b..00000000
--- a/resources/lib/libraries/mutagen/id3/__init__.py
+++ /dev/null
@@ -1,1093 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Michael Urman
-#               2006  Lukas Lalinsky
-#               2013  Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""ID3v2 reading and writing.
-
-This is based off of the following references:
-
-* http://id3.org/id3v2.4.0-structure
-* http://id3.org/id3v2.4.0-frames
-* http://id3.org/id3v2.3.0
-* http://id3.org/id3v2-00
-* http://id3.org/ID3v1
-
-Its largest deviation from the above (versions 2.3 and 2.2) is that it
-will not interpret the / characters as a separator, and will almost
-always accept null separators to generate multi-valued text frames.
-
-Because ID3 frame structure differs between frame types, each frame is
-implemented as a different class (e.g. TIT2 as mutagen.id3.TIT2). Each
-frame's documentation contains a list of its attributes.
-
-Since this file's documentation is a little unwieldy, you are probably
-interested in the :class:`ID3` class to start with.
-"""
-
-__all__ = ['ID3', 'ID3FileType', 'Frames', 'Open', 'delete']
-
-import struct
-import errno
-
-from struct import unpack, pack, error as StructError
-
-import mutagen
-from mutagen._util import insert_bytes, delete_bytes, DictProxy, enum
-from mutagen._tags import PaddingInfo
-from .._compat import chr_, PY3
-
-from ._util import *
-from ._frames import *
-from ._specs import *
-
-
-@enum
-class ID3v1SaveOptions(object):
-
-    REMOVE = 0
-    """ID3v1 tags will be removed"""
-
-    UPDATE = 1
-    """ID3v1 tags will be updated but not added"""
-
-    CREATE = 2
-    """ID3v1 tags will be created and/or updated"""
-
-
-def _fullread(fileobj, size):
-    """Read a certain number of bytes from the source file.
-
-    Raises ValueError on invalid size input or EOFError/IOError.
-    """
-
-    if size < 0:
-        raise ValueError('Requested bytes (%s) less than zero' % size)
-    data = fileobj.read(size)
-    if len(data) != size:
-        raise EOFError("Not enough data to read")
-    return data
-
-
-class ID3Header(object):
-
-    _V24 = (2, 4, 0)
-    _V23 = (2, 3, 0)
-    _V22 = (2, 2, 0)
-    _V11 = (1, 1)
-
-    f_unsynch = property(lambda s: bool(s._flags & 0x80))
-    f_extended = property(lambda s: bool(s._flags & 0x40))
-    f_experimental = property(lambda s: bool(s._flags & 0x20))
-    f_footer = property(lambda s: bool(s._flags & 0x10))
-
-    def __init__(self, fileobj=None):
-        """Raises ID3NoHeaderError, ID3UnsupportedVersionError or error"""
-
-        if fileobj is None:
-            # for testing
-            self._flags = 0
-            return
-
-        fn = getattr(fileobj, "name", "<unknown>")
-        try:
-            data = _fullread(fileobj, 10)
-        except EOFError:
-            raise ID3NoHeaderError("%s: too small" % fn)
-
-        id3, vmaj, vrev, flags, size = unpack('>3sBBB4s', data)
-        self._flags = flags
-        self.size = BitPaddedInt(size) + 10
-        self.version = (2, vmaj, vrev)
-
-        if id3 != b'ID3':
-            raise ID3NoHeaderError("%r doesn't start with an ID3 tag" % fn)
-
-        if vmaj not in [2, 3, 4]:
-            raise ID3UnsupportedVersionError("%r ID3v2.%d not supported"
-                                             % (fn, vmaj))
-
-        if not BitPaddedInt.has_valid_padding(size):
-            raise error("Header size not synchsafe")
-
-        if (self.version >= self._V24) and (flags & 0x0f):
-            raise error(
-                "%r has invalid flags %#02x" % (fn, flags))
-        elif (self._V23 <= self.version < self._V24) and (flags & 0x1f):
-            raise error(
-                "%r has invalid flags %#02x" % (fn, flags))
-
-        if self.f_extended:
-            try:
-                extsize_data = _fullread(fileobj, 4)
-            except EOFError:
-                raise error("%s: too small" % fn)
-
-            if PY3:
-                frame_id = extsize_data.decode("ascii", "replace")
-            else:
-                frame_id = extsize_data
-
-            if frame_id in Frames:
-                # Some tagger sets the extended header flag but
-                # doesn't write an extended header; in this case, the
-                # ID3 data follows immediately. Since no extended
-                # header is going to be long enough to actually match
-                # a frame, and if it's *not* a frame we're going to be
-                # completely lost anyway, this seems to be the most
-                # correct check.
-                # http://code.google.com/p/quodlibet/issues/detail?id=126
-                self._flags ^= 0x40
-                extsize = 0
-                fileobj.seek(-4, 1)
-            elif self.version >= self._V24:
-                # "Where the 'Extended header size' is the size of the whole
-                # extended header, stored as a 32 bit synchsafe integer."
-                extsize = BitPaddedInt(extsize_data) - 4
-                if not BitPaddedInt.has_valid_padding(extsize_data):
-                    raise error(
-                        "Extended header size not synchsafe")
-            else:
-                # "Where the 'Extended header size', currently 6 or 10 bytes,
-                # excludes itself."
-                extsize = unpack('>L', extsize_data)[0]
-
-            try:
-                self._extdata = _fullread(fileobj, extsize)
-            except EOFError:
-                raise error("%s: too small" % fn)
-
-
-class ID3(DictProxy, mutagen.Metadata):
-    """A file with an ID3v2 tag.
-
-    Attributes:
-
-    * version -- ID3 tag version as a tuple
-    * unknown_frames -- raw frame data of any unknown frames found
-    * size -- the total size of the ID3 tag, including the header
-    """
-
-    __module__ = "mutagen.id3"
-
-    PEDANTIC = True
-    """Deprecated. Doesn't have any effect"""
-
-    filename = None
-
-    def __init__(self, *args, **kwargs):
-        self.unknown_frames = []
-        self.__unknown_version = None
-        self._header = None
-        self._version = (2, 4, 0)
-        super(ID3, self).__init__(*args, **kwargs)
-
-    @property
-    def version(self):
-        """ID3 tag version as a tuple (of the loaded file)"""
-
-        if self._header is not None:
-            return self._header.version
-        return self._version
-
-    @version.setter
-    def version(self, value):
-        self._version = value
-
-    @property
-    def f_unsynch(self):
-        if self._header is not None:
-            return self._header.f_unsynch
-        return False
-
-    @property
-    def f_extended(self):
-        if self._header is not None:
-            return self._header.f_extended
-        return False
-
-    @property
-    def size(self):
-        if self._header is not None:
-            return self._header.size
-        return 0
-
-    def _pre_load_header(self, fileobj):
-        # XXX: for aiff to adjust the offset..
-        pass
-
-    def load(self, filename, known_frames=None, translate=True, v2_version=4):
-        """Load tags from a filename.
-
-        Keyword arguments:
-
-        * filename -- filename to load tag data from
-        * known_frames -- dict mapping frame IDs to Frame objects
-        * translate -- Update all tags to ID3v2.3/4 internally. If you
-                       intend to save, this must be true or you have to
-                       call update_to_v23() / update_to_v24() manually.
-        * v2_version -- if update_to_v23 or update_to_v24 get called (3 or 4)
-
-        Example of loading a custom frame::
-
-            my_frames = dict(mutagen.id3.Frames)
-            class XMYF(Frame): ...
-            my_frames["XMYF"] = XMYF
-            mutagen.id3.ID3(filename, known_frames=my_frames)
-        """
-
-        if v2_version not in (3, 4):
-            raise ValueError("Only 3 and 4 possible for v2_version")
-
-        self.filename = filename
-        self.unknown_frames = []
-        self.__known_frames = known_frames
-        self._header = None
-        self._padding = 0  # for testing
-
-        with open(filename, 'rb') as fileobj:
-            self._pre_load_header(fileobj)
-
-            try:
-                self._header = ID3Header(fileobj)
-            except (ID3NoHeaderError, ID3UnsupportedVersionError):
-                frames, offset = _find_id3v1(fileobj)
-                if frames is None:
-                    raise
-
-                self.version = ID3Header._V11
-                for v in frames.values():
-                    self.add(v)
-            else:
-                frames = self.__known_frames
-                if frames is None:
-                    if self.version >= ID3Header._V23:
-                        frames = Frames
-                    elif self.version >= ID3Header._V22:
-                        frames = Frames_2_2
-
-                try:
-                    data = _fullread(fileobj, self.size - 10)
-                except (ValueError, EOFError, IOError) as e:
-                    raise error(e)
-
-                for frame in self.__read_frames(data, frames=frames):
-                    if isinstance(frame, Frame):
-                        self.add(frame)
-                    else:
-                        self.unknown_frames.append(frame)
-                self.__unknown_version = self.version[:2]
-
-        if translate:
-            if v2_version == 3:
-                self.update_to_v23()
-            else:
-                self.update_to_v24()
-
-    def getall(self, key):
-        """Return all frames with a given name (the list may be empty).
-
-        This is best explained by examples::
-
-            id3.getall('TIT2') == [id3['TIT2']]
-            id3.getall('TTTT') == []
-            id3.getall('TXXX') == [TXXX(desc='woo', text='bar'),
-                                   TXXX(desc='baz', text='quuuux'), ...]
-
-        Since this is based on the frame's HashKey, which is
-        colon-separated, you can use it to do things like
-        ``getall('COMM:MusicMatch')`` or ``getall('TXXX:QuodLibet:')``.
-        """
-        if key in self:
-            return [self[key]]
-        else:
-            key = key + ":"
-            return [v for s, v in self.items() if s.startswith(key)]
-
-    def delall(self, key):
-        """Delete all tags of a given kind; see getall."""
-        if key in self:
-            del(self[key])
-        else:
-            key = key + ":"
-            for k in list(self.keys()):
-                if k.startswith(key):
-                    del(self[k])
-
-    def setall(self, key, values):
-        """Delete frames of the given type and add frames in 'values'."""
-        self.delall(key)
-        for tag in values:
-            self[tag.HashKey] = tag
-
-    def pprint(self):
-        """Return tags in a human-readable format.
-
-        "Human-readable" is used loosely here. The format is intended
-        to mirror that used for Vorbis or APEv2 output, e.g.
-
-            ``TIT2=My Title``
-
-        However, ID3 frames can have multiple keys:
-
-            ``POPM=user@example.org=3 128/255``
-        """
-        frames = sorted(Frame.pprint(s) for s in self.values())
-        return "\n".join(frames)
-
-    def loaded_frame(self, tag):
-        """Deprecated; use the add method."""
-        # turn 2.2 into 2.3/2.4 tags
-        if len(type(tag).__name__) == 3:
-            tag = type(tag).__base__(tag)
-        self[tag.HashKey] = tag
-
-    # add = loaded_frame (and vice versa) break applications that
-    # expect to be able to override loaded_frame (e.g. Quod Libet),
-    # as does making loaded_frame call add.
-    def add(self, frame):
-        """Add a frame to the tag."""
-        return self.loaded_frame(frame)
-
-    def __read_frames(self, data, frames):
-        assert self.version >= ID3Header._V22
-
-        if self.version < ID3Header._V24 and self.f_unsynch:
-            try:
-                data = unsynch.decode(data)
-            except ValueError:
-                pass
-
-        if self.version >= ID3Header._V23:
-            if self.version < ID3Header._V24:
-                bpi = int
-            else:
-                bpi = _determine_bpi(data, frames)
-
-            while data:
-                header = data[:10]
-                try:
-                    name, size, flags = unpack('>4sLH', header)
-                except struct.error:
-                    return  # not enough header
-                if name.strip(b'\x00') == b'':
-                    return
-
-                size = bpi(size)
-                framedata = data[10:10 + size]
-                data = data[10 + size:]
-                self._padding = len(data)
-                if size == 0:
-                    continue  # drop empty frames
-
-                if PY3:
-                    try:
-                        name = name.decode('ascii')
-                    except UnicodeDecodeError:
-                        continue
-
-                try:
-                    # someone writes 2.3 frames with 2.2 names
-                    if name[-1] == "\x00":
-                        tag = Frames_2_2[name[:-1]]
-                        name = tag.__base__.__name__
-
-                    tag = frames[name]
-                except KeyError:
-                    if is_valid_frame_id(name):
-                        yield header + framedata
-                else:
-                    try:
-                        yield tag._fromData(self._header, flags, framedata)
-                    except NotImplementedError:
-                        yield header + framedata
-                    except ID3JunkFrameError:
-                        pass
-        elif self.version >= ID3Header._V22:
-            while data:
-                header = data[0:6]
-                try:
-                    name, size = unpack('>3s3s', header)
-                except struct.error:
-                    return  # not enough header
-                size, = struct.unpack('>L', b'\x00' + size)
-                if name.strip(b'\x00') == b'':
-                    return
-
-                framedata = data[6:6 + size]
-                data = data[6 + size:]
-                self._padding = len(data)
-                if size == 0:
-                    continue  # drop empty frames
-
-                if PY3:
-                    try:
-                        name = name.decode('ascii')
-                    except UnicodeDecodeError:
-                        continue
-
-                try:
-                    tag = frames[name]
-                except KeyError:
-                    if is_valid_frame_id(name):
-                        yield header + framedata
-                else:
-                    try:
-                        yield tag._fromData(self._header, 0, framedata)
-                    except (ID3EncryptionUnsupportedError,
-                            NotImplementedError):
-                        yield header + framedata
-                    except ID3JunkFrameError:
-                        pass
-
-    def _prepare_data(self, fileobj, start, available, v2_version, v23_sep,
-                      pad_func):
-        if v2_version == 3:
-            version = ID3Header._V23
-        elif v2_version == 4:
-            version = ID3Header._V24
-        else:
-            raise ValueError("Only 3 or 4 allowed for v2_version")
-
-        # Sort frames by 'importance'
-        order = ["TIT2", "TPE1", "TRCK", "TALB", "TPOS", "TDRC", "TCON"]
-        order = dict((b, a) for a, b in enumerate(order))
-        last = len(order)
-        frames = sorted(self.items(),
-                        key=lambda a: (order.get(a[0][:4], last), a[0]))
-
-        framedata = [self.__save_frame(frame, version=version, v23_sep=v23_sep)
-                     for (key, frame) in frames]
-
-        # only write unknown frames if they were loaded from the version
-        # we are saving with or upgraded to it
-        if self.__unknown_version == version[:2]:
-            framedata.extend(data for data in self.unknown_frames
-                             if len(data) > 10)
-
-        needed = sum(map(len, framedata)) + 10
-
-        fileobj.seek(0, 2)
-        trailing_size = fileobj.tell() - start
-
-        info = PaddingInfo(available - needed, trailing_size)
-        new_padding = info._get_padding(pad_func)
-        if new_padding < 0:
-            raise error("invalid padding")
-        new_size = needed + new_padding
-
-        new_framesize = BitPaddedInt.to_str(new_size - 10, width=4)
-        header = pack('>3sBBB4s', b'ID3', v2_version, 0, 0, new_framesize)
-
-        data = bytearray(header)
-        for frame in framedata:
-            data += frame
-        assert new_size >= len(data)
-        data += (new_size - len(data)) * b'\x00'
-        assert new_size == len(data)
-
-        return data
-
-    def save(self, filename=None, v1=1, v2_version=4, v23_sep='/',
-             padding=None):
-        """Save changes to a file.
-
-        Args:
-            filename:
-                Filename to save the tag to. If no filename is given,
-                the one most recently loaded is used.
-            v1 (ID3v1SaveOptions):
-                if 0, ID3v1 tags will be removed.
-                if 1, ID3v1 tags will be updated but not added.
-                if 2, ID3v1 tags will be created and/or updated
-            v2 (int):
-                version of ID3v2 tags (3 or 4).
-            v23_sep (str):
-                the separator used to join multiple text values
-                if v2_version == 3. Defaults to '/' but if it's None
-                will be the ID3v2v2.4 null separator.
-            padding (function):
-                A function taking a PaddingInfo which should
-                return the amount of padding to use. If None (default)
-                will default to something reasonable.
-
-        By default Mutagen saves ID3v2.4 tags. If you want to save ID3v2.3
-        tags, you must call method update_to_v23 before saving the file.
-
-        The lack of a way to update only an ID3v1 tag is intentional.
-
-        Can raise id3.error.
-        """
-
-        if filename is None:
-            filename = self.filename
-
-        try:
-            f = open(filename, 'rb+')
-        except IOError as err:
-            from errno import ENOENT
-            if err.errno != ENOENT:
-                raise
-            f = open(filename, 'ab')  # create, then reopen
-            f = open(filename, 'rb+')
-
-        try:
-            try:
-                header = ID3Header(f)
-            except ID3NoHeaderError:
-                old_size = 0
-            else:
-                old_size = header.size
-
-            data = self._prepare_data(
-                f, 0, old_size, v2_version, v23_sep, padding)
-            new_size = len(data)
-
-            if (old_size < new_size):
-                insert_bytes(f, new_size - old_size, old_size)
-            elif (old_size > new_size):
-                delete_bytes(f, old_size - new_size, new_size)
-            f.seek(0)
-            f.write(data)
-
-            self.__save_v1(f, v1)
-
-        finally:
-            f.close()
-
-    def __save_v1(self, f, v1):
-        tag, offset = _find_id3v1(f)
-        has_v1 = tag is not None
-
-        f.seek(offset, 2)
-        if v1 == ID3v1SaveOptions.UPDATE and has_v1 or \
-                v1 == ID3v1SaveOptions.CREATE:
-            f.write(MakeID3v1(self))
-        else:
-            f.truncate()
-
-    def delete(self, filename=None, delete_v1=True, delete_v2=True):
-        """Remove tags from a file.
-
-        If no filename is given, the one most recently loaded is used.
-
-        Keyword arguments:
-
-        * delete_v1 -- delete any ID3v1 tag
-        * delete_v2 -- delete any ID3v2 tag
-        """
-        if filename is None:
-            filename = self.filename
-        delete(filename, delete_v1, delete_v2)
-        self.clear()
-
-    def __save_frame(self, frame, name=None, version=ID3Header._V24,
-                     v23_sep=None):
-        flags = 0
-        if isinstance(frame, TextFrame):
-            if len(str(frame)) == 0:
-                return b''
-
-        if version == ID3Header._V23:
-            framev23 = frame._get_v23_frame(sep=v23_sep)
-            framedata = framev23._writeData()
-        else:
-            framedata = frame._writeData()
-
-        usize = len(framedata)
-        if usize > 2048:
-            # Disabled as this causes iTunes and other programs
-            # to fail to find these frames, which usually includes
-            # e.g. APIC.
-            # framedata = BitPaddedInt.to_str(usize) + framedata.encode('zlib')
-            # flags |= Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN
-            pass
-
-        if version == ID3Header._V24:
-            bits = 7
-        elif version == ID3Header._V23:
-            bits = 8
-        else:
-            raise ValueError
-
-        datasize = BitPaddedInt.to_str(len(framedata), width=4, bits=bits)
-
-        if name is not None:
-            assert isinstance(name, bytes)
-            frame_name = name
-        else:
-            frame_name = type(frame).__name__
-            if PY3:
-                frame_name = frame_name.encode("ascii")
-
-        header = pack('>4s4sH', frame_name, datasize, flags)
-        return header + framedata
-
-    def __update_common(self):
-        """Updates done by both v23 and v24 update"""
-
-        if "TCON" in self:
-            # Get rid of "(xx)Foobr" format.
-            self["TCON"].genres = self["TCON"].genres
-
-        # ID3v2.2 LNK frames are just way too different to upgrade.
-        for frame in self.getall("LINK"):
-            if len(frame.frameid) != 4:
-                del self[frame.HashKey]
-
-        mimes = {"PNG": "image/png", "JPG": "image/jpeg"}
-        for pic in self.getall("APIC"):
-            if pic.mime in mimes:
-                newpic = APIC(
-                    encoding=pic.encoding, mime=mimes[pic.mime],
-                    type=pic.type, desc=pic.desc, data=pic.data)
-                self.add(newpic)
-
-    def update_to_v24(self):
-        """Convert older tags into an ID3v2.4 tag.
-
-        This updates old ID3v2 frames to ID3v2.4 ones (e.g. TYER to
-        TDRC). If you intend to save tags, you must call this function
-        at some point; it is called by default when loading the tag.
-        """
-
-        self.__update_common()
-
-        if self.__unknown_version == (2, 3):
-            # convert unknown 2.3 frames (flags/size) to 2.4
-            converted = []
-            for frame in self.unknown_frames:
-                try:
-                    name, size, flags = unpack('>4sLH', frame[:10])
-                except struct.error:
-                    continue
-
-                try:
-                    frame = BinaryFrame._fromData(
-                        self._header, flags, frame[10:])
-                except (error, NotImplementedError):
-                    continue
-
-                converted.append(self.__save_frame(frame, name=name))
-            self.unknown_frames[:] = converted
-            self.__unknown_version = (2, 4)
-
-        # TDAT, TYER, and TIME have been turned into TDRC.
-        try:
-            date = text_type(self.get("TYER", ""))
-            if date.strip(u"\x00"):
-                self.pop("TYER")
-                dat = text_type(self.get("TDAT", ""))
-                if dat.strip("\x00"):
-                    self.pop("TDAT")
-                    date = "%s-%s-%s" % (date, dat[2:], dat[:2])
-                    time = text_type(self.get("TIME", ""))
-                    if time.strip("\x00"):
-                        self.pop("TIME")
-                        date += "T%s:%s:00" % (time[:2], time[2:])
-                if "TDRC" not in self:
-                    self.add(TDRC(encoding=0, text=date))
-        except UnicodeDecodeError:
-            # Old ID3 tags have *lots* of Unicode problems, so if TYER
-            # is bad, just chuck the frames.
-            pass
-
-        # TORY can be the first part of a TDOR.
-        if "TORY" in self:
-            f = self.pop("TORY")
-            if "TDOR" not in self:
-                try:
-                    self.add(TDOR(encoding=0, text=str(f)))
-                except UnicodeDecodeError:
-                    pass
-
-        # IPLS is now TIPL.
-        if "IPLS" in self:
-            f = self.pop("IPLS")
-            if "TIPL" not in self:
-                self.add(TIPL(encoding=f.encoding, people=f.people))
-
-        # These can't be trivially translated to any ID3v2.4 tags, or
-        # should have been removed already.
-        for key in ["RVAD", "EQUA", "TRDA", "TSIZ", "TDAT", "TIME", "CRM"]:
-            if key in self:
-                del(self[key])
-
-    def update_to_v23(self):
-        """Convert older (and newer) tags into an ID3v2.3 tag.
-
-        This updates incompatible ID3v2 frames to ID3v2.3 ones. If you
-        intend to save tags as ID3v2.3, you must call this function
-        at some point.
-
-        If you want to to go off spec and include some v2.4 frames
-        in v2.3, remove them before calling this and add them back afterwards.
-        """
-
-        self.__update_common()
-
-        # we could downgrade unknown v2.4 frames here, but given that
-        # the main reason to save v2.3 is compatibility and this
-        # might increase the chance of some parser breaking.. better not
-
-        # TMCL, TIPL -> TIPL
-        if "TIPL" in self or "TMCL" in self:
-            people = []
-            if "TIPL" in self:
-                f = self.pop("TIPL")
-                people.extend(f.people)
-            if "TMCL" in self:
-                f = self.pop("TMCL")
-                people.extend(f.people)
-            if "IPLS" not in self:
-                self.add(IPLS(encoding=f.encoding, people=people))
-
-        # TDOR -> TORY
-        if "TDOR" in self:
-            f = self.pop("TDOR")
-            if f.text:
-                d = f.text[0]
-                if d.year and "TORY" not in self:
-                    self.add(TORY(encoding=f.encoding, text="%04d" % d.year))
-
-        # TDRC -> TYER, TDAT, TIME
-        if "TDRC" in self:
-            f = self.pop("TDRC")
-            if f.text:
-                d = f.text[0]
-                if d.year and "TYER" not in self:
-                    self.add(TYER(encoding=f.encoding, text="%04d" % d.year))
-                if d.month and d.day and "TDAT" not in self:
-                    self.add(TDAT(encoding=f.encoding,
-                                  text="%02d%02d" % (d.day, d.month)))
-                if d.hour and d.minute and "TIME" not in self:
-                    self.add(TIME(encoding=f.encoding,
-                                  text="%02d%02d" % (d.hour, d.minute)))
-
-        # New frames added in v2.4
-        v24_frames = [
-            'ASPI', 'EQU2', 'RVA2', 'SEEK', 'SIGN', 'TDEN', 'TDOR',
-            'TDRC', 'TDRL', 'TDTG', 'TIPL', 'TMCL', 'TMOO', 'TPRO',
-            'TSOA', 'TSOP', 'TSOT', 'TSST',
-        ]
-
-        for key in v24_frames:
-            if key in self:
-                del(self[key])
-
-
-def delete(filename, delete_v1=True, delete_v2=True):
-    """Remove tags from a file.
-
-    Keyword arguments:
-
-    * delete_v1 -- delete any ID3v1 tag
-    * delete_v2 -- delete any ID3v2 tag
-    """
-
-    with open(filename, 'rb+') as f:
-
-        if delete_v1:
-            tag, offset = _find_id3v1(f)
-            if tag is not None:
-                f.seek(offset, 2)
-                f.truncate()
-
-        # technically an insize=0 tag is invalid, but we delete it anyway
-        # (primarily because we used to write it)
-        if delete_v2:
-            f.seek(0, 0)
-            idata = f.read(10)
-            try:
-                id3, vmaj, vrev, flags, insize = unpack('>3sBBB4s', idata)
-            except struct.error:
-                id3, insize = b'', -1
-            insize = BitPaddedInt(insize)
-            if id3 == b'ID3' and insize >= 0:
-                delete_bytes(f, insize + 10, 0)
-
-
-# support open(filename) as interface
-Open = ID3
-
-
-def _determine_bpi(data, frames, EMPTY=b"\x00" * 10):
-    """Takes id3v2.4 frame data and determines if ints or bitpaddedints
-    should be used for parsing. Needed because iTunes used to write
-    normal ints for frame sizes.
-    """
-
-    # count number of tags found as BitPaddedInt and how far past
-    o = 0
-    asbpi = 0
-    while o < len(data) - 10:
-        part = data[o:o + 10]
-        if part == EMPTY:
-            bpioff = -((len(data) - o) % 10)
-            break
-        name, size, flags = unpack('>4sLH', part)
-        size = BitPaddedInt(size)
-        o += 10 + size
-        if PY3:
-            try:
-                name = name.decode("ascii")
-            except UnicodeDecodeError:
-                continue
-        if name in frames:
-            asbpi += 1
-    else:
-        bpioff = o - len(data)
-
-    # count number of tags found as int and how far past
-    o = 0
-    asint = 0
-    while o < len(data) - 10:
-        part = data[o:o + 10]
-        if part == EMPTY:
-            intoff = -((len(data) - o) % 10)
-            break
-        name, size, flags = unpack('>4sLH', part)
-        o += 10 + size
-        if PY3:
-            try:
-                name = name.decode("ascii")
-            except UnicodeDecodeError:
-                continue
-        if name in frames:
-            asint += 1
-    else:
-        intoff = o - len(data)
-
-    # if more tags as int, or equal and bpi is past and int is not
-    if asint > asbpi or (asint == asbpi and (bpioff >= 1 and intoff <= 1)):
-        return int
-    return BitPaddedInt
-
-
-def _find_id3v1(fileobj):
-    """Returns a tuple of (id3tag, offset_to_end) or (None, 0)
-
-    offset mainly because we used to write too short tags in some cases and
-    we need the offset to delete them.
-    """
-
-    # id3v1 is always at the end (after apev2)
-
-    extra_read = b"APETAGEX".index(b"TAG")
-
-    try:
-        fileobj.seek(-128 - extra_read, 2)
-    except IOError as e:
-        if e.errno == errno.EINVAL:
-            # If the file is too small, might be ok since we wrote too small
-            # tags at some point. let's see how the parsing goes..
-            fileobj.seek(0, 0)
-        else:
-            raise
-
-    data = fileobj.read(128 + extra_read)
-    try:
-        idx = data.index(b"TAG")
-    except ValueError:
-        return (None, 0)
-    else:
-        # FIXME: make use of the apev2 parser here
-        # if TAG is part of APETAGEX assume this is an APEv2 tag
-        try:
-            ape_idx = data.index(b"APETAGEX")
-        except ValueError:
-            pass
-        else:
-            if idx == ape_idx + extra_read:
-                return (None, 0)
-
-        tag = ParseID3v1(data[idx:])
-        if tag is None:
-            return (None, 0)
-
-        offset = idx - len(data)
-        return (tag, offset)
-
-
-# ID3v1.1 support.
-def ParseID3v1(data):
-    """Parse an ID3v1 tag, returning a list of ID3v2.4 frames.
-
-    Returns a {frame_name: frame} dict or None.
-    """
-
-    try:
-        data = data[data.index(b"TAG"):]
-    except ValueError:
-        return None
-    if 128 < len(data) or len(data) < 124:
-        return None
-
-    # Issue #69 - Previous versions of Mutagen, when encountering
-    # out-of-spec TDRC and TYER frames of less than four characters,
-    # wrote only the characters available - e.g. "1" or "" - into the
-    # year field. To parse those, reduce the size of the year field.
-    # Amazingly, "0s" works as a struct format string.
-    unpack_fmt = "3s30s30s30s%ds29sBB" % (len(data) - 124)
-
-    try:
-        tag, title, artist, album, year, comment, track, genre = unpack(
-            unpack_fmt, data)
-    except StructError:
-        return None
-
-    if tag != b"TAG":
-        return None
-
-    def fix(data):
-        return data.split(b"\x00")[0].strip().decode('latin1')
-
-    title, artist, album, year, comment = map(
-        fix, [title, artist, album, year, comment])
-
-    frames = {}
-    if title:
-        frames["TIT2"] = TIT2(encoding=0, text=title)
-    if artist:
-        frames["TPE1"] = TPE1(encoding=0, text=[artist])
-    if album:
-        frames["TALB"] = TALB(encoding=0, text=album)
-    if year:
-        frames["TDRC"] = TDRC(encoding=0, text=year)
-    if comment:
-        frames["COMM"] = COMM(
-            encoding=0, lang="eng", desc="ID3v1 Comment", text=comment)
-    # Don't read a track number if it looks like the comment was
-    # padded with spaces instead of nulls (thanks, WinAmp).
-    if track and ((track != 32) or (data[-3] == b'\x00'[0])):
-        frames["TRCK"] = TRCK(encoding=0, text=str(track))
-    if genre != 255:
-        frames["TCON"] = TCON(encoding=0, text=str(genre))
-    return frames
-
-
-def MakeID3v1(id3):
-    """Return an ID3v1.1 tag string from a dict of ID3v2.4 frames."""
-
-    v1 = {}
-
-    for v2id, name in {"TIT2": "title", "TPE1": "artist",
-                       "TALB": "album"}.items():
-        if v2id in id3:
-            text = id3[v2id].text[0].encode('latin1', 'replace')[:30]
-        else:
-            text = b""
-        v1[name] = text + (b"\x00" * (30 - len(text)))
-
-    if "COMM" in id3:
-        cmnt = id3["COMM"].text[0].encode('latin1', 'replace')[:28]
-    else:
-        cmnt = b""
-    v1["comment"] = cmnt + (b"\x00" * (29 - len(cmnt)))
-
-    if "TRCK" in id3:
-        try:
-            v1["track"] = chr_(+id3["TRCK"])
-        except ValueError:
-            v1["track"] = b"\x00"
-    else:
-        v1["track"] = b"\x00"
-
-    if "TCON" in id3:
-        try:
-            genre = id3["TCON"].genres[0]
-        except IndexError:
-            pass
-        else:
-            if genre in TCON.GENRES:
-                v1["genre"] = chr_(TCON.GENRES.index(genre))
-    if "genre" not in v1:
-        v1["genre"] = b"\xff"
-
-    if "TDRC" in id3:
-        year = text_type(id3["TDRC"]).encode('ascii')
-    elif "TYER" in id3:
-        year = text_type(id3["TYER"]).encode('ascii')
-    else:
-        year = b""
-    v1["year"] = (year + b"\x00\x00\x00\x00")[:4]
-
-    return (
-        b"TAG" +
-        v1["title"] +
-        v1["artist"] +
-        v1["album"] +
-        v1["year"] +
-        v1["comment"] +
-        v1["track"] +
-        v1["genre"]
-    )
-
-
-class ID3FileType(mutagen.FileType):
-    """An unknown type of file with ID3 tags."""
-
-    ID3 = ID3
-
-    class _Info(mutagen.StreamInfo):
-        length = 0
-
-        def __init__(self, fileobj, offset):
-            pass
-
-        @staticmethod
-        def pprint():
-            return "Unknown format with ID3 tag"
-
-    @staticmethod
-    def score(filename, fileobj, header_data):
-        return header_data.startswith(b"ID3")
-
-    def add_tags(self, ID3=None):
-        """Add an empty ID3 tag to the file.
-
-        A custom tag reader may be used in instead of the default
-        mutagen.id3.ID3 object, e.g. an EasyID3 reader.
-        """
-        if ID3 is None:
-            ID3 = self.ID3
-        if self.tags is None:
-            self.ID3 = ID3
-            self.tags = ID3()
-        else:
-            raise error("an ID3 tag already exists")
-
-    def load(self, filename, ID3=None, **kwargs):
-        """Load stream and tag information from a file.
-
-        A custom tag reader may be used in instead of the default
-        mutagen.id3.ID3 object, e.g. an EasyID3 reader.
-        """
-
-        if ID3 is None:
-            ID3 = self.ID3
-        else:
-            # If this was initialized with EasyID3, remember that for
-            # when tags are auto-instantiated in add_tags.
-            self.ID3 = ID3
-        self.filename = filename
-        try:
-            self.tags = ID3(filename, **kwargs)
-        except ID3NoHeaderError:
-            self.tags = None
-
-        if self.tags is not None:
-            try:
-                offset = self.tags.size
-            except AttributeError:
-                offset = None
-        else:
-            offset = None
-
-        with open(filename, "rb") as fileobj:
-            self.info = self._Info(fileobj, offset)
diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/__init__.cpython-35.pyc
deleted file mode 100644
index f423db1af7e6371db8a6eea5d505a35cb1aee338..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 27785
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

diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_frames.cpython-35.pyc
deleted file mode 100644
index 0331df593173cd69e7761b5b023fe931ec86c0b1..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 64560
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

diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_specs.cpython-35.pyc
deleted file mode 100644
index 9af16aa1ca17196b9bf38ccd837732df328d8b9f..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 22816
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
bC^ysnQE2@B+4Mj<mL5&Nls=g5PpAJcimw4r

diff --git a/resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/id3/__pycache__/_util.cpython-35.pyc
deleted file mode 100644
index c2c75dadb9a532645a03141da6396906431288b9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 4933
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

diff --git a/resources/lib/libraries/mutagen/id3/_frames.py b/resources/lib/libraries/mutagen/id3/_frames.py
deleted file mode 100644
index c185cef3..00000000
--- a/resources/lib/libraries/mutagen/id3/_frames.py
+++ /dev/null
@@ -1,1925 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Michael Urman
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-import zlib
-from struct import unpack
-
-from ._util import ID3JunkFrameError, ID3EncryptionUnsupportedError, unsynch
-from ._specs import (
-    BinaryDataSpec, StringSpec, Latin1TextSpec, EncodedTextSpec, ByteSpec,
-    EncodingSpec, ASPIIndexSpec, SizedIntegerSpec, IntegerSpec,
-    VolumeAdjustmentsSpec, VolumePeakSpec, VolumeAdjustmentSpec,
-    ChannelSpec, MultiSpec, SynchronizedTextSpec, KeyEventSpec, TimeStampSpec,
-    EncodedNumericPartTextSpec, EncodedNumericTextSpec, SpecError)
-from .._compat import text_type, string_types, swap_to_string, iteritems, izip
-
-
-def is_valid_frame_id(frame_id):
-    return frame_id.isalnum() and frame_id.isupper()
-
-
-def _bytes2key(b):
-    assert isinstance(b, bytes)
-
-    return b.decode("latin1")
-
-
-class Frame(object):
-    """Fundamental unit of ID3 data.
-
-    ID3 tags are split into frames. Each frame has a potentially
-    different structure, and so this base class is not very featureful.
-    """
-
-    FLAG23_ALTERTAG = 0x8000
-    FLAG23_ALTERFILE = 0x4000
-    FLAG23_READONLY = 0x2000
-    FLAG23_COMPRESS = 0x0080
-    FLAG23_ENCRYPT = 0x0040
-    FLAG23_GROUP = 0x0020
-
-    FLAG24_ALTERTAG = 0x4000
-    FLAG24_ALTERFILE = 0x2000
-    FLAG24_READONLY = 0x1000
-    FLAG24_GROUPID = 0x0040
-    FLAG24_COMPRESS = 0x0008
-    FLAG24_ENCRYPT = 0x0004
-    FLAG24_UNSYNCH = 0x0002
-    FLAG24_DATALEN = 0x0001
-
-    _framespec = []
-
-    def __init__(self, *args, **kwargs):
-        if len(args) == 1 and len(kwargs) == 0 and \
-                isinstance(args[0], type(self)):
-            other = args[0]
-            # ask the sub class to fill in our data
-            other._to_other(self)
-        else:
-            for checker, val in izip(self._framespec, args):
-                setattr(self, checker.name, checker.validate(self, val))
-            for checker in self._framespec[len(args):]:
-                try:
-                    validated = checker.validate(
-                        self, kwargs.get(checker.name, None))
-                except ValueError as e:
-                    raise ValueError("%s: %s" % (checker.name, e))
-                setattr(self, checker.name, validated)
-
-    def _to_other(self, other):
-        # this impl covers subclasses with the same framespec
-        if other._framespec is not self._framespec:
-            raise ValueError
-
-        for checker in other._framespec:
-            setattr(other, checker.name, getattr(self, checker.name))
-
-    def _get_v23_frame(self, **kwargs):
-        """Returns a frame copy which is suitable for writing into a v2.3 tag.
-
-        kwargs get passed to the specs.
-        """
-
-        new_kwargs = {}
-        for checker in self._framespec:
-            name = checker.name
-            value = getattr(self, name)
-            new_kwargs[name] = checker._validate23(self, value, **kwargs)
-        return type(self)(**new_kwargs)
-
-    @property
-    def HashKey(self):
-        """An internal key used to ensure frame uniqueness in a tag"""
-
-        return self.FrameID
-
-    @property
-    def FrameID(self):
-        """ID3v2 three or four character frame ID"""
-
-        return type(self).__name__
-
-    def __repr__(self):
-        """Python representation of a frame.
-
-        The string returned is a valid Python expression to construct
-        a copy of this frame.
-        """
-        kw = []
-        for attr in self._framespec:
-            # so repr works during __init__
-            if hasattr(self, attr.name):
-                kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
-        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
-
-    def _readData(self, data):
-        """Raises ID3JunkFrameError; Returns leftover data"""
-
-        for reader in self._framespec:
-            if len(data):
-                try:
-                    value, data = reader.read(self, data)
-                except SpecError as e:
-                    raise ID3JunkFrameError(e)
-            else:
-                raise ID3JunkFrameError("no data left")
-            setattr(self, reader.name, value)
-
-        return data
-
-    def _writeData(self):
-        data = []
-        for writer in self._framespec:
-            data.append(writer.write(self, getattr(self, writer.name)))
-        return b''.join(data)
-
-    def pprint(self):
-        """Return a human-readable representation of the frame."""
-        return "%s=%s" % (type(self).__name__, self._pprint())
-
-    def _pprint(self):
-        return "[unrepresentable data]"
-
-    @classmethod
-    def _fromData(cls, id3, tflags, data):
-        """Construct this ID3 frame from raw string data.
-
-        Raises:
-
-        ID3JunkFrameError in case parsing failed
-        NotImplementedError in case parsing isn't implemented
-        ID3EncryptionUnsupportedError in case the frame is encrypted.
-        """
-
-        if id3.version >= id3._V24:
-            if tflags & (Frame.FLAG24_COMPRESS | Frame.FLAG24_DATALEN):
-                # The data length int is syncsafe in 2.4 (but not 2.3).
-                # However, we don't actually need the data length int,
-                # except to work around a QL 0.12 bug, and in that case
-                # all we need are the raw bytes.
-                datalen_bytes = data[:4]
-                data = data[4:]
-            if tflags & Frame.FLAG24_UNSYNCH or id3.f_unsynch:
-                try:
-                    data = unsynch.decode(data)
-                except ValueError:
-                    # Some things write synch-unsafe data with either the frame
-                    # or global unsynch flag set. Try to load them as is.
-                    # https://bitbucket.org/lazka/mutagen/issue/210
-                    # https://bitbucket.org/lazka/mutagen/issue/223
-                    pass
-            if tflags & Frame.FLAG24_ENCRYPT:
-                raise ID3EncryptionUnsupportedError
-            if tflags & Frame.FLAG24_COMPRESS:
-                try:
-                    data = zlib.decompress(data)
-                except zlib.error as err:
-                    # the initial mutagen that went out with QL 0.12 did not
-                    # write the 4 bytes of uncompressed size. Compensate.
-                    data = datalen_bytes + data
-                    try:
-                        data = zlib.decompress(data)
-                    except zlib.error as err:
-                        raise ID3JunkFrameError(
-                            'zlib: %s: %r' % (err, data))
-
-        elif id3.version >= id3._V23:
-            if tflags & Frame.FLAG23_COMPRESS:
-                usize, = unpack('>L', data[:4])
-                data = data[4:]
-            if tflags & Frame.FLAG23_ENCRYPT:
-                raise ID3EncryptionUnsupportedError
-            if tflags & Frame.FLAG23_COMPRESS:
-                try:
-                    data = zlib.decompress(data)
-                except zlib.error as err:
-                    raise ID3JunkFrameError('zlib: %s: %r' % (err, data))
-
-        frame = cls()
-        frame._readData(data)
-        return frame
-
-    def __hash__(self):
-        raise TypeError("Frame objects are unhashable")
-
-
-class FrameOpt(Frame):
-    """A frame with optional parts.
-
-    Some ID3 frames have optional data; this class extends Frame to
-    provide support for those parts.
-    """
-
-    _optionalspec = []
-
-    def __init__(self, *args, **kwargs):
-        super(FrameOpt, self).__init__(*args, **kwargs)
-        for spec in self._optionalspec:
-            if spec.name in kwargs:
-                validated = spec.validate(self, kwargs[spec.name])
-                setattr(self, spec.name, validated)
-            else:
-                break
-
-    def _to_other(self, other):
-        super(FrameOpt, self)._to_other(other)
-
-        # this impl covers subclasses with the same optionalspec
-        if other._optionalspec is not self._optionalspec:
-            raise ValueError
-
-        for checker in other._optionalspec:
-            if hasattr(self, checker.name):
-                setattr(other, checker.name, getattr(self, checker.name))
-
-    def _readData(self, data):
-        """Raises ID3JunkFrameError; Returns leftover data"""
-
-        for reader in self._framespec:
-            if len(data):
-                try:
-                    value, data = reader.read(self, data)
-                except SpecError as e:
-                    raise ID3JunkFrameError(e)
-            else:
-                raise ID3JunkFrameError("no data left")
-            setattr(self, reader.name, value)
-
-        if data:
-            for reader in self._optionalspec:
-                if len(data):
-                    try:
-                        value, data = reader.read(self, data)
-                    except SpecError as e:
-                        raise ID3JunkFrameError(e)
-                else:
-                    break
-                setattr(self, reader.name, value)
-
-        return data
-
-    def _writeData(self):
-        data = []
-        for writer in self._framespec:
-            data.append(writer.write(self, getattr(self, writer.name)))
-        for writer in self._optionalspec:
-            try:
-                data.append(writer.write(self, getattr(self, writer.name)))
-            except AttributeError:
-                break
-        return b''.join(data)
-
-    def __repr__(self):
-        kw = []
-        for attr in self._framespec:
-            kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
-        for attr in self._optionalspec:
-            if hasattr(self, attr.name):
-                kw.append('%s=%r' % (attr.name, getattr(self, attr.name)))
-        return '%s(%s)' % (type(self).__name__, ', '.join(kw))
-
-
-@swap_to_string
-class TextFrame(Frame):
-    """Text strings.
-
-    Text frames support casts to unicode or str objects, as well as
-    list-like indexing, extend, and append.
-
-    Iterating over a TextFrame iterates over its strings, not its
-    characters.
-
-    Text frames have a 'text' attribute which is the list of strings,
-    and an 'encoding' attribute; 0 for ISO-8859 1, 1 UTF-16, 2 for
-    UTF-16BE, and 3 for UTF-8. If you don't want to worry about
-    encodings, just set it to 3.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'),
-    ]
-
-    def __bytes__(self):
-        return text_type(self).encode('utf-8')
-
-    def __str__(self):
-        return u'\u0000'.join(self.text)
-
-    def __eq__(self, other):
-        if isinstance(other, bytes):
-            return bytes(self) == other
-        elif isinstance(other, text_type):
-            return text_type(self) == other
-        return self.text == other
-
-    __hash__ = Frame.__hash__
-
-    def __getitem__(self, item):
-        return self.text[item]
-
-    def __iter__(self):
-        return iter(self.text)
-
-    def append(self, value):
-        """Append a string."""
-
-        return self.text.append(value)
-
-    def extend(self, value):
-        """Extend the list by appending all strings from the given list."""
-
-        return self.text.extend(value)
-
-    def _pprint(self):
-        return " / ".join(self.text)
-
-
-class NumericTextFrame(TextFrame):
-    """Numerical text strings.
-
-    The numeric value of these frames can be gotten with unary plus, e.g.::
-
-        frame = TLEN('12345')
-        length = +frame
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        MultiSpec('text', EncodedNumericTextSpec('text'), sep=u'\u0000'),
-    ]
-
-    def __pos__(self):
-        """Return the numerical value of the string."""
-        return int(self.text[0])
-
-
-class NumericPartTextFrame(TextFrame):
-    """Multivalue numerical text strings.
-
-    These strings indicate 'part (e.g. track) X of Y', and unary plus
-    returns the first value::
-
-        frame = TRCK('4/15')
-        track = +frame # track == 4
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        MultiSpec('text', EncodedNumericPartTextSpec('text'), sep=u'\u0000'),
-    ]
-
-    def __pos__(self):
-        return int(self.text[0].split("/")[0])
-
-
-@swap_to_string
-class TimeStampTextFrame(TextFrame):
-    """A list of time stamps.
-
-    The 'text' attribute in this frame is a list of ID3TimeStamp
-    objects, not a list of strings.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        MultiSpec('text', TimeStampSpec('stamp'), sep=u','),
-    ]
-
-    def __bytes__(self):
-        return text_type(self).encode('utf-8')
-
-    def __str__(self):
-        return u','.join([stamp.text for stamp in self.text])
-
-    def _pprint(self):
-        return u" / ".join([stamp.text for stamp in self.text])
-
-
-@swap_to_string
-class UrlFrame(Frame):
-    """A frame containing a URL string.
-
-    The ID3 specification is silent about IRIs and normalized URL
-    forms. Mutagen assumes all URLs in files are encoded as Latin 1,
-    but string conversion of this frame returns a UTF-8 representation
-    for compatibility with other string conversions.
-
-    The only sane way to handle URLs in MP3s is to restrict them to
-    ASCII.
-    """
-
-    _framespec = [Latin1TextSpec('url')]
-
-    def __bytes__(self):
-        return self.url.encode('utf-8')
-
-    def __str__(self):
-        return self.url
-
-    def __eq__(self, other):
-        return self.url == other
-
-    __hash__ = Frame.__hash__
-
-    def _pprint(self):
-        return self.url
-
-
-class UrlFrameU(UrlFrame):
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.url)
-
-
-class TALB(TextFrame):
-    "Album"
-
-
-class TBPM(NumericTextFrame):
-    "Beats per minute"
-
-
-class TCOM(TextFrame):
-    "Composer"
-
-
-class TCON(TextFrame):
-    """Content type (Genre)
-
-    ID3 has several ways genres can be represented; for convenience,
-    use the 'genres' property rather than the 'text' attribute.
-    """
-
-    from mutagen._constants import GENRES
-    GENRES = GENRES
-
-    def __get_genres(self):
-        genres = []
-        import re
-        genre_re = re.compile(r"((?:\((?P<id>[0-9]+|RX|CR)\))*)(?P<str>.+)?")
-        for value in self.text:
-            # 255 possible entries in id3v1
-            if value.isdigit() and int(value) < 256:
-                try:
-                    genres.append(self.GENRES[int(value)])
-                except IndexError:
-                    genres.append(u"Unknown")
-            elif value == "CR":
-                genres.append(u"Cover")
-            elif value == "RX":
-                genres.append(u"Remix")
-            elif value:
-                newgenres = []
-                genreid, dummy, genrename = genre_re.match(value).groups()
-
-                if genreid:
-                    for gid in genreid[1:-1].split(")("):
-                        if gid.isdigit() and int(gid) < len(self.GENRES):
-                            gid = text_type(self.GENRES[int(gid)])
-                            newgenres.append(gid)
-                        elif gid == "CR":
-                            newgenres.append(u"Cover")
-                        elif gid == "RX":
-                            newgenres.append(u"Remix")
-                        else:
-                            newgenres.append(u"Unknown")
-
-                if genrename:
-                    # "Unescaping" the first parenthesis
-                    if genrename.startswith("(("):
-                        genrename = genrename[1:]
-                    if genrename not in newgenres:
-                        newgenres.append(genrename)
-
-                genres.extend(newgenres)
-
-        return genres
-
-    def __set_genres(self, genres):
-        if isinstance(genres, string_types):
-            genres = [genres]
-        self.text = [self.__decode(g) for g in genres]
-
-    def __decode(self, value):
-        if isinstance(value, bytes):
-            enc = EncodedTextSpec._encodings[self.encoding][0]
-            return value.decode(enc)
-        else:
-            return value
-
-    genres = property(__get_genres, __set_genres, None,
-                      "A list of genres parsed from the raw text data.")
-
-    def _pprint(self):
-        return " / ".join(self.genres)
-
-
-class TCOP(TextFrame):
-    "Copyright (c)"
-
-
-class TCMP(NumericTextFrame):
-    "iTunes Compilation Flag"
-
-
-class TDAT(TextFrame):
-    "Date of recording (DDMM)"
-
-
-class TDEN(TimeStampTextFrame):
-    "Encoding Time"
-
-
-class TDES(TextFrame):
-    "iTunes Podcast Description"
-
-
-class TDOR(TimeStampTextFrame):
-    "Original Release Time"
-
-
-class TDLY(NumericTextFrame):
-    "Audio Delay (ms)"
-
-
-class TDRC(TimeStampTextFrame):
-    "Recording Time"
-
-
-class TDRL(TimeStampTextFrame):
-    "Release Time"
-
-
-class TDTG(TimeStampTextFrame):
-    "Tagging Time"
-
-
-class TENC(TextFrame):
-    "Encoder"
-
-
-class TEXT(TextFrame):
-    "Lyricist"
-
-
-class TFLT(TextFrame):
-    "File type"
-
-
-class TGID(TextFrame):
-    "iTunes Podcast Identifier"
-
-
-class TIME(TextFrame):
-    "Time of recording (HHMM)"
-
-
-class TIT1(TextFrame):
-    "Content group description"
-
-
-class TIT2(TextFrame):
-    "Title"
-
-
-class TIT3(TextFrame):
-    "Subtitle/Description refinement"
-
-
-class TKEY(TextFrame):
-    "Starting Key"
-
-
-class TLAN(TextFrame):
-    "Audio Languages"
-
-
-class TLEN(NumericTextFrame):
-    "Audio Length (ms)"
-
-
-class TMED(TextFrame):
-    "Source Media Type"
-
-
-class TMOO(TextFrame):
-    "Mood"
-
-
-class TOAL(TextFrame):
-    "Original Album"
-
-
-class TOFN(TextFrame):
-    "Original Filename"
-
-
-class TOLY(TextFrame):
-    "Original Lyricist"
-
-
-class TOPE(TextFrame):
-    "Original Artist/Performer"
-
-
-class TORY(NumericTextFrame):
-    "Original Release Year"
-
-
-class TOWN(TextFrame):
-    "Owner/Licensee"
-
-
-class TPE1(TextFrame):
-    "Lead Artist/Performer/Soloist/Group"
-
-
-class TPE2(TextFrame):
-    "Band/Orchestra/Accompaniment"
-
-
-class TPE3(TextFrame):
-    "Conductor"
-
-
-class TPE4(TextFrame):
-    "Interpreter/Remixer/Modifier"
-
-
-class TPOS(NumericPartTextFrame):
-    "Part of set"
-
-
-class TPRO(TextFrame):
-    "Produced (P)"
-
-
-class TPUB(TextFrame):
-    "Publisher"
-
-
-class TRCK(NumericPartTextFrame):
-    "Track Number"
-
-
-class TRDA(TextFrame):
-    "Recording Dates"
-
-
-class TRSN(TextFrame):
-    "Internet Radio Station Name"
-
-
-class TRSO(TextFrame):
-    "Internet Radio Station Owner"
-
-
-class TSIZ(NumericTextFrame):
-    "Size of audio data (bytes)"
-
-
-class TSO2(TextFrame):
-    "iTunes Album Artist Sort"
-
-
-class TSOA(TextFrame):
-    "Album Sort Order key"
-
-
-class TSOC(TextFrame):
-    "iTunes Composer Sort"
-
-
-class TSOP(TextFrame):
-    "Perfomer Sort Order key"
-
-
-class TSOT(TextFrame):
-    "Title Sort Order key"
-
-
-class TSRC(TextFrame):
-    "International Standard Recording Code (ISRC)"
-
-
-class TSSE(TextFrame):
-    "Encoder settings"
-
-
-class TSST(TextFrame):
-    "Set Subtitle"
-
-
-class TYER(NumericTextFrame):
-    "Year of recording"
-
-
-class TXXX(TextFrame):
-    """User-defined text data.
-
-    TXXX frames have a 'desc' attribute which is set to any Unicode
-    value (though the encoding of the text and the description must be
-    the same). Many taggers use this frame to store freeform keys.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        EncodedTextSpec('desc'),
-        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.desc)
-
-    def _pprint(self):
-        return "%s=%s" % (self.desc, " / ".join(self.text))
-
-
-class WCOM(UrlFrameU):
-    "Commercial Information"
-
-
-class WCOP(UrlFrame):
-    "Copyright Information"
-
-
-class WFED(UrlFrame):
-    "iTunes Podcast Feed"
-
-
-class WOAF(UrlFrame):
-    "Official File Information"
-
-
-class WOAR(UrlFrameU):
-    "Official Artist/Performer Information"
-
-
-class WOAS(UrlFrame):
-    "Official Source Information"
-
-
-class WORS(UrlFrame):
-    "Official Internet Radio Information"
-
-
-class WPAY(UrlFrame):
-    "Payment Information"
-
-
-class WPUB(UrlFrame):
-    "Official Publisher Information"
-
-
-class WXXX(UrlFrame):
-    """User-defined URL data.
-
-    Like TXXX, this has a freeform description associated with it.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        EncodedTextSpec('desc'),
-        Latin1TextSpec('url'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.desc)
-
-
-class PairedTextFrame(Frame):
-    """Paired text strings.
-
-    Some ID3 frames pair text strings, to associate names with a more
-    specific involvement in the song. The 'people' attribute of these
-    frames contains a list of pairs::
-
-        [['trumpet', 'Miles Davis'], ['bass', 'Paul Chambers']]
-
-    Like text frames, these frames also have an encoding attribute.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        MultiSpec('people',
-                  EncodedTextSpec('involvement'),
-                  EncodedTextSpec('person'))
-    ]
-
-    def __eq__(self, other):
-        return self.people == other
-
-    __hash__ = Frame.__hash__
-
-
-class TIPL(PairedTextFrame):
-    "Involved People List"
-
-
-class TMCL(PairedTextFrame):
-    "Musicians Credits List"
-
-
-class IPLS(TIPL):
-    "Involved People List"
-
-
-class BinaryFrame(Frame):
-    """Binary data
-
-    The 'data' attribute contains the raw byte string.
-    """
-
-    _framespec = [BinaryDataSpec('data')]
-
-    def __eq__(self, other):
-        return self.data == other
-
-    __hash__ = Frame.__hash__
-
-
-class MCDI(BinaryFrame):
-    "Binary dump of CD's TOC"
-
-
-class ETCO(Frame):
-    """Event timing codes."""
-
-    _framespec = [
-        ByteSpec("format"),
-        KeyEventSpec("events"),
-    ]
-
-    def __eq__(self, other):
-        return self.events == other
-
-    __hash__ = Frame.__hash__
-
-
-class MLLT(Frame):
-    """MPEG location lookup table.
-
-    This frame's attributes may be changed in the future based on
-    feedback from real-world use.
-    """
-
-    _framespec = [
-        SizedIntegerSpec('frames', 2),
-        SizedIntegerSpec('bytes', 3),
-        SizedIntegerSpec('milliseconds', 3),
-        ByteSpec('bits_for_bytes'),
-        ByteSpec('bits_for_milliseconds'),
-        BinaryDataSpec('data'),
-    ]
-
-    def __eq__(self, other):
-        return self.data == other
-
-    __hash__ = Frame.__hash__
-
-
-class SYTC(Frame):
-    """Synchronised tempo codes.
-
-    This frame's attributes may be changed in the future based on
-    feedback from real-world use.
-    """
-
-    _framespec = [
-        ByteSpec("format"),
-        BinaryDataSpec("data"),
-    ]
-
-    def __eq__(self, other):
-        return self.data == other
-
-    __hash__ = Frame.__hash__
-
-
-@swap_to_string
-class USLT(Frame):
-    """Unsynchronised lyrics/text transcription.
-
-    Lyrics have a three letter ISO language code ('lang'), a
-    description ('desc'), and a block of plain text ('text').
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        StringSpec('lang', 3),
-        EncodedTextSpec('desc'),
-        EncodedTextSpec('text'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
-
-    def __bytes__(self):
-        return self.text.encode('utf-8')
-
-    def __str__(self):
-        return self.text
-
-    def __eq__(self, other):
-        return self.text == other
-
-    __hash__ = Frame.__hash__
-
-
-@swap_to_string
-class SYLT(Frame):
-    """Synchronised lyrics/text."""
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        StringSpec('lang', 3),
-        ByteSpec('format'),
-        ByteSpec('type'),
-        EncodedTextSpec('desc'),
-        SynchronizedTextSpec('text'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
-
-    def __eq__(self, other):
-        return str(self) == other
-
-    __hash__ = Frame.__hash__
-
-    def __str__(self):
-        return u"".join(text for (text, time) in self.text)
-
-    def __bytes__(self):
-        return text_type(self).encode("utf-8")
-
-
-class COMM(TextFrame):
-    """User comment.
-
-    User comment frames have a descrption, like TXXX, and also a three
-    letter ISO language code in the 'lang' attribute.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        StringSpec('lang', 3),
-        EncodedTextSpec('desc'),
-        MultiSpec('text', EncodedTextSpec('text'), sep=u'\u0000'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s:%s' % (self.FrameID, self.desc, self.lang)
-
-    def _pprint(self):
-        return "%s=%s=%s" % (self.desc, self.lang, " / ".join(self.text))
-
-
-class RVA2(Frame):
-    """Relative volume adjustment (2).
-
-    This frame is used to implemented volume scaling, and in
-    particular, normalization using ReplayGain.
-
-    Attributes:
-
-    * desc -- description or context of this adjustment
-    * channel -- audio channel to adjust (master is 1)
-    * gain -- a + or - dB gain relative to some reference level
-    * peak -- peak of the audio as a floating point number, [0, 1]
-
-    When storing ReplayGain tags, use descriptions of 'album' and
-    'track' on channel 1.
-    """
-
-    _framespec = [
-        Latin1TextSpec('desc'),
-        ChannelSpec('channel'),
-        VolumeAdjustmentSpec('gain'),
-        VolumePeakSpec('peak'),
-    ]
-
-    _channels = ["Other", "Master volume", "Front right", "Front left",
-                 "Back right", "Back left", "Front centre", "Back centre",
-                 "Subwoofer"]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.desc)
-
-    def __eq__(self, other):
-        try:
-            return ((str(self) == other) or
-                    (self.desc == other.desc and
-                     self.channel == other.channel and
-                     self.gain == other.gain and
-                     self.peak == other.peak))
-        except AttributeError:
-            return False
-
-    __hash__ = Frame.__hash__
-
-    def __str__(self):
-        return "%s: %+0.4f dB/%0.4f" % (
-            self._channels[self.channel], self.gain, self.peak)
-
-
-class EQU2(Frame):
-    """Equalisation (2).
-
-    Attributes:
-    method -- interpolation method (0 = band, 1 = linear)
-    desc -- identifying description
-    adjustments -- list of (frequency, vol_adjustment) pairs
-    """
-
-    _framespec = [
-        ByteSpec("method"),
-        Latin1TextSpec("desc"),
-        VolumeAdjustmentsSpec("adjustments"),
-    ]
-
-    def __eq__(self, other):
-        return self.adjustments == other
-
-    __hash__ = Frame.__hash__
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.desc)
-
-
-# class RVAD: unsupported
-# class EQUA: unsupported
-
-
-class RVRB(Frame):
-    """Reverb."""
-
-    _framespec = [
-        SizedIntegerSpec('left', 2),
-        SizedIntegerSpec('right', 2),
-        ByteSpec('bounce_left'),
-        ByteSpec('bounce_right'),
-        ByteSpec('feedback_ltl'),
-        ByteSpec('feedback_ltr'),
-        ByteSpec('feedback_rtr'),
-        ByteSpec('feedback_rtl'),
-        ByteSpec('premix_ltr'),
-        ByteSpec('premix_rtl'),
-    ]
-
-    def __eq__(self, other):
-        return (self.left, self.right) == other
-
-    __hash__ = Frame.__hash__
-
-
-class APIC(Frame):
-    """Attached (or linked) Picture.
-
-    Attributes:
-
-    * encoding -- text encoding for the description
-    * mime -- a MIME type (e.g. image/jpeg) or '-->' if the data is a URI
-    * type -- the source of the image (3 is the album front cover)
-    * desc -- a text description of the image
-    * data -- raw image data, as a byte string
-
-    Mutagen will automatically compress large images when saving tags.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        Latin1TextSpec('mime'),
-        ByteSpec('type'),
-        EncodedTextSpec('desc'),
-        BinaryDataSpec('data'),
-    ]
-
-    def __eq__(self, other):
-        return self.data == other
-
-    __hash__ = Frame.__hash__
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.desc)
-
-    def _validate_from_22(self, other, checker):
-        if checker.name == "mime":
-            self.mime = other.mime.decode("ascii", "ignore")
-        else:
-            super(APIC, self)._validate_from_22(other, checker)
-
-    def _pprint(self):
-        return "%s (%s, %d bytes)" % (
-            self.desc, self.mime, len(self.data))
-
-
-class PCNT(Frame):
-    """Play counter.
-
-    The 'count' attribute contains the (recorded) number of times this
-    file has been played.
-
-    This frame is basically obsoleted by POPM.
-    """
-
-    _framespec = [IntegerSpec('count')]
-
-    def __eq__(self, other):
-        return self.count == other
-
-    __hash__ = Frame.__hash__
-
-    def __pos__(self):
-        return self.count
-
-    def _pprint(self):
-        return text_type(self.count)
-
-
-class POPM(FrameOpt):
-    """Popularimeter.
-
-    This frame keys a rating (out of 255) and a play count to an email
-    address.
-
-    Attributes:
-
-    * email -- email this POPM frame is for
-    * rating -- rating from 0 to 255
-    * count -- number of times the files has been played (optional)
-    """
-
-    _framespec = [
-        Latin1TextSpec('email'),
-        ByteSpec('rating'),
-    ]
-
-    _optionalspec = [IntegerSpec('count')]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.email)
-
-    def __eq__(self, other):
-        return self.rating == other
-
-    __hash__ = FrameOpt.__hash__
-
-    def __pos__(self):
-        return self.rating
-
-    def _pprint(self):
-        return "%s=%r %r/255" % (
-            self.email, getattr(self, 'count', None), self.rating)
-
-
-class GEOB(Frame):
-    """General Encapsulated Object.
-
-    A blob of binary data, that is not a picture (those go in APIC).
-
-    Attributes:
-
-    * encoding -- encoding of the description
-    * mime -- MIME type of the data or '-->' if the data is a URI
-    * filename -- suggested filename if extracted
-    * desc -- text description of the data
-    * data -- raw data, as a byte string
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        Latin1TextSpec('mime'),
-        EncodedTextSpec('filename'),
-        EncodedTextSpec('desc'),
-        BinaryDataSpec('data'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.desc)
-
-    def __eq__(self, other):
-        return self.data == other
-
-    __hash__ = Frame.__hash__
-
-
-class RBUF(FrameOpt):
-    """Recommended buffer size.
-
-    Attributes:
-
-    * size -- recommended buffer size in bytes
-    * info -- if ID3 tags may be elsewhere in the file (optional)
-    * offset -- the location of the next ID3 tag, if any
-
-    Mutagen will not find the next tag itself.
-    """
-
-    _framespec = [SizedIntegerSpec('size', 3)]
-
-    _optionalspec = [
-        ByteSpec('info'),
-        SizedIntegerSpec('offset', 4),
-    ]
-
-    def __eq__(self, other):
-        return self.size == other
-
-    __hash__ = FrameOpt.__hash__
-
-    def __pos__(self):
-        return self.size
-
-
-@swap_to_string
-class AENC(FrameOpt):
-    """Audio encryption.
-
-    Attributes:
-
-    * owner -- key identifying this encryption type
-    * preview_start -- unencrypted data block offset
-    * preview_length -- number of unencrypted blocks
-    * data -- data required for decryption (optional)
-
-    Mutagen cannot decrypt files.
-    """
-
-    _framespec = [
-        Latin1TextSpec('owner'),
-        SizedIntegerSpec('preview_start', 2),
-        SizedIntegerSpec('preview_length', 2),
-    ]
-
-    _optionalspec = [BinaryDataSpec('data')]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.owner)
-
-    def __bytes__(self):
-        return self.owner.encode('utf-8')
-
-    def __str__(self):
-        return self.owner
-
-    def __eq__(self, other):
-        return self.owner == other
-
-    __hash__ = FrameOpt.__hash__
-
-
-class LINK(FrameOpt):
-    """Linked information.
-
-    Attributes:
-
-    * frameid -- the ID of the linked frame
-    * url -- the location of the linked frame
-    * data -- further ID information for the frame
-    """
-
-    _framespec = [
-        StringSpec('frameid', 4),
-        Latin1TextSpec('url'),
-    ]
-
-    _optionalspec = [BinaryDataSpec('data')]
-
-    @property
-    def HashKey(self):
-        try:
-            return "%s:%s:%s:%s" % (
-                self.FrameID, self.frameid, self.url, _bytes2key(self.data))
-        except AttributeError:
-            return "%s:%s:%s" % (self.FrameID, self.frameid, self.url)
-
-    def __eq__(self, other):
-        try:
-            return (self.frameid, self.url, self.data) == other
-        except AttributeError:
-            return (self.frameid, self.url) == other
-
-    __hash__ = FrameOpt.__hash__
-
-
-class POSS(Frame):
-    """Position synchronisation frame
-
-    Attribute:
-
-    * format -- format of the position attribute (frames or milliseconds)
-    * position -- current position of the file
-    """
-
-    _framespec = [
-        ByteSpec('format'),
-        IntegerSpec('position'),
-    ]
-
-    def __pos__(self):
-        return self.position
-
-    def __eq__(self, other):
-        return self.position == other
-
-    __hash__ = Frame.__hash__
-
-
-class UFID(Frame):
-    """Unique file identifier.
-
-    Attributes:
-
-    * owner -- format/type of identifier
-    * data -- identifier
-    """
-
-    _framespec = [
-        Latin1TextSpec('owner'),
-        BinaryDataSpec('data'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.owner)
-
-    def __eq__(s, o):
-        if isinstance(o, UFI):
-            return s.owner == o.owner and s.data == o.data
-        else:
-            return s.data == o
-
-    __hash__ = Frame.__hash__
-
-    def _pprint(self):
-        return "%s=%r" % (self.owner, self.data)
-
-
-@swap_to_string
-class USER(Frame):
-    """Terms of use.
-
-    Attributes:
-
-    * encoding -- text encoding
-    * lang -- ISO three letter language code
-    * text -- licensing terms for the audio
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        StringSpec('lang', 3),
-        EncodedTextSpec('text'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.lang)
-
-    def __bytes__(self):
-        return self.text.encode('utf-8')
-
-    def __str__(self):
-        return self.text
-
-    def __eq__(self, other):
-        return self.text == other
-
-    __hash__ = Frame.__hash__
-
-    def _pprint(self):
-        return "%r=%s" % (self.lang, self.text)
-
-
-@swap_to_string
-class OWNE(Frame):
-    """Ownership frame."""
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        Latin1TextSpec('price'),
-        StringSpec('date', 8),
-        EncodedTextSpec('seller'),
-    ]
-
-    def __bytes__(self):
-        return self.seller.encode('utf-8')
-
-    def __str__(self):
-        return self.seller
-
-    def __eq__(self, other):
-        return self.seller == other
-
-    __hash__ = Frame.__hash__
-
-
-class COMR(FrameOpt):
-    """Commercial frame."""
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        Latin1TextSpec('price'),
-        StringSpec('valid_until', 8),
-        Latin1TextSpec('contact'),
-        ByteSpec('format'),
-        EncodedTextSpec('seller'),
-        EncodedTextSpec('desc'),
-    ]
-
-    _optionalspec = [
-        Latin1TextSpec('mime'),
-        BinaryDataSpec('logo'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, _bytes2key(self._writeData()))
-
-    def __eq__(self, other):
-        return self._writeData() == other._writeData()
-
-    __hash__ = FrameOpt.__hash__
-
-
-@swap_to_string
-class ENCR(Frame):
-    """Encryption method registration.
-
-    The standard does not allow multiple ENCR frames with the same owner
-    or the same method. Mutagen only verifies that the owner is unique.
-    """
-
-    _framespec = [
-        Latin1TextSpec('owner'),
-        ByteSpec('method'),
-        BinaryDataSpec('data'),
-    ]
-
-    @property
-    def HashKey(self):
-        return "%s:%s" % (self.FrameID, self.owner)
-
-    def __bytes__(self):
-        return self.data
-
-    def __eq__(self, other):
-        return self.data == other
-
-    __hash__ = Frame.__hash__
-
-
-@swap_to_string
-class GRID(FrameOpt):
-    """Group identification registration."""
-
-    _framespec = [
-        Latin1TextSpec('owner'),
-        ByteSpec('group'),
-    ]
-
-    _optionalspec = [BinaryDataSpec('data')]
-
-    @property
-    def HashKey(self):
-        return '%s:%s' % (self.FrameID, self.group)
-
-    def __pos__(self):
-        return self.group
-
-    def __bytes__(self):
-        return self.owner.encode('utf-8')
-
-    def __str__(self):
-        return self.owner
-
-    def __eq__(self, other):
-        return self.owner == other or self.group == other
-
-    __hash__ = FrameOpt.__hash__
-
-
-@swap_to_string
-class PRIV(Frame):
-    """Private frame."""
-
-    _framespec = [
-        Latin1TextSpec('owner'),
-        BinaryDataSpec('data'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s:%s' % (
-            self.FrameID, self.owner, _bytes2key(self.data))
-
-    def __bytes__(self):
-        return self.data
-
-    def __eq__(self, other):
-        return self.data == other
-
-    def _pprint(self):
-        return "%s=%r" % (self.owner, self.data)
-
-    __hash__ = Frame.__hash__
-
-
-@swap_to_string
-class SIGN(Frame):
-    """Signature frame."""
-
-    _framespec = [
-        ByteSpec('group'),
-        BinaryDataSpec('sig'),
-    ]
-
-    @property
-    def HashKey(self):
-        return '%s:%s:%s' % (self.FrameID, self.group, _bytes2key(self.sig))
-
-    def __bytes__(self):
-        return self.sig
-
-    def __eq__(self, other):
-        return self.sig == other
-
-    __hash__ = Frame.__hash__
-
-
-class SEEK(Frame):
-    """Seek frame.
-
-    Mutagen does not find tags at seek offsets.
-    """
-
-    _framespec = [IntegerSpec('offset')]
-
-    def __pos__(self):
-        return self.offset
-
-    def __eq__(self, other):
-        return self.offset == other
-
-    __hash__ = Frame.__hash__
-
-
-class ASPI(Frame):
-    """Audio seek point index.
-
-    Attributes: S, L, N, b, and Fi. For the meaning of these, see
-    the ID3v2.4 specification. Fi is a list of integers.
-    """
-    _framespec = [
-        SizedIntegerSpec("S", 4),
-        SizedIntegerSpec("L", 4),
-        SizedIntegerSpec("N", 2),
-        ByteSpec("b"),
-        ASPIIndexSpec("Fi"),
-    ]
-
-    def __eq__(self, other):
-        return self.Fi == other
-
-    __hash__ = Frame.__hash__
-
-
-# ID3v2.2 frames
-class UFI(UFID):
-    "Unique File Identifier"
-
-
-class TT1(TIT1):
-    "Content group description"
-
-
-class TT2(TIT2):
-    "Title"
-
-
-class TT3(TIT3):
-    "Subtitle/Description refinement"
-
-
-class TP1(TPE1):
-    "Lead Artist/Performer/Soloist/Group"
-
-
-class TP2(TPE2):
-    "Band/Orchestra/Accompaniment"
-
-
-class TP3(TPE3):
-    "Conductor"
-
-
-class TP4(TPE4):
-    "Interpreter/Remixer/Modifier"
-
-
-class TCM(TCOM):
-    "Composer"
-
-
-class TXT(TEXT):
-    "Lyricist"
-
-
-class TLA(TLAN):
-    "Audio Language(s)"
-
-
-class TCO(TCON):
-    "Content Type (Genre)"
-
-
-class TAL(TALB):
-    "Album"
-
-
-class TPA(TPOS):
-    "Part of set"
-
-
-class TRK(TRCK):
-    "Track Number"
-
-
-class TRC(TSRC):
-    "International Standard Recording Code (ISRC)"
-
-
-class TYE(TYER):
-    "Year of recording"
-
-
-class TDA(TDAT):
-    "Date of recording (DDMM)"
-
-
-class TIM(TIME):
-    "Time of recording (HHMM)"
-
-
-class TRD(TRDA):
-    "Recording Dates"
-
-
-class TMT(TMED):
-    "Source Media Type"
-
-
-class TFT(TFLT):
-    "File Type"
-
-
-class TBP(TBPM):
-    "Beats per minute"
-
-
-class TCP(TCMP):
-    "iTunes Compilation Flag"
-
-
-class TCR(TCOP):
-    "Copyright (C)"
-
-
-class TPB(TPUB):
-    "Publisher"
-
-
-class TEN(TENC):
-    "Encoder"
-
-
-class TSS(TSSE):
-    "Encoder settings"
-
-
-class TOF(TOFN):
-    "Original Filename"
-
-
-class TLE(TLEN):
-    "Audio Length (ms)"
-
-
-class TSI(TSIZ):
-    "Audio Data size (bytes)"
-
-
-class TDY(TDLY):
-    "Audio Delay (ms)"
-
-
-class TKE(TKEY):
-    "Starting Key"
-
-
-class TOT(TOAL):
-    "Original Album"
-
-
-class TOA(TOPE):
-    "Original Artist/Perfomer"
-
-
-class TOL(TOLY):
-    "Original Lyricist"
-
-
-class TOR(TORY):
-    "Original Release Year"
-
-
-class TXX(TXXX):
-    "User-defined Text"
-
-
-class WAF(WOAF):
-    "Official File Information"
-
-
-class WAR(WOAR):
-    "Official Artist/Performer Information"
-
-
-class WAS(WOAS):
-    "Official Source Information"
-
-
-class WCM(WCOM):
-    "Commercial Information"
-
-
-class WCP(WCOP):
-    "Copyright Information"
-
-
-class WPB(WPUB):
-    "Official Publisher Information"
-
-
-class WXX(WXXX):
-    "User-defined URL"
-
-
-class IPL(IPLS):
-    "Involved people list"
-
-
-class MCI(MCDI):
-    "Binary dump of CD's TOC"
-
-
-class ETC(ETCO):
-    "Event timing codes"
-
-
-class MLL(MLLT):
-    "MPEG location lookup table"
-
-
-class STC(SYTC):
-    "Synced tempo codes"
-
-
-class ULT(USLT):
-    "Unsychronised lyrics/text transcription"
-
-
-class SLT(SYLT):
-    "Synchronised lyrics/text"
-
-
-class COM(COMM):
-    "Comment"
-
-
-# class RVA(RVAD)
-# class EQU(EQUA)
-
-
-class REV(RVRB):
-    "Reverb"
-
-
-class PIC(APIC):
-    """Attached Picture.
-
-    The 'mime' attribute of an ID3v2.2 attached picture must be either
-    'PNG' or 'JPG'.
-    """
-
-    _framespec = [
-        EncodingSpec('encoding'),
-        StringSpec('mime', 3),
-        ByteSpec('type'),
-        EncodedTextSpec('desc'),
-        BinaryDataSpec('data')
-    ]
-
-    def _to_other(self, other):
-        if not isinstance(other, APIC):
-            raise TypeError
-
-        other.encoding = self.encoding
-        other.mime = self.mime
-        other.type = self.type
-        other.desc = self.desc
-        other.data = self.data
-
-
-class GEO(GEOB):
-    "General Encapsulated Object"
-
-
-class CNT(PCNT):
-    "Play counter"
-
-
-class POP(POPM):
-    "Popularimeter"
-
-
-class BUF(RBUF):
-    "Recommended buffer size"
-
-
-class CRM(Frame):
-    """Encrypted meta frame"""
-    _framespec = [Latin1TextSpec('owner'), Latin1TextSpec('desc'),
-                  BinaryDataSpec('data')]
-
-    def __eq__(self, other):
-        return self.data == other
-    __hash__ = Frame.__hash__
-
-
-class CRA(AENC):
-    "Audio encryption"
-
-
-class LNK(LINK):
-    """Linked information"""
-
-    _framespec = [
-        StringSpec('frameid', 3),
-        Latin1TextSpec('url')
-    ]
-
-    _optionalspec = [BinaryDataSpec('data')]
-
-    def _to_other(self, other):
-        if not isinstance(other, LINK):
-            raise TypeError
-
-        other.frameid = self.frameid
-        other.url = self.url
-        if hasattr(self, "data"):
-            other.data = self.data
-
-
-Frames = {}
-"""All supported ID3v2.3/4 frames, keyed by frame name."""
-
-
-Frames_2_2 = {}
-"""All supported ID3v2.2 frames, keyed by frame name."""
-
-
-k, v = None, None
-for k, v in iteritems(globals()):
-    if isinstance(v, type) and issubclass(v, Frame):
-        v.__module__ = "mutagen.id3"
-
-        if len(k) == 3:
-            Frames_2_2[k] = v
-        elif len(k) == 4:
-            Frames[k] = v
-
-try:
-    del k
-    del v
-except NameError:
-    pass
diff --git a/resources/lib/libraries/mutagen/id3/_specs.py b/resources/lib/libraries/mutagen/id3/_specs.py
deleted file mode 100644
index 4358a65d..00000000
--- a/resources/lib/libraries/mutagen/id3/_specs.py
+++ /dev/null
@@ -1,635 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Michael Urman
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-import struct
-from struct import unpack, pack
-
-from .._compat import text_type, chr_, PY3, swap_to_string, string_types, \
-    xrange
-from .._util import total_ordering, decode_terminated, enum, izip
-from ._util import BitPaddedInt
-
-
-@enum
-class PictureType(object):
-    """Enumeration of image types defined by the ID3 standard for the APIC
-    frame, but also reused in WMA/FLAC/VorbisComment.
-    """
-
-    OTHER = 0
-    """Other"""
-
-    FILE_ICON = 1
-    """32x32 pixels 'file icon' (PNG only)"""
-
-    OTHER_FILE_ICON = 2
-    """Other file icon"""
-
-    COVER_FRONT = 3
-    """Cover (front)"""
-
-    COVER_BACK = 4
-    """Cover (back)"""
-
-    LEAFLET_PAGE = 5
-    """Leaflet page"""
-
-    MEDIA = 6
-    """Media (e.g. label side of CD)"""
-
-    LEAD_ARTIST = 7
-    """Lead artist/lead performer/soloist"""
-
-    ARTIST = 8
-    """Artist/performer"""
-
-    CONDUCTOR = 9
-    """Conductor"""
-
-    BAND = 10
-    """Band/Orchestra"""
-
-    COMPOSER = 11
-    """Composer"""
-
-    LYRICIST = 12
-    """Lyricist/text writer"""
-
-    RECORDING_LOCATION = 13
-    """Recording Location"""
-
-    DURING_RECORDING = 14
-    """During recording"""
-
-    DURING_PERFORMANCE = 15
-    """During performance"""
-
-    SCREEN_CAPTURE = 16
-    """Movie/video screen capture"""
-
-    FISH = 17
-    """A bright coloured fish"""
-
-    ILLUSTRATION = 18
-    """Illustration"""
-
-    BAND_LOGOTYPE = 19
-    """Band/artist logotype"""
-
-    PUBLISHER_LOGOTYPE = 20
-    """Publisher/Studio logotype"""
-
-
-class SpecError(Exception):
-    pass
-
-
-class Spec(object):
-
-    def __init__(self, name):
-        self.name = name
-
-    def __hash__(self):
-        raise TypeError("Spec objects are unhashable")
-
-    def _validate23(self, frame, value, **kwargs):
-        """Return a possibly modified value which, if written,
-        results in valid id3v2.3 data.
-        """
-
-        return value
-
-    def read(self, frame, data):
-        """Returns the (value, left_data) or raises SpecError"""
-
-        raise NotImplementedError
-
-    def write(self, frame, value):
-        raise NotImplementedError
-
-    def validate(self, frame, value):
-        """Returns the validated data or raises ValueError/TypeError"""
-
-        raise NotImplementedError
-
-
-class ByteSpec(Spec):
-    def read(self, frame, data):
-        return bytearray(data)[0], data[1:]
-
-    def write(self, frame, value):
-        return chr_(value)
-
-    def validate(self, frame, value):
-        if value is not None:
-            chr_(value)
-        return value
-
-
-class IntegerSpec(Spec):
-    def read(self, frame, data):
-        return int(BitPaddedInt(data, bits=8)), b''
-
-    def write(self, frame, value):
-        return BitPaddedInt.to_str(value, bits=8, width=-1)
-
-    def validate(self, frame, value):
-        return value
-
-
-class SizedIntegerSpec(Spec):
-    def __init__(self, name, size):
-        self.name, self.__sz = name, size
-
-    def read(self, frame, data):
-        return int(BitPaddedInt(data[:self.__sz], bits=8)), data[self.__sz:]
-
-    def write(self, frame, value):
-        return BitPaddedInt.to_str(value, bits=8, width=self.__sz)
-
-    def validate(self, frame, value):
-        return value
-
-
-@enum
-class Encoding(object):
-    """Text Encoding"""
-
-    LATIN1 = 0
-    """ISO-8859-1"""
-
-    UTF16 = 1
-    """UTF-16 with BOM"""
-
-    UTF16BE = 2
-    """UTF-16BE without BOM"""
-
-    UTF8 = 3
-    """UTF-8"""
-
-
-class EncodingSpec(ByteSpec):
-
-    def read(self, frame, data):
-        enc, data = super(EncodingSpec, self).read(frame, data)
-        if enc not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE,
-                       Encoding.UTF8):
-            raise SpecError('Invalid Encoding: %r' % enc)
-        return enc, data
-
-    def validate(self, frame, value):
-        if value is None:
-            return None
-        if value not in (Encoding.LATIN1, Encoding.UTF16, Encoding.UTF16BE,
-                         Encoding.UTF8):
-            raise ValueError('Invalid Encoding: %r' % value)
-        return value
-
-    def _validate23(self, frame, value, **kwargs):
-        # only 0, 1 are valid in v2.3, default to utf-16
-        if value not in (Encoding.LATIN1, Encoding.UTF16):
-            value = Encoding.UTF16
-        return value
-
-
-class StringSpec(Spec):
-    """A fixed size ASCII only payload."""
-
-    def __init__(self, name, length):
-        super(StringSpec, self).__init__(name)
-        self.len = length
-
-    def read(s, frame, data):
-        chunk = data[:s.len]
-        try:
-            ascii = chunk.decode("ascii")
-        except UnicodeDecodeError:
-            raise SpecError("not ascii")
-        else:
-            if PY3:
-                chunk = ascii
-
-        return chunk, data[s.len:]
-
-    def write(s, frame, value):
-        if value is None:
-            return b'\x00' * s.len
-        else:
-            if PY3:
-                value = value.encode("ascii")
-            return (bytes(value) + b'\x00' * s.len)[:s.len]
-
-    def validate(s, frame, value):
-        if value is None:
-            return None
-
-        if PY3:
-            if not isinstance(value, str):
-                raise TypeError("%s has to be str" % s.name)
-            value.encode("ascii")
-        else:
-            if not isinstance(value, bytes):
-                value = value.encode("ascii")
-
-        if len(value) == s.len:
-            return value
-
-        raise ValueError('Invalid StringSpec[%d] data: %r' % (s.len, value))
-
-
-class BinaryDataSpec(Spec):
-    def read(self, frame, data):
-        return data, b''
-
-    def write(self, frame, value):
-        if value is None:
-            return b""
-        if isinstance(value, bytes):
-            return value
-        value = text_type(value).encode("ascii")
-        return value
-
-    def validate(self, frame, value):
-        if value is None:
-            return None
-
-        if isinstance(value, bytes):
-            return value
-        elif PY3:
-            raise TypeError("%s has to be bytes" % self.name)
-
-        value = text_type(value).encode("ascii")
-        return value
-
-
-class EncodedTextSpec(Spec):
-
-    _encodings = {
-        Encoding.LATIN1: ('latin1', b'\x00'),
-        Encoding.UTF16: ('utf16', b'\x00\x00'),
-        Encoding.UTF16BE: ('utf_16_be', b'\x00\x00'),
-        Encoding.UTF8: ('utf8', b'\x00'),
-    }
-
-    def read(self, frame, data):
-        enc, term = self._encodings[frame.encoding]
-        try:
-            # allow missing termination
-            return decode_terminated(data, enc, strict=False)
-        except ValueError:
-            # utf-16 termination with missing BOM, or single NULL
-            if not data[:len(term)].strip(b"\x00"):
-                return u"", data[len(term):]
-
-            # utf-16 data with single NULL, see issue 169
-            try:
-                return decode_terminated(data + b"\x00", enc)
-            except ValueError:
-                raise SpecError("Decoding error")
-
-    def write(self, frame, value):
-        enc, term = self._encodings[frame.encoding]
-        return value.encode(enc) + term
-
-    def validate(self, frame, value):
-        return text_type(value)
-
-
-class MultiSpec(Spec):
-    def __init__(self, name, *specs, **kw):
-        super(MultiSpec, self).__init__(name)
-        self.specs = specs
-        self.sep = kw.get('sep')
-
-    def read(self, frame, data):
-        values = []
-        while data:
-            record = []
-            for spec in self.specs:
-                value, data = spec.read(frame, data)
-                record.append(value)
-            if len(self.specs) != 1:
-                values.append(record)
-            else:
-                values.append(record[0])
-        return values, data
-
-    def write(self, frame, value):
-        data = []
-        if len(self.specs) == 1:
-            for v in value:
-                data.append(self.specs[0].write(frame, v))
-        else:
-            for record in value:
-                for v, s in izip(record, self.specs):
-                    data.append(s.write(frame, v))
-        return b''.join(data)
-
-    def validate(self, frame, value):
-        if value is None:
-            return []
-        if self.sep and isinstance(value, string_types):
-            value = value.split(self.sep)
-        if isinstance(value, list):
-            if len(self.specs) == 1:
-                return [self.specs[0].validate(frame, v) for v in value]
-            else:
-                return [
-                    [s.validate(frame, v) for (v, s) in izip(val, self.specs)]
-                    for val in value]
-        raise ValueError('Invalid MultiSpec data: %r' % value)
-
-    def _validate23(self, frame, value, **kwargs):
-        if len(self.specs) != 1:
-            return [[s._validate23(frame, v, **kwargs)
-                     for (v, s) in izip(val, self.specs)]
-                    for val in value]
-
-        spec = self.specs[0]
-
-        # Merge single text spec multispecs only.
-        # (TimeStampSpec beeing the exception, but it's not a valid v2.3 frame)
-        if not isinstance(spec, EncodedTextSpec) or \
-                isinstance(spec, TimeStampSpec):
-            return value
-
-        value = [spec._validate23(frame, v, **kwargs) for v in value]
-        if kwargs.get("sep") is not None:
-            return [spec.validate(frame, kwargs["sep"].join(value))]
-        return value
-
-
-class EncodedNumericTextSpec(EncodedTextSpec):
-    pass
-
-
-class EncodedNumericPartTextSpec(EncodedTextSpec):
-    pass
-
-
-class Latin1TextSpec(EncodedTextSpec):
-    def read(self, frame, data):
-        if b'\x00' in data:
-            data, ret = data.split(b'\x00', 1)
-        else:
-            ret = b''
-        return data.decode('latin1'), ret
-
-    def write(self, data, value):
-        return value.encode('latin1') + b'\x00'
-
-    def validate(self, frame, value):
-        return text_type(value)
-
-
-@swap_to_string
-@total_ordering
-class ID3TimeStamp(object):
-    """A time stamp in ID3v2 format.
-
-    This is a restricted form of the ISO 8601 standard; time stamps
-    take the form of:
-        YYYY-MM-DD HH:MM:SS
-    Or some partial form (YYYY-MM-DD HH, YYYY, etc.).
-
-    The 'text' attribute contains the raw text data of the time stamp.
-    """
-
-    import re
-
-    def __init__(self, text):
-        if isinstance(text, ID3TimeStamp):
-            text = text.text
-        elif not isinstance(text, text_type):
-            if PY3:
-                raise TypeError("not a str")
-            text = text.decode("utf-8")
-
-        self.text = text
-
-    __formats = ['%04d'] + ['%02d'] * 5
-    __seps = ['-', '-', ' ', ':', ':', 'x']
-
-    def get_text(self):
-        parts = [self.year, self.month, self.day,
-                 self.hour, self.minute, self.second]
-        pieces = []
-        for i, part in enumerate(parts):
-            if part is None:
-                break
-            pieces.append(self.__formats[i] % part + self.__seps[i])
-        return u''.join(pieces)[:-1]
-
-    def set_text(self, text, splitre=re.compile('[-T:/.]|\s+')):
-        year, month, day, hour, minute, second = \
-            splitre.split(text + ':::::')[:6]
-        for a in 'year month day hour minute second'.split():
-            try:
-                v = int(locals()[a])
-            except ValueError:
-                v = None
-            setattr(self, a, v)
-
-    text = property(get_text, set_text, doc="ID3v2.4 date and time.")
-
-    def __str__(self):
-        return self.text
-
-    def __bytes__(self):
-        return self.text.encode("utf-8")
-
-    def __repr__(self):
-        return repr(self.text)
-
-    def __eq__(self, other):
-        return self.text == other.text
-
-    def __lt__(self, other):
-        return self.text < other.text
-
-    __hash__ = object.__hash__
-
-    def encode(self, *args):
-        return self.text.encode(*args)
-
-
-class TimeStampSpec(EncodedTextSpec):
-    def read(self, frame, data):
-        value, data = super(TimeStampSpec, self).read(frame, data)
-        return self.validate(frame, value), data
-
-    def write(self, frame, data):
-        return super(TimeStampSpec, self).write(frame,
-                                                data.text.replace(' ', 'T'))
-
-    def validate(self, frame, value):
-        try:
-            return ID3TimeStamp(value)
-        except TypeError:
-            raise ValueError("Invalid ID3TimeStamp: %r" % value)
-
-
-class ChannelSpec(ByteSpec):
-    (OTHER, MASTER, FRONTRIGHT, FRONTLEFT, BACKRIGHT, BACKLEFT, FRONTCENTRE,
-     BACKCENTRE, SUBWOOFER) = xrange(9)
-
-
-class VolumeAdjustmentSpec(Spec):
-    def read(self, frame, data):
-        value, = unpack('>h', data[0:2])
-        return value / 512.0, data[2:]
-
-    def write(self, frame, value):
-        number = int(round(value * 512))
-        # pack only fails in 2.7, do it manually in 2.6
-        if not -32768 <= number <= 32767:
-            raise SpecError("not in range")
-        return pack('>h', number)
-
-    def validate(self, frame, value):
-        if value is not None:
-            try:
-                self.write(frame, value)
-            except SpecError:
-                raise ValueError("out of range")
-        return value
-
-
-class VolumePeakSpec(Spec):
-    def read(self, frame, data):
-        # http://bugs.xmms.org/attachment.cgi?id=113&action=view
-        peak = 0
-        data_array = bytearray(data)
-        bits = data_array[0]
-        vol_bytes = min(4, (bits + 7) >> 3)
-        # not enough frame data
-        if vol_bytes + 1 > len(data):
-            raise SpecError("not enough frame data")
-        shift = ((8 - (bits & 7)) & 7) + (4 - vol_bytes) * 8
-        for i in xrange(1, vol_bytes + 1):
-            peak *= 256
-            peak += data_array[i]
-        peak *= 2 ** shift
-        return (float(peak) / (2 ** 31 - 1)), data[1 + vol_bytes:]
-
-    def write(self, frame, value):
-        number = int(round(value * 32768))
-        # pack only fails in 2.7, do it manually in 2.6
-        if not 0 <= number <= 65535:
-            raise SpecError("not in range")
-        # always write as 16 bits for sanity.
-        return b"\x10" + pack('>H', number)
-
-    def validate(self, frame, value):
-        if value is not None:
-            try:
-                self.write(frame, value)
-            except SpecError:
-                raise ValueError("out of range")
-        return value
-
-
-class SynchronizedTextSpec(EncodedTextSpec):
-    def read(self, frame, data):
-        texts = []
-        encoding, term = self._encodings[frame.encoding]
-        while data:
-            try:
-                value, data = decode_terminated(data, encoding)
-            except ValueError:
-                raise SpecError("decoding error")
-
-            if len(data) < 4:
-                raise SpecError("not enough data")
-            time, = struct.unpack(">I", data[:4])
-
-            texts.append((value, time))
-            data = data[4:]
-        return texts, b""
-
-    def write(self, frame, value):
-        data = []
-        encoding, term = self._encodings[frame.encoding]
-        for text, time in value:
-            text = text.encode(encoding) + term
-            data.append(text + struct.pack(">I", time))
-        return b"".join(data)
-
-    def validate(self, frame, value):
-        return value
-
-
-class KeyEventSpec(Spec):
-    def read(self, frame, data):
-        events = []
-        while len(data) >= 5:
-            events.append(struct.unpack(">bI", data[:5]))
-            data = data[5:]
-        return events, data
-
-    def write(self, frame, value):
-        return b"".join(struct.pack(">bI", *event) for event in value)
-
-    def validate(self, frame, value):
-        return value
-
-
-class VolumeAdjustmentsSpec(Spec):
-    # Not to be confused with VolumeAdjustmentSpec.
-    def read(self, frame, data):
-        adjustments = {}
-        while len(data) >= 4:
-            freq, adj = struct.unpack(">Hh", data[:4])
-            data = data[4:]
-            freq /= 2.0
-            adj /= 512.0
-            adjustments[freq] = adj
-        adjustments = sorted(adjustments.items())
-        return adjustments, data
-
-    def write(self, frame, value):
-        value.sort()
-        return b"".join(struct.pack(">Hh", int(freq * 2), int(adj * 512))
-                        for (freq, adj) in value)
-
-    def validate(self, frame, value):
-        return value
-
-
-class ASPIIndexSpec(Spec):
-    def read(self, frame, data):
-        if frame.b == 16:
-            format = "H"
-            size = 2
-        elif frame.b == 8:
-            format = "B"
-            size = 1
-        else:
-            raise SpecError("invalid bit count in ASPI (%d)" % frame.b)
-
-        indexes = data[:frame.N * size]
-        data = data[frame.N * size:]
-        try:
-            return list(struct.unpack(">" + format * frame.N, indexes)), data
-        except struct.error as e:
-            raise SpecError(e)
-
-    def write(self, frame, values):
-        if frame.b == 16:
-            format = "H"
-        elif frame.b == 8:
-            format = "B"
-        else:
-            raise SpecError("frame.b must be 8 or 16")
-        try:
-            return struct.pack(">" + format * frame.N, *values)
-        except struct.error as e:
-            raise SpecError(e)
-
-    def validate(self, frame, values):
-        return values
diff --git a/resources/lib/libraries/mutagen/id3/_util.py b/resources/lib/libraries/mutagen/id3/_util.py
deleted file mode 100644
index 29f7241d..00000000
--- a/resources/lib/libraries/mutagen/id3/_util.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2005  Michael Urman
-#               2013  Christoph Reiter
-#               2014  Ben Ockmore
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-from .._compat import long_, integer_types, PY3
-from .._util import MutagenError
-
-
-class error(MutagenError):
-    pass
-
-
-class ID3NoHeaderError(error, ValueError):
-    pass
-
-
-class ID3UnsupportedVersionError(error, NotImplementedError):
-    pass
-
-
-class ID3EncryptionUnsupportedError(error, NotImplementedError):
-    pass
-
-
-class ID3JunkFrameError(error, ValueError):
-    pass
-
-
-class unsynch(object):
-    @staticmethod
-    def decode(value):
-        fragments = bytearray(value).split(b'\xff')
-        if len(fragments) > 1 and not fragments[-1]:
-            raise ValueError('string ended unsafe')
-
-        for f in fragments[1:]:
-            if (not f) or (f[0] >= 0xE0):
-                raise ValueError('invalid sync-safe string')
-
-            if f[0] == 0x00:
-                del f[0]
-
-        return bytes(bytearray(b'\xff').join(fragments))
-
-    @staticmethod
-    def encode(value):
-        fragments = bytearray(value).split(b'\xff')
-        for f in fragments[1:]:
-            if (not f) or (f[0] >= 0xE0) or (f[0] == 0x00):
-                f.insert(0, 0x00)
-        return bytes(bytearray(b'\xff').join(fragments))
-
-
-class _BitPaddedMixin(object):
-
-    def as_str(self, width=4, minwidth=4):
-        return self.to_str(self, self.bits, self.bigendian, width, minwidth)
-
-    @staticmethod
-    def to_str(value, bits=7, bigendian=True, width=4, minwidth=4):
-        mask = (1 << bits) - 1
-
-        if width != -1:
-            index = 0
-            bytes_ = bytearray(width)
-            try:
-                while value:
-                    bytes_[index] = value & mask
-                    value >>= bits
-                    index += 1
-            except IndexError:
-                raise ValueError('Value too wide (>%d bytes)' % width)
-        else:
-            # PCNT and POPM use growing integers
-            # of at least 4 bytes (=minwidth) as counters.
-            bytes_ = bytearray()
-            append = bytes_.append
-            while value:
-                append(value & mask)
-                value >>= bits
-            bytes_ = bytes_.ljust(minwidth, b"\x00")
-
-        if bigendian:
-            bytes_.reverse()
-        return bytes(bytes_)
-
-    @staticmethod
-    def has_valid_padding(value, bits=7):
-        """Whether the padding bits are all zero"""
-
-        assert bits <= 8
-
-        mask = (((1 << (8 - bits)) - 1) << bits)
-
-        if isinstance(value, integer_types):
-            while value:
-                if value & mask:
-                    return False
-                value >>= 8
-        elif isinstance(value, bytes):
-            for byte in bytearray(value):
-                if byte & mask:
-                    return False
-        else:
-            raise TypeError
-
-        return True
-
-
-class BitPaddedInt(int, _BitPaddedMixin):
-
-    def __new__(cls, value, bits=7, bigendian=True):
-
-        mask = (1 << (bits)) - 1
-        numeric_value = 0
-        shift = 0
-
-        if isinstance(value, integer_types):
-            while value:
-                numeric_value += (value & mask) << shift
-                value >>= 8
-                shift += bits
-        elif isinstance(value, bytes):
-            if bigendian:
-                value = reversed(value)
-            for byte in bytearray(value):
-                numeric_value += (byte & mask) << shift
-                shift += bits
-        else:
-            raise TypeError
-
-        if isinstance(numeric_value, int):
-            self = int.__new__(BitPaddedInt, numeric_value)
-        else:
-            self = long_.__new__(BitPaddedLong, numeric_value)
-
-        self.bits = bits
-        self.bigendian = bigendian
-        return self
-
-if PY3:
-    BitPaddedLong = BitPaddedInt
-else:
-    class BitPaddedLong(long_, _BitPaddedMixin):
-        pass
-
-
-class ID3BadUnsynchData(error, ValueError):
-    """Deprecated"""
-
-
-class ID3BadCompressedData(error, ValueError):
-    """Deprecated"""
-
-
-class ID3TagError(error, ValueError):
-    """Deprecated"""
-
-
-class ID3Warning(error, UserWarning):
-    """Deprecated"""
diff --git a/resources/lib/libraries/mutagen/m4a.py b/resources/lib/libraries/mutagen/m4a.py
deleted file mode 100644
index 5730ace3..00000000
--- a/resources/lib/libraries/mutagen/m4a.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright 2006 Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""
-since 1.9: mutagen.m4a is deprecated; use mutagen.mp4 instead.
-since 1.31: mutagen.m4a will no longer work; any operation that could fail
-            will fail now.
-"""
-
-import warnings
-
-from mutagen import FileType, Metadata, StreamInfo
-from ._util import DictProxy, MutagenError
-
-warnings.warn(
-    "mutagen.m4a is deprecated; use mutagen.mp4 instead.",
-    DeprecationWarning)
-
-
-class error(IOError, MutagenError):
-    pass
-
-
-class M4AMetadataError(error):
-    pass
-
-
-class M4AStreamInfoError(error):
-    pass
-
-
-class M4AMetadataValueError(ValueError, M4AMetadataError):
-    pass
-
-
-__all__ = ['M4A', 'Open', 'delete', 'M4ACover']
-
-
-class M4ACover(bytes):
-
-    FORMAT_JPEG = 0x0D
-    FORMAT_PNG = 0x0E
-
-    def __new__(cls, data, imageformat=None):
-        self = bytes.__new__(cls, data)
-        if imageformat is None:
-            imageformat = M4ACover.FORMAT_JPEG
-        self.imageformat = imageformat
-        return self
-
-
-class M4ATags(DictProxy, Metadata):
-
-    def load(self, atoms, fileobj):
-        raise error("deprecated")
-
-    def save(self, filename):
-        raise error("deprecated")
-
-    def delete(self, filename):
-        raise error("deprecated")
-
-    def pprint(self):
-        return u""
-
-
-class M4AInfo(StreamInfo):
-
-    bitrate = 0
-
-    def __init__(self, atoms, fileobj):
-        raise error("deprecated")
-
-    def pprint(self):
-        return u""
-
-
-class M4A(FileType):
-
-    _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
-
-    def load(self, filename):
-        raise error("deprecated")
-
-    def add_tags(self):
-        self.tags = M4ATags()
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return 0
-
-
-Open = M4A
-
-
-def delete(filename):
-    raise error("deprecated")
diff --git a/resources/lib/libraries/mutagen/monkeysaudio.py b/resources/lib/libraries/mutagen/monkeysaudio.py
deleted file mode 100644
index 0e29273f..00000000
--- a/resources/lib/libraries/mutagen/monkeysaudio.py
+++ /dev/null
@@ -1,86 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Lukas Lalinsky
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Monkey's Audio streams with APEv2 tags.
-
-Monkey's Audio is a very efficient lossless audio compressor developed
-by Matt Ashland.
-
-For more information, see http://www.monkeysaudio.com/.
-"""
-
-__all__ = ["MonkeysAudio", "Open", "delete"]
-
-import struct
-
-from ._compat import endswith
-from mutagen import StreamInfo
-from mutagen.apev2 import APEv2File, error, delete
-from mutagen._util import cdata
-
-
-class MonkeysAudioHeaderError(error):
-    pass
-
-
-class MonkeysAudioInfo(StreamInfo):
-    """Monkey's Audio stream information.
-
-    Attributes:
-
-    * channels -- number of audio channels
-    * length -- file length in seconds, as a float
-    * sample_rate -- audio sampling rate in Hz
-    * bits_per_sample -- bits per sample
-    * version -- Monkey's Audio stream version, as a float (eg: 3.99)
-    """
-
-    def __init__(self, fileobj):
-        header = fileobj.read(76)
-        if len(header) != 76 or not header.startswith(b"MAC "):
-            raise MonkeysAudioHeaderError("not a Monkey's Audio file")
-        self.version = cdata.ushort_le(header[4:6])
-        if self.version >= 3980:
-            (blocks_per_frame, final_frame_blocks, total_frames,
-             self.bits_per_sample, self.channels,
-             self.sample_rate) = struct.unpack("<IIIHHI", header[56:76])
-        else:
-            compression_level = cdata.ushort_le(header[6:8])
-            self.channels, self.sample_rate = struct.unpack(
-                "<HI", header[10:16])
-            total_frames, final_frame_blocks = struct.unpack(
-                "<II", header[24:32])
-            if self.version >= 3950:
-                blocks_per_frame = 73728 * 4
-            elif self.version >= 3900 or (self.version >= 3800 and
-                                          compression_level == 4):
-                blocks_per_frame = 73728
-            else:
-                blocks_per_frame = 9216
-        self.version /= 1000.0
-        self.length = 0.0
-        if (self.sample_rate != 0) and (total_frames > 0):
-            total_blocks = ((total_frames - 1) * blocks_per_frame +
-                            final_frame_blocks)
-            self.length = float(total_blocks) / self.sample_rate
-
-    def pprint(self):
-        return u"Monkey's Audio %.2f, %.2f seconds, %d Hz" % (
-            self.version, self.length, self.sample_rate)
-
-
-class MonkeysAudio(APEv2File):
-    _Info = MonkeysAudioInfo
-    _mimes = ["audio/ape", "audio/x-ape"]
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return header.startswith(b"MAC ") + endswith(filename.lower(), ".ape")
-
-
-Open = MonkeysAudio
diff --git a/resources/lib/libraries/mutagen/mp3.py b/resources/lib/libraries/mutagen/mp3.py
deleted file mode 100644
index afb600cf..00000000
--- a/resources/lib/libraries/mutagen/mp3.py
+++ /dev/null
@@ -1,362 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""MPEG audio stream information and tags."""
-
-import os
-import struct
-
-from ._compat import endswith, xrange
-from ._mp3util import XingHeader, XingHeaderError, VBRIHeader, VBRIHeaderError
-from mutagen import StreamInfo
-from mutagen._util import MutagenError, enum
-from mutagen.id3 import ID3FileType, BitPaddedInt, delete
-
-__all__ = ["MP3", "Open", "delete", "MP3"]
-
-
-class error(RuntimeError, MutagenError):
-    pass
-
-
-class HeaderNotFoundError(error, IOError):
-    pass
-
-
-class InvalidMPEGHeader(error, IOError):
-    pass
-
-
-@enum
-class BitrateMode(object):
-
-    UNKNOWN = 0
-    """Probably a CBR file, but not sure"""
-
-    CBR = 1
-    """Constant Bitrate"""
-
-    VBR = 2
-    """Variable Bitrate"""
-
-    ABR = 3
-    """Average Bitrate (a variant of VBR)"""
-
-
-def _guess_xing_bitrate_mode(xing):
-
-    if xing.lame_header:
-        lame = xing.lame_header
-        if lame.vbr_method in (1, 8):
-            return BitrateMode.CBR
-        elif lame.vbr_method in (2, 9):
-            return BitrateMode.ABR
-        elif lame.vbr_method in (3, 4, 5, 6):
-            return BitrateMode.VBR
-        # everything else undefined, continue guessing
-
-    # info tags get only written by lame for cbr files
-    if xing.is_info:
-        return BitrateMode.CBR
-
-    # older lame and non-lame with some variant of vbr
-    if xing.vbr_scale != -1 or xing.lame_version:
-        return BitrateMode.VBR
-
-    return BitrateMode.UNKNOWN
-
-
-# Mode values.
-STEREO, JOINTSTEREO, DUALCHANNEL, MONO = xrange(4)
-
-
-class MPEGInfo(StreamInfo):
-    """MPEG audio stream information
-
-    Parse information about an MPEG audio file. This also reads the
-    Xing VBR header format.
-
-    This code was implemented based on the format documentation at
-    http://mpgedit.org/mpgedit/mpeg_format/mpeghdr.htm.
-
-    Useful attributes:
-
-    * length -- audio length, in seconds
-    * channels -- number of audio channels
-    * bitrate -- audio bitrate, in bits per second
-    * sketchy -- if true, the file may not be valid MPEG audio
-    * encoder_info -- a string containing encoder name and possibly version.
-                      In case a lame tag is present this will start with
-                      ``"LAME "``, if unknown it is empty, otherwise the
-                      text format is undefined.
-    * bitrate_mode -- a :class:`BitrateMode`
-
-    * track_gain -- replaygain track gain (89db) or None
-    * track_peak -- replaygain track peak or None
-    * album_gain -- replaygain album gain (89db) or None
-
-    Useless attributes:
-
-    * version -- MPEG version (1, 2, 2.5)
-    * layer -- 1, 2, or 3
-    * mode -- One of STEREO, JOINTSTEREO, DUALCHANNEL, or MONO (0-3)
-    * protected -- whether or not the file is "protected"
-    * padding -- whether or not audio frames are padded
-    * sample_rate -- audio sample rate, in Hz
-    """
-
-    # Map (version, layer) tuples to bitrates.
-    __BITRATE = {
-        (1, 1): [0, 32, 64, 96, 128, 160, 192, 224,
-                 256, 288, 320, 352, 384, 416, 448],
-        (1, 2): [0, 32, 48, 56, 64, 80, 96, 112, 128,
-                 160, 192, 224, 256, 320, 384],
-        (1, 3): [0, 32, 40, 48, 56, 64, 80, 96, 112,
-                 128, 160, 192, 224, 256, 320],
-        (2, 1): [0, 32, 48, 56, 64, 80, 96, 112, 128,
-                 144, 160, 176, 192, 224, 256],
-        (2, 2): [0, 8, 16, 24, 32, 40, 48, 56, 64,
-                 80, 96, 112, 128, 144, 160],
-    }
-
-    __BITRATE[(2, 3)] = __BITRATE[(2, 2)]
-    for i in xrange(1, 4):
-        __BITRATE[(2.5, i)] = __BITRATE[(2, i)]
-
-    # Map version to sample rates.
-    __RATES = {
-        1: [44100, 48000, 32000],
-        2: [22050, 24000, 16000],
-        2.5: [11025, 12000, 8000]
-    }
-
-    sketchy = False
-    encoder_info = u""
-    bitrate_mode = BitrateMode.UNKNOWN
-    track_gain = track_peak = album_gain = album_peak = None
-
-    def __init__(self, fileobj, offset=None):
-        """Parse MPEG stream information from a file-like object.
-
-        If an offset argument is given, it is used to start looking
-        for stream information and Xing headers; otherwise, ID3v2 tags
-        will be skipped automatically. A correct offset can make
-        loading files significantly faster.
-        """
-
-        try:
-            size = os.path.getsize(fileobj.name)
-        except (IOError, OSError, AttributeError):
-            fileobj.seek(0, 2)
-            size = fileobj.tell()
-
-        # If we don't get an offset, try to skip an ID3v2 tag.
-        if offset is None:
-            fileobj.seek(0, 0)
-            idata = fileobj.read(10)
-            try:
-                id3, insize = struct.unpack('>3sxxx4s', idata)
-            except struct.error:
-                id3, insize = b'', 0
-            insize = BitPaddedInt(insize)
-            if id3 == b'ID3' and insize > 0:
-                offset = insize + 10
-            else:
-                offset = 0
-
-        # Try to find two valid headers (meaning, very likely MPEG data)
-        # at the given offset, 30% through the file, 60% through the file,
-        # and 90% through the file.
-        for i in [offset, 0.3 * size, 0.6 * size, 0.9 * size]:
-            try:
-                self.__try(fileobj, int(i), size - offset)
-            except error:
-                pass
-            else:
-                break
-        # If we can't find any two consecutive frames, try to find just
-        # one frame back at the original offset given.
-        else:
-            self.__try(fileobj, offset, size - offset, False)
-            self.sketchy = True
-
-    def __try(self, fileobj, offset, real_size, check_second=True):
-        # This is going to be one really long function; bear with it,
-        # because there's not really a sane point to cut it up.
-        fileobj.seek(offset, 0)
-
-        # We "know" we have an MPEG file if we find two frames that look like
-        # valid MPEG data. If we can't find them in 32k of reads, something
-        # is horribly wrong (the longest frame can only be about 4k). This
-        # is assuming the offset didn't lie.
-        data = fileobj.read(32768)
-
-        frame_1 = data.find(b"\xff")
-        while 0 <= frame_1 <= (len(data) - 4):
-            frame_data = struct.unpack(">I", data[frame_1:frame_1 + 4])[0]
-            if ((frame_data >> 16) & 0xE0) != 0xE0:
-                frame_1 = data.find(b"\xff", frame_1 + 2)
-            else:
-                version = (frame_data >> 19) & 0x3
-                layer = (frame_data >> 17) & 0x3
-                protection = (frame_data >> 16) & 0x1
-                bitrate = (frame_data >> 12) & 0xF
-                sample_rate = (frame_data >> 10) & 0x3
-                padding = (frame_data >> 9) & 0x1
-                # private = (frame_data >> 8) & 0x1
-                self.mode = (frame_data >> 6) & 0x3
-                # mode_extension = (frame_data >> 4) & 0x3
-                # copyright = (frame_data >> 3) & 0x1
-                # original = (frame_data >> 2) & 0x1
-                # emphasis = (frame_data >> 0) & 0x3
-                if (version == 1 or layer == 0 or sample_rate == 0x3 or
-                        bitrate == 0 or bitrate == 0xF):
-                    frame_1 = data.find(b"\xff", frame_1 + 2)
-                else:
-                    break
-        else:
-            raise HeaderNotFoundError("can't sync to an MPEG frame")
-
-        self.channels = 1 if self.mode == MONO else 2
-
-        # There is a serious problem here, which is that many flags
-        # in an MPEG header are backwards.
-        self.version = [2.5, None, 2, 1][version]
-        self.layer = 4 - layer
-        self.protected = not protection
-        self.padding = bool(padding)
-
-        self.bitrate = self.__BITRATE[(self.version, self.layer)][bitrate]
-        self.bitrate *= 1000
-        self.sample_rate = self.__RATES[self.version][sample_rate]
-
-        if self.layer == 1:
-            frame_length = (
-                (12 * self.bitrate // self.sample_rate) + padding) * 4
-            frame_size = 384
-        elif self.version >= 2 and self.layer == 3:
-            frame_length = (72 * self.bitrate // self.sample_rate) + padding
-            frame_size = 576
-        else:
-            frame_length = (144 * self.bitrate // self.sample_rate) + padding
-            frame_size = 1152
-
-        if check_second:
-            possible = int(frame_1 + frame_length)
-            if possible > len(data) + 4:
-                raise HeaderNotFoundError("can't sync to second MPEG frame")
-            try:
-                frame_data = struct.unpack(
-                    ">H", data[possible:possible + 2])[0]
-            except struct.error:
-                raise HeaderNotFoundError("can't sync to second MPEG frame")
-            if (frame_data & 0xFFE0) != 0xFFE0:
-                raise HeaderNotFoundError("can't sync to second MPEG frame")
-
-        self.length = 8 * real_size / float(self.bitrate)
-
-        # Try to find/parse the Xing header, which trumps the above length
-        # and bitrate calculation.
-
-        if self.layer != 3:
-            return
-
-        # Xing
-        xing_offset = XingHeader.get_offset(self)
-        fileobj.seek(offset + frame_1 + xing_offset, 0)
-        try:
-            xing = XingHeader(fileobj)
-        except XingHeaderError:
-            pass
-        else:
-            lame = xing.lame_header
-            self.sketchy = False
-            self.bitrate_mode = _guess_xing_bitrate_mode(xing)
-            if xing.frames != -1:
-                samples = frame_size * xing.frames
-                if lame is not None:
-                    samples -= lame.encoder_delay_start
-                    samples -= lame.encoder_padding_end
-                self.length = float(samples) / self.sample_rate
-            if xing.bytes != -1 and self.length:
-                self.bitrate = int((xing.bytes * 8) / self.length)
-            if xing.lame_version:
-                self.encoder_info = u"LAME %s" % xing.lame_version
-            if lame is not None:
-                self.track_gain = lame.track_gain_adjustment
-                self.track_peak = lame.track_peak
-                self.album_gain = lame.album_gain_adjustment
-            return
-
-        # VBRI
-        vbri_offset = VBRIHeader.get_offset(self)
-        fileobj.seek(offset + frame_1 + vbri_offset, 0)
-        try:
-            vbri = VBRIHeader(fileobj)
-        except VBRIHeaderError:
-            pass
-        else:
-            self.bitrate_mode = BitrateMode.VBR
-            self.encoder_info = u"FhG"
-            self.sketchy = False
-            self.length = float(frame_size * vbri.frames) / self.sample_rate
-            if self.length:
-                self.bitrate = int((vbri.bytes * 8) / self.length)
-
-    def pprint(self):
-        info = str(self.bitrate_mode).split(".", 1)[-1]
-        if self.bitrate_mode == BitrateMode.UNKNOWN:
-            info = u"CBR?"
-        if self.encoder_info:
-            info += ", %s" % self.encoder_info
-        s = u"MPEG %s layer %d, %d bps (%s), %s Hz, %d chn, %.2f seconds" % (
-            self.version, self.layer, self.bitrate, info,
-            self.sample_rate, self.channels, self.length)
-        if self.sketchy:
-            s += u" (sketchy)"
-        return s
-
-
-class MP3(ID3FileType):
-    """An MPEG audio (usually MPEG-1 Layer 3) file.
-
-    :ivar info: :class:`MPEGInfo`
-    :ivar tags: :class:`ID3 <mutagen.id3.ID3>`
-    """
-
-    _Info = MPEGInfo
-
-    _mimes = ["audio/mpeg", "audio/mpg", "audio/x-mpeg"]
-
-    @property
-    def mime(self):
-        l = self.info.layer
-        return ["audio/mp%d" % l, "audio/x-mp%d" % l] + super(MP3, self).mime
-
-    @staticmethod
-    def score(filename, fileobj, header_data):
-        filename = filename.lower()
-
-        return (header_data.startswith(b"ID3") * 2 +
-                endswith(filename, b".mp3") +
-                endswith(filename, b".mp2") + endswith(filename, b".mpg") +
-                endswith(filename, b".mpeg"))
-
-
-Open = MP3
-
-
-class EasyMP3(MP3):
-    """Like MP3, but uses EasyID3 for tags.
-
-    :ivar info: :class:`MPEGInfo`
-    :ivar tags: :class:`EasyID3 <mutagen.easyid3.EasyID3>`
-    """
-
-    from mutagen.easyid3 import EasyID3 as ID3
-    ID3 = ID3
diff --git a/resources/lib/libraries/mutagen/mp4/__init__.py b/resources/lib/libraries/mutagen/mp4/__init__.py
deleted file mode 100644
index bc242ee8..00000000
--- a/resources/lib/libraries/mutagen/mp4/__init__.py
+++ /dev/null
@@ -1,1010 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write MPEG-4 audio files with iTunes metadata.
-
-This module will read MPEG-4 audio information and metadata,
-as found in Apple's MP4 (aka M4A, M4B, M4P) files.
-
-There is no official specification for this format. The source code
-for TagLib, FAAD, and various MPEG specifications at
-
-* http://developer.apple.com/documentation/QuickTime/QTFF/
-* http://www.geocities.com/xhelmboyx/quicktime/formats/mp4-layout.txt
-* http://standards.iso.org/ittf/PubliclyAvailableStandards/\
-c041828_ISO_IEC_14496-12_2005(E).zip
-* http://wiki.multimedia.cx/index.php?title=Apple_QuickTime
-
-were all consulted.
-"""
-
-import struct
-import sys
-
-from mutagen import FileType, Metadata, StreamInfo, PaddingInfo
-from mutagen._constants import GENRES
-from mutagen._util import (cdata, insert_bytes, DictProxy, MutagenError,
-                           hashable, enum, get_size, resize_bytes)
-from mutagen._compat import (reraise, PY2, string_types, text_type, chr_,
-                             iteritems, PY3, cBytesIO, izip, xrange)
-from ._atom import Atoms, Atom, AtomError
-from ._util import parse_full_atom
-from ._as_entry import AudioSampleEntry, ASEntryError
-
-
-class error(IOError, MutagenError):
-    pass
-
-
-class MP4MetadataError(error):
-    pass
-
-
-class MP4StreamInfoError(error):
-    pass
-
-
-class MP4MetadataValueError(ValueError, MP4MetadataError):
-    pass
-
-
-__all__ = ['MP4', 'Open', 'delete', 'MP4Cover', 'MP4FreeForm', 'AtomDataType']
-
-
-@enum
-class AtomDataType(object):
-    """Enum for `dataformat` attribute of MP4FreeForm.
-
-    .. versionadded:: 1.25
-    """
-
-    IMPLICIT = 0
-    """for use with tags for which no type needs to be indicated because
-       only one type is allowed"""
-
-    UTF8 = 1
-    """without any count or null terminator"""
-
-    UTF16 = 2
-    """also known as UTF-16BE"""
-
-    SJIS = 3
-    """deprecated unless it is needed for special Japanese characters"""
-
-    HTML = 6
-    """the HTML file header specifies which HTML version"""
-
-    XML = 7
-    """the XML header must identify the DTD or schemas"""
-
-    UUID = 8
-    """also known as GUID; stored as 16 bytes in binary (valid as an ID)"""
-
-    ISRC = 9
-    """stored as UTF-8 text (valid as an ID)"""
-
-    MI3P = 10
-    """stored as UTF-8 text (valid as an ID)"""
-
-    GIF = 12
-    """(deprecated) a GIF image"""
-
-    JPEG = 13
-    """a JPEG image"""
-
-    PNG = 14
-    """PNG image"""
-
-    URL = 15
-    """absolute, in UTF-8 characters"""
-
-    DURATION = 16
-    """in milliseconds, 32-bit integer"""
-
-    DATETIME = 17
-    """in UTC, counting seconds since midnight, January 1, 1904;
-       32 or 64-bits"""
-
-    GENRES = 18
-    """a list of enumerated values"""
-
-    INTEGER = 21
-    """a signed big-endian integer with length one of { 1,2,3,4,8 } bytes"""
-
-    RIAA_PA = 24
-    """RIAA parental advisory; { -1=no, 1=yes, 0=unspecified },
-       8-bit ingteger"""
-
-    UPC = 25
-    """Universal Product Code, in text UTF-8 format (valid as an ID)"""
-
-    BMP = 27
-    """Windows bitmap image"""
-
-
-@hashable
-class MP4Cover(bytes):
-    """A cover artwork.
-
-    Attributes:
-
-    * imageformat -- format of the image (either FORMAT_JPEG or FORMAT_PNG)
-    """
-
-    FORMAT_JPEG = AtomDataType.JPEG
-    FORMAT_PNG = AtomDataType.PNG
-
-    def __new__(cls, data, *args, **kwargs):
-        return bytes.__new__(cls, data)
-
-    def __init__(self, data, imageformat=FORMAT_JPEG):
-        self.imageformat = imageformat
-
-    __hash__ = bytes.__hash__
-
-    def __eq__(self, other):
-        if not isinstance(other, MP4Cover):
-            return bytes(self) == other
-
-        return (bytes(self) == bytes(other) and
-                self.imageformat == other.imageformat)
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def __repr__(self):
-        return "%s(%r, %r)" % (
-            type(self).__name__, bytes(self),
-            AtomDataType(self.imageformat))
-
-
-@hashable
-class MP4FreeForm(bytes):
-    """A freeform value.
-
-    Attributes:
-
-    * dataformat -- format of the data (see AtomDataType)
-    """
-
-    FORMAT_DATA = AtomDataType.IMPLICIT  # deprecated
-    FORMAT_TEXT = AtomDataType.UTF8  # deprecated
-
-    def __new__(cls, data, *args, **kwargs):
-        return bytes.__new__(cls, data)
-
-    def __init__(self, data, dataformat=AtomDataType.UTF8, version=0):
-        self.dataformat = dataformat
-        self.version = version
-
-    __hash__ = bytes.__hash__
-
-    def __eq__(self, other):
-        if not isinstance(other, MP4FreeForm):
-            return bytes(self) == other
-
-        return (bytes(self) == bytes(other) and
-                self.dataformat == other.dataformat and
-                self.version == other.version)
-
-    def __ne__(self, other):
-        return not self.__eq__(other)
-
-    def __repr__(self):
-        return "%s(%r, %r)" % (
-            type(self).__name__, bytes(self),
-            AtomDataType(self.dataformat))
-
-
-
-def _name2key(name):
-    if PY2:
-        return name
-    return name.decode("latin-1")
-
-
-def _key2name(key):
-    if PY2:
-        return key
-    return key.encode("latin-1")
-
-
-def _find_padding(atom_path):
-    # Check for padding "free" atom
-    # XXX: we only use them if they are adjacent to ilst, and only one.
-    # and there also is a top level free atom which we could use maybe..?
-
-    meta, ilst = atom_path[-2:]
-    assert meta.name == b"meta" and ilst.name == b"ilst"
-    index = meta.children.index(ilst)
-    try:
-        prev = meta.children[index - 1]
-        if prev.name == b"free":
-            return prev
-    except IndexError:
-        pass
-
-    try:
-        next_ = meta.children[index + 1]
-        if next_.name == b"free":
-            return next_
-    except IndexError:
-        pass
-
-
-class MP4Tags(DictProxy, Metadata):
-    r"""Dictionary containing Apple iTunes metadata list key/values.
-
-    Keys are four byte identifiers, except for freeform ('----')
-    keys. Values are usually unicode strings, but some atoms have a
-    special structure:
-
-    Text values (multiple values per key are supported):
-
-    * '\\xa9nam' -- track title
-    * '\\xa9alb' -- album
-    * '\\xa9ART' -- artist
-    * 'aART' -- album artist
-    * '\\xa9wrt' -- composer
-    * '\\xa9day' -- year
-    * '\\xa9cmt' -- comment
-    * 'desc' -- description (usually used in podcasts)
-    * 'purd' -- purchase date
-    * '\\xa9grp' -- grouping
-    * '\\xa9gen' -- genre
-    * '\\xa9lyr' -- lyrics
-    * 'purl' -- podcast URL
-    * 'egid' -- podcast episode GUID
-    * 'catg' -- podcast category
-    * 'keyw' -- podcast keywords
-    * '\\xa9too' -- encoded by
-    * 'cprt' -- copyright
-    * 'soal' -- album sort order
-    * 'soaa' -- album artist sort order
-    * 'soar' -- artist sort order
-    * 'sonm' -- title sort order
-    * 'soco' -- composer sort order
-    * 'sosn' -- show sort order
-    * 'tvsh' -- show name
-
-    Boolean values:
-
-    * 'cpil' -- part of a compilation
-    * 'pgap' -- part of a gapless album
-    * 'pcst' -- podcast (iTunes reads this only on import)
-
-    Tuples of ints (multiple values per key are supported):
-
-    * 'trkn' -- track number, total tracks
-    * 'disk' -- disc number, total discs
-
-    Others:
-
-    * 'tmpo' -- tempo/BPM, 16 bit int
-    * 'covr' -- cover artwork, list of MP4Cover objects (which are
-      tagged strs)
-    * 'gnre' -- ID3v1 genre. Not supported, use '\\xa9gen' instead.
-
-    The freeform '----' frames use a key in the format '----:mean:name'
-    where 'mean' is usually 'com.apple.iTunes' and 'name' is a unique
-    identifier for this frame. The value is a str, but is probably
-    text that can be decoded as UTF-8. Multiple values per key are
-    supported.
-
-    MP4 tag data cannot exist outside of the structure of an MP4 file,
-    so this class should not be manually instantiated.
-
-    Unknown non-text tags and tags that failed to parse will be written
-    back as is.
-    """
-
-    def __init__(self, *args, **kwargs):
-        self._failed_atoms = {}
-        super(MP4Tags, self).__init__(*args, **kwargs)
-
-    def load(self, atoms, fileobj):
-        try:
-            path = atoms.path(b"moov", b"udta", b"meta", b"ilst")
-        except KeyError as key:
-            raise MP4MetadataError(key)
-
-        free = _find_padding(path)
-        self._padding = free.datalength if free is not None else 0
-
-        ilst = path[-1]
-        for atom in ilst.children:
-            ok, data = atom.read(fileobj)
-            if not ok:
-                raise MP4MetadataError("Not enough data")
-
-            try:
-                if atom.name in self.__atoms:
-                    info = self.__atoms[atom.name]
-                    info[0](self, atom, data)
-                else:
-                    # unknown atom, try as text
-                    self.__parse_text(atom, data, implicit=False)
-            except MP4MetadataError:
-                # parsing failed, save them so we can write them back
-                key = _name2key(atom.name)
-                self._failed_atoms.setdefault(key, []).append(data)
-
-    def __setitem__(self, key, value):
-        if not isinstance(key, str):
-            raise TypeError("key has to be str")
-        super(MP4Tags, self).__setitem__(key, value)
-
-    @classmethod
-    def _can_load(cls, atoms):
-        return b"moov.udta.meta.ilst" in atoms
-
-    @staticmethod
-    def _key_sort(item):
-        (key, v) = item
-        # iTunes always writes the tags in order of "relevance", try
-        # to copy it as closely as possible.
-        order = ["\xa9nam", "\xa9ART", "\xa9wrt", "\xa9alb",
-                 "\xa9gen", "gnre", "trkn", "disk",
-                 "\xa9day", "cpil", "pgap", "pcst", "tmpo",
-                 "\xa9too", "----", "covr", "\xa9lyr"]
-        order = dict(izip(order, xrange(len(order))))
-        last = len(order)
-        # If there's no key-based way to distinguish, order by length.
-        # If there's still no way, go by string comparison on the
-        # values, so we at least have something determinstic.
-        return (order.get(key[:4], last), len(repr(v)), repr(v))
-
-    def save(self, filename, padding=None):
-        """Save the metadata to the given filename."""
-
-        values = []
-        items = sorted(self.items(), key=self._key_sort)
-        for key, value in items:
-            atom_name = _key2name(key)[:4]
-            if atom_name in self.__atoms:
-                render_func = self.__atoms[atom_name][1]
-            else:
-                render_func = type(self).__render_text
-
-            try:
-                values.append(render_func(self, key, value))
-            except (TypeError, ValueError) as s:
-                reraise(MP4MetadataValueError, s, sys.exc_info()[2])
-
-        for key, failed in iteritems(self._failed_atoms):
-            # don't write atoms back if we have added a new one with
-            # the same name, this excludes freeform which can have
-            # multiple atoms with the same key (most parsers seem to be able
-            # to handle that)
-            if key in self:
-                assert _key2name(key) != b"----"
-                continue
-            for data in failed:
-                values.append(Atom.render(_key2name(key), data))
-
-        data = Atom.render(b"ilst", b"".join(values))
-
-        # Find the old atoms.
-        with open(filename, "rb+") as fileobj:
-            try:
-                atoms = Atoms(fileobj)
-            except AtomError as err:
-                reraise(error, err, sys.exc_info()[2])
-
-            self.__save(fileobj, atoms, data, padding)
-
-    def __save(self, fileobj, atoms, data, padding):
-        try:
-            path = atoms.path(b"moov", b"udta", b"meta", b"ilst")
-        except KeyError:
-            self.__save_new(fileobj, atoms, data, padding)
-        else:
-            self.__save_existing(fileobj, atoms, path, data, padding)
-
-    def __pad_ilst(self, data, length=None):
-        if length is None:
-            length = ((len(data) + 1023) & ~1023) - len(data)
-        return Atom.render(b"free", b"\x00" * length)
-
-    def __save_new(self, fileobj, atoms, ilst_data, padding_func):
-        hdlr = Atom.render(b"hdlr", b"\x00" * 8 + b"mdirappl" + b"\x00" * 9)
-        meta_data = b"\x00\x00\x00\x00" + hdlr + ilst_data
-
-        try:
-            path = atoms.path(b"moov", b"udta")
-        except KeyError:
-            path = atoms.path(b"moov")
-
-        offset = path[-1]._dataoffset
-
-        # ignoring some atom overhead... but we don't have padding left anyway
-        # and padding_size is guaranteed to be less than zero
-        content_size = get_size(fileobj) - offset
-        padding_size = -len(meta_data)
-        assert padding_size < 0
-        info = PaddingInfo(padding_size, content_size)
-        new_padding = info._get_padding(padding_func)
-        new_padding = min(0xFFFFFFFF, new_padding)
-
-        free = Atom.render(b"free", b"\x00" * new_padding)
-        meta = Atom.render(b"meta", meta_data + free)
-        if path[-1].name != b"udta":
-            # moov.udta not found -- create one
-            data = Atom.render(b"udta", meta)
-        else:
-            data = meta
-
-        insert_bytes(fileobj, len(data), offset)
-        fileobj.seek(offset)
-        fileobj.write(data)
-        self.__update_parents(fileobj, path, len(data))
-        self.__update_offsets(fileobj, atoms, len(data), offset)
-
-    def __save_existing(self, fileobj, atoms, path, ilst_data, padding_func):
-        # Replace the old ilst atom.
-        ilst = path[-1]
-        offset = ilst.offset
-        length = ilst.length
-
-        # Use adjacent free atom if there is one
-        free = _find_padding(path)
-        if free is not None:
-            offset = min(offset, free.offset)
-            length += free.length
-
-        # Always add a padding atom to make things easier
-        padding_overhead = len(Atom.render(b"free", b""))
-        content_size = get_size(fileobj) - (offset + length)
-        padding_size = length - (len(ilst_data) + padding_overhead)
-        info = PaddingInfo(padding_size, content_size)
-        new_padding = info._get_padding(padding_func)
-        # Limit padding size so we can be sure the free atom overhead is as we
-        # calculated above (see Atom.render)
-        new_padding = min(0xFFFFFFFF, new_padding)
-
-        ilst_data += Atom.render(b"free", b"\x00" * new_padding)
-
-        resize_bytes(fileobj, length, len(ilst_data), offset)
-        delta = len(ilst_data) - length
-
-        fileobj.seek(offset)
-        fileobj.write(ilst_data)
-        self.__update_parents(fileobj, path[:-1], delta)
-        self.__update_offsets(fileobj, atoms, delta, offset)
-
-    def __update_parents(self, fileobj, path, delta):
-        """Update all parent atoms with the new size."""
-
-        if delta == 0:
-            return
-
-        for atom in path:
-            fileobj.seek(atom.offset)
-            size = cdata.uint_be(fileobj.read(4))
-            if size == 1:  # 64bit
-                # skip name (4B) and read size (8B)
-                size = cdata.ulonglong_be(fileobj.read(12)[4:])
-                fileobj.seek(atom.offset + 8)
-                fileobj.write(cdata.to_ulonglong_be(size + delta))
-            else:  # 32bit
-                fileobj.seek(atom.offset)
-                fileobj.write(cdata.to_uint_be(size + delta))
-
-    def __update_offset_table(self, fileobj, fmt, atom, delta, offset):
-        """Update offset table in the specified atom."""
-        if atom.offset > offset:
-            atom.offset += delta
-        fileobj.seek(atom.offset + 12)
-        data = fileobj.read(atom.length - 12)
-        fmt = fmt % cdata.uint_be(data[:4])
-        offsets = struct.unpack(fmt, data[4:])
-        offsets = [o + (0, delta)[offset < o] for o in offsets]
-        fileobj.seek(atom.offset + 16)
-        fileobj.write(struct.pack(fmt, *offsets))
-
-    def __update_tfhd(self, fileobj, atom, delta, offset):
-        if atom.offset > offset:
-            atom.offset += delta
-        fileobj.seek(atom.offset + 9)
-        data = fileobj.read(atom.length - 9)
-        flags = cdata.uint_be(b"\x00" + data[:3])
-        if flags & 1:
-            o = cdata.ulonglong_be(data[7:15])
-            if o > offset:
-                o += delta
-            fileobj.seek(atom.offset + 16)
-            fileobj.write(cdata.to_ulonglong_be(o))
-
-    def __update_offsets(self, fileobj, atoms, delta, offset):
-        """Update offset tables in all 'stco' and 'co64' atoms."""
-        if delta == 0:
-            return
-        moov = atoms[b"moov"]
-        for atom in moov.findall(b'stco', True):
-            self.__update_offset_table(fileobj, ">%dI", atom, delta, offset)
-        for atom in moov.findall(b'co64', True):
-            self.__update_offset_table(fileobj, ">%dQ", atom, delta, offset)
-        try:
-            for atom in atoms[b"moof"].findall(b'tfhd', True):
-                self.__update_tfhd(fileobj, atom, delta, offset)
-        except KeyError:
-            pass
-
-    def __parse_data(self, atom, data):
-        pos = 0
-        while pos < atom.length - 8:
-            head = data[pos:pos + 12]
-            if len(head) != 12:
-                raise MP4MetadataError("truncated atom % r" % atom.name)
-            length, name = struct.unpack(">I4s", head[:8])
-            version = ord(head[8:9])
-            flags = struct.unpack(">I", b"\x00" + head[9:12])[0]
-            if name != b"data":
-                raise MP4MetadataError(
-                    "unexpected atom %r inside %r" % (name, atom.name))
-
-            chunk = data[pos + 16:pos + length]
-            if len(chunk) != length - 16:
-                raise MP4MetadataError("truncated atom % r" % atom.name)
-            yield version, flags, chunk
-            pos += length
-
-    def __add(self, key, value, single=False):
-        assert isinstance(key, str)
-
-        if single:
-            self[key] = value
-        else:
-            self.setdefault(key, []).extend(value)
-
-    def __render_data(self, key, version, flags, value):
-        return Atom.render(_key2name(key), b"".join([
-            Atom.render(
-                b"data", struct.pack(">2I", version << 24 | flags, 0) + data)
-            for data in value]))
-
-    def __parse_freeform(self, atom, data):
-        length = cdata.uint_be(data[:4])
-        mean = data[12:length]
-        pos = length
-        length = cdata.uint_be(data[pos:pos + 4])
-        name = data[pos + 12:pos + length]
-        pos += length
-        value = []
-        while pos < atom.length - 8:
-            length, atom_name = struct.unpack(">I4s", data[pos:pos + 8])
-            if atom_name != b"data":
-                raise MP4MetadataError(
-                    "unexpected atom %r inside %r" % (atom_name, atom.name))
-
-            version = ord(data[pos + 8:pos + 8 + 1])
-            flags = struct.unpack(">I", b"\x00" + data[pos + 9:pos + 12])[0]
-            value.append(MP4FreeForm(data[pos + 16:pos + length],
-                                     dataformat=flags, version=version))
-            pos += length
-
-        key = _name2key(atom.name + b":" + mean + b":" + name)
-        self.__add(key, value)
-
-    def __render_freeform(self, key, value):
-        if isinstance(value, bytes):
-            value = [value]
-
-        dummy, mean, name = _key2name(key).split(b":", 2)
-        mean = struct.pack(">I4sI", len(mean) + 12, b"mean", 0) + mean
-        name = struct.pack(">I4sI", len(name) + 12, b"name", 0) + name
-
-        data = b""
-        for v in value:
-            flags = AtomDataType.UTF8
-            version = 0
-            if isinstance(v, MP4FreeForm):
-                flags = v.dataformat
-                version = v.version
-
-            data += struct.pack(
-                ">I4s2I", len(v) + 16, b"data", version << 24 | flags, 0)
-            data += v
-
-        return Atom.render(b"----", mean + name + data)
-
-    def __parse_pair(self, atom, data):
-        key = _name2key(atom.name)
-        values = [struct.unpack(">2H", d[2:6]) for
-                  version, flags, d in self.__parse_data(atom, data)]
-        self.__add(key, values)
-
-    def __render_pair(self, key, value):
-        data = []
-        for (track, total) in value:
-            if 0 <= track < 1 << 16 and 0 <= total < 1 << 16:
-                data.append(struct.pack(">4H", 0, track, total, 0))
-            else:
-                raise MP4MetadataValueError(
-                    "invalid numeric pair %r" % ((track, total),))
-        return self.__render_data(key, 0, AtomDataType.IMPLICIT, data)
-
-    def __render_pair_no_trailing(self, key, value):
-        data = []
-        for (track, total) in value:
-            if 0 <= track < 1 << 16 and 0 <= total < 1 << 16:
-                data.append(struct.pack(">3H", 0, track, total))
-            else:
-                raise MP4MetadataValueError(
-                    "invalid numeric pair %r" % ((track, total),))
-        return self.__render_data(key, 0, AtomDataType.IMPLICIT, data)
-
-    def __parse_genre(self, atom, data):
-        values = []
-        for version, flags, data in self.__parse_data(atom, data):
-            # version = 0, flags = 0
-            if len(data) != 2:
-                raise MP4MetadataValueError("invalid genre")
-            genre = cdata.short_be(data)
-            # Translate to a freeform genre.
-            try:
-                genre = GENRES[genre - 1]
-            except IndexError:
-                # this will make us write it back at least
-                raise MP4MetadataValueError("unknown genre")
-            values.append(genre)
-        key = _name2key(b"\xa9gen")
-        self.__add(key, values)
-
-    def __parse_tempo(self, atom, data):
-        values = []
-        for version, flags, data in self.__parse_data(atom, data):
-            # version = 0, flags = 0 or 21
-            if len(data) != 2:
-                raise MP4MetadataValueError("invalid tempo")
-            values.append(cdata.ushort_be(data))
-        key = _name2key(atom.name)
-        self.__add(key, values)
-
-    def __render_tempo(self, key, value):
-        try:
-            if len(value) == 0:
-                return self.__render_data(key, 0, AtomDataType.INTEGER, b"")
-
-            if (min(value) < 0) or (max(value) >= 2 ** 16):
-                raise MP4MetadataValueError(
-                    "invalid 16 bit integers: %r" % value)
-        except TypeError:
-            raise MP4MetadataValueError(
-                "tmpo must be a list of 16 bit integers")
-
-        values = [cdata.to_ushort_be(v) for v in value]
-        return self.__render_data(key, 0, AtomDataType.INTEGER, values)
-
-    def __parse_bool(self, atom, data):
-        for version, flags, data in self.__parse_data(atom, data):
-            if len(data) != 1:
-                raise MP4MetadataValueError("invalid bool")
-
-            value = bool(ord(data))
-            key = _name2key(atom.name)
-            self.__add(key, value, single=True)
-
-    def __render_bool(self, key, value):
-        return self.__render_data(
-            key, 0, AtomDataType.INTEGER, [chr_(bool(value))])
-
-    def __parse_cover(self, atom, data):
-        values = []
-        pos = 0
-        while pos < atom.length - 8:
-            length, name, imageformat = struct.unpack(">I4sI",
-                                                      data[pos:pos + 12])
-            if name != b"data":
-                if name == b"name":
-                    pos += length
-                    continue
-                raise MP4MetadataError(
-                    "unexpected atom %r inside 'covr'" % name)
-            if imageformat not in (MP4Cover.FORMAT_JPEG, MP4Cover.FORMAT_PNG):
-                # Sometimes AtomDataType.IMPLICIT or simply wrong.
-                # In all cases it was jpeg, so default to it
-                imageformat = MP4Cover.FORMAT_JPEG
-            cover = MP4Cover(data[pos + 16:pos + length], imageformat)
-            values.append(cover)
-            pos += length
-
-        key = _name2key(atom.name)
-        self.__add(key, values)
-
-    def __render_cover(self, key, value):
-        atom_data = []
-        for cover in value:
-            try:
-                imageformat = cover.imageformat
-            except AttributeError:
-                imageformat = MP4Cover.FORMAT_JPEG
-            atom_data.append(Atom.render(
-                b"data", struct.pack(">2I", imageformat, 0) + cover))
-        return Atom.render(_key2name(key), b"".join(atom_data))
-
-    def __parse_text(self, atom, data, implicit=True):
-        # implicit = False, for parsing unknown atoms only take utf8 ones.
-        # For known ones we can assume the implicit are utf8 too.
-        values = []
-        for version, flags, atom_data in self.__parse_data(atom, data):
-            if implicit:
-                if flags not in (AtomDataType.IMPLICIT, AtomDataType.UTF8):
-                    raise MP4MetadataError(
-                        "Unknown atom type %r for %r" % (flags, atom.name))
-            else:
-                if flags != AtomDataType.UTF8:
-                    raise MP4MetadataError(
-                        "%r is not text, ignore" % atom.name)
-
-            try:
-                text = atom_data.decode("utf-8")
-            except UnicodeDecodeError as e:
-                raise MP4MetadataError("%s: %s" % (_name2key(atom.name), e))
-
-            values.append(text)
-
-        key = _name2key(atom.name)
-        self.__add(key, values)
-
-    def __render_text(self, key, value, flags=AtomDataType.UTF8):
-        if isinstance(value, string_types):
-            value = [value]
-
-        encoded = []
-        for v in value:
-            if not isinstance(v, text_type):
-                if PY3:
-                    raise TypeError("%r not str" % v)
-                v = v.decode("utf-8")
-            encoded.append(v.encode("utf-8"))
-
-        return self.__render_data(key, 0, flags, encoded)
-
-    def delete(self, filename):
-        """Remove the metadata from the given filename."""
-
-        self._failed_atoms.clear()
-        self.clear()
-        self.save(filename, padding=lambda x: 0)
-
-    __atoms = {
-        b"----": (__parse_freeform, __render_freeform),
-        b"trkn": (__parse_pair, __render_pair),
-        b"disk": (__parse_pair, __render_pair_no_trailing),
-        b"gnre": (__parse_genre, None),
-        b"tmpo": (__parse_tempo, __render_tempo),
-        b"cpil": (__parse_bool, __render_bool),
-        b"pgap": (__parse_bool, __render_bool),
-        b"pcst": (__parse_bool, __render_bool),
-        b"covr": (__parse_cover, __render_cover),
-        b"purl": (__parse_text, __render_text),
-        b"egid": (__parse_text, __render_text),
-    }
-
-    # these allow implicit flags and parse as text
-    for name in [b"\xa9nam", b"\xa9alb", b"\xa9ART", b"aART", b"\xa9wrt",
-                 b"\xa9day", b"\xa9cmt", b"desc", b"purd", b"\xa9grp",
-                 b"\xa9gen", b"\xa9lyr", b"catg", b"keyw", b"\xa9too",
-                 b"cprt", b"soal", b"soaa", b"soar", b"sonm", b"soco",
-                 b"sosn", b"tvsh"]:
-        __atoms[name] = (__parse_text, __render_text)
-
-    def pprint(self):
-
-        def to_line(key, value):
-            assert isinstance(key, text_type)
-            if isinstance(value, text_type):
-                return u"%s=%s" % (key, value)
-            return u"%s=%r" % (key, value)
-
-        values = []
-        for key, value in sorted(iteritems(self)):
-            if not isinstance(key, text_type):
-                key = key.decode("latin-1")
-            if key == "covr":
-                values.append(u"%s=%s" % (key, u", ".join(
-                    [u"[%d bytes of data]" % len(data) for data in value])))
-            elif isinstance(value, list):
-                for v in value:
-                    values.append(to_line(key, v))
-            else:
-                values.append(to_line(key, value))
-        return u"\n".join(values)
-
-
-class MP4Info(StreamInfo):
-    """MPEG-4 stream information.
-
-    Attributes:
-
-    * bitrate -- bitrate in bits per second, as an int
-    * length -- file length in seconds, as a float
-    * channels -- number of audio channels
-    * sample_rate -- audio sampling rate in Hz
-    * bits_per_sample -- bits per sample
-    * codec (string):
-        * if starting with ``"mp4a"`` uses an mp4a audio codec
-          (see the codec parameter in rfc6381 for details e.g. ``"mp4a.40.2"``)
-        * for everything else see a list of possible values at
-          http://www.mp4ra.org/codecs.html
-
-        e.g. ``"mp4a"``, ``"alac"``, ``"mp4a.40.2"``, ``"ac-3"`` etc.
-    * codec_description (string):
-        Name of the codec used (ALAC, AAC LC, AC-3...). Values might change in
-        the future, use for display purposes only.
-    """
-
-    bitrate = 0
-    channels = 0
-    sample_rate = 0
-    bits_per_sample = 0
-    codec = u""
-    codec_name = u""
-
-    def __init__(self, atoms, fileobj):
-        try:
-            moov = atoms[b"moov"]
-        except KeyError:
-            raise MP4StreamInfoError("not a MP4 file")
-
-        for trak in moov.findall(b"trak"):
-            hdlr = trak[b"mdia", b"hdlr"]
-            ok, data = hdlr.read(fileobj)
-            if not ok:
-                raise MP4StreamInfoError("Not enough data")
-            if data[8:12] == b"soun":
-                break
-        else:
-            raise MP4StreamInfoError("track has no audio data")
-
-        mdhd = trak[b"mdia", b"mdhd"]
-        ok, data = mdhd.read(fileobj)
-        if not ok:
-            raise MP4StreamInfoError("Not enough data")
-
-        try:
-            version, flags, data = parse_full_atom(data)
-        except ValueError as e:
-            raise MP4StreamInfoError(e)
-
-        if version == 0:
-            offset = 8
-            fmt = ">2I"
-        elif version == 1:
-            offset = 16
-            fmt = ">IQ"
-        else:
-            raise MP4StreamInfoError("Unknown mdhd version %d" % version)
-
-        end = offset + struct.calcsize(fmt)
-        unit, length = struct.unpack(fmt, data[offset:end])
-        try:
-            self.length = float(length) / unit
-        except ZeroDivisionError:
-            self.length = 0
-
-        try:
-            atom = trak[b"mdia", b"minf", b"stbl", b"stsd"]
-        except KeyError:
-            pass
-        else:
-            self._parse_stsd(atom, fileobj)
-
-    def _parse_stsd(self, atom, fileobj):
-        """Sets channels, bits_per_sample, sample_rate and optionally bitrate.
-
-        Can raise MP4StreamInfoError.
-        """
-
-        assert atom.name == b"stsd"
-
-        ok, data = atom.read(fileobj)
-        if not ok:
-            raise MP4StreamInfoError("Invalid stsd")
-
-        try:
-            version, flags, data = parse_full_atom(data)
-        except ValueError as e:
-            raise MP4StreamInfoError(e)
-
-        if version != 0:
-            raise MP4StreamInfoError("Unsupported stsd version")
-
-        try:
-            num_entries, offset = cdata.uint32_be_from(data, 0)
-        except cdata.error as e:
-            raise MP4StreamInfoError(e)
-
-        if num_entries == 0:
-            return
-
-        # look at the first entry if there is one
-        entry_fileobj = cBytesIO(data[offset:])
-        try:
-            entry_atom = Atom(entry_fileobj)
-        except AtomError as e:
-            raise MP4StreamInfoError(e)
-
-        try:
-            entry = AudioSampleEntry(entry_atom, entry_fileobj)
-        except ASEntryError as e:
-            raise MP4StreamInfoError(e)
-        else:
-            self.channels = entry.channels
-            self.bits_per_sample = entry.sample_size
-            self.sample_rate = entry.sample_rate
-            self.bitrate = entry.bitrate
-            self.codec = entry.codec
-            self.codec_description = entry.codec_description
-
-    def pprint(self):
-        return "MPEG-4 audio (%s), %.2f seconds, %d bps" % (
-            self.codec_description, self.length, self.bitrate)
-
-
-class MP4(FileType):
-    """An MPEG-4 audio file, probably containing AAC.
-
-    If more than one track is present in the file, the first is used.
-    Only audio ('soun') tracks will be read.
-
-    :ivar info: :class:`MP4Info`
-    :ivar tags: :class:`MP4Tags`
-    """
-
-    MP4Tags = MP4Tags
-
-    _mimes = ["audio/mp4", "audio/x-m4a", "audio/mpeg4", "audio/aac"]
-
-    def load(self, filename):
-        self.filename = filename
-        with open(filename, "rb") as fileobj:
-            try:
-                atoms = Atoms(fileobj)
-            except AtomError as err:
-                reraise(error, err, sys.exc_info()[2])
-
-            try:
-                self.info = MP4Info(atoms, fileobj)
-            except error:
-                raise
-            except Exception as err:
-                reraise(MP4StreamInfoError, err, sys.exc_info()[2])
-
-            if not MP4Tags._can_load(atoms):
-                self.tags = None
-                self._padding = 0
-            else:
-                try:
-                    self.tags = self.MP4Tags(atoms, fileobj)
-                except error:
-                    raise
-                except Exception as err:
-                    reraise(MP4MetadataError, err, sys.exc_info()[2])
-                else:
-                    self._padding = self.tags._padding
-
-    def add_tags(self):
-        if self.tags is None:
-            self.tags = self.MP4Tags()
-        else:
-            raise error("an MP4 tag already exists")
-
-    @staticmethod
-    def score(filename, fileobj, header_data):
-        return (b"ftyp" in header_data) + (b"mp4" in header_data)
-
-
-Open = MP4
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    MP4(filename).delete()
diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/__init__.cpython-35.pyc
deleted file mode 100644
index de968da940fc35ac7e446528711bd4b0f1e18e24..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 31145
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

diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_as_entry.cpython-35.pyc
deleted file mode 100644
index 31483c463fa273b25f89cbb8fab1e7d451fa97b9..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 14269
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
NQ`65(e{uSE{s(x{fz$v1

diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_atom.cpython-35.pyc
deleted file mode 100644
index f4abf385e2cb9aadc4560b18c414dfbf0ef1a9cc..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 6248
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
L*-CESn(zGw2^KqW

diff --git a/resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc b/resources/lib/libraries/mutagen/mp4/__pycache__/_util.cpython-35.pyc
deleted file mode 100644
index e7df30cc48b2f9edb33acfd449a0cb20fcf0e36c..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 590
zcmYjN%}OId5U!rdY$R(|@u2L%rH?z{=CHe7L_`SQM1-KQG7viHPBLpx_t4!VMD*g%
z3-}hkQeQpg=6O$6w}G{os;R24s`{(i>~=d(qt~-P3BU_{5RBmv)lxJZKLQe<B5*-~
z@C0}Y4*(bFv|x~D3#@{i_-xFXV`CCRloT~X?W5Wm8V7I#a8JU#O91l?9QsMf+!1g|
zKyfq<eN5mhW^XZ@!rvC~EMn-8h;0QB>j~Vvcj25*5t*dOsC~@`x)PK*U0KSEpjX1!
zQY*oGbWqFEQ8yD6ZE9L7`h5xR(1~PIyF+U>mzwds*P$qVrl?`174)3RMjRQV%{#Ja
z;ZoOYN;@i)Zl*ID_BUbwMKCsA;v~;}Duwc$|NTDIhMRC^-)>6fh9i-uz7^Vidr8M<
zk<K%iRjm8>v%goI&fQF_U%SO|IVx=F#E+VdFWFSsqG}xeRZ-Qyi(%tR*{kP%t!BoG
z;iQpr7;f=7km{g;Y=f*On`E1qFBqNR<K5+iUhIzCA{Jl&V7gfESN1E-8z~M}FtTBf
G_4Ex6hm83E

diff --git a/resources/lib/libraries/mutagen/mp4/_as_entry.py b/resources/lib/libraries/mutagen/mp4/_as_entry.py
deleted file mode 100644
index 306d5720..00000000
--- a/resources/lib/libraries/mutagen/mp4/_as_entry.py
+++ /dev/null
@@ -1,542 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2014 Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-from mutagen._compat import cBytesIO, xrange
-from mutagen.aac import ProgramConfigElement
-from mutagen._util import BitReader, BitReaderError, cdata
-from mutagen._compat import text_type
-from ._util import parse_full_atom
-from ._atom import Atom, AtomError
-
-
-class ASEntryError(Exception):
-    pass
-
-
-class AudioSampleEntry(object):
-    """Parses an AudioSampleEntry atom.
-
-    Private API.
-
-    Attrs:
-        channels (int): number of channels
-        sample_size (int): sample size in bits
-        sample_rate (int): sample rate in Hz
-        bitrate (int): bits per second (0 means unknown)
-        codec (string):
-            audio codec, either 'mp4a[.*][.*]' (rfc6381) or 'alac'
-        codec_description (string): descriptive codec name e.g. "AAC LC+SBR"
-
-    Can raise ASEntryError.
-    """
-
-    channels = 0
-    sample_size = 0
-    sample_rate = 0
-    bitrate = 0
-    codec = None
-    codec_description = None
-
-    def __init__(self, atom, fileobj):
-        ok, data = atom.read(fileobj)
-        if not ok:
-            raise ASEntryError("too short %r atom" % atom.name)
-
-        fileobj = cBytesIO(data)
-        r = BitReader(fileobj)
-
-        try:
-            # SampleEntry
-            r.skip(6 * 8)  # reserved
-            r.skip(2 * 8)  # data_ref_index
-
-            # AudioSampleEntry
-            r.skip(8 * 8)  # reserved
-            self.channels = r.bits(16)
-            self.sample_size = r.bits(16)
-            r.skip(2 * 8)  # pre_defined
-            r.skip(2 * 8)  # reserved
-            self.sample_rate = r.bits(32) >> 16
-        except BitReaderError as e:
-            raise ASEntryError(e)
-
-        assert r.is_aligned()
-
-        try:
-            extra = Atom(fileobj)
-        except AtomError as e:
-            raise ASEntryError(e)
-
-        self.codec = atom.name.decode("latin-1")
-        self.codec_description = None
-
-        if atom.name == b"mp4a" and extra.name == b"esds":
-            self._parse_esds(extra, fileobj)
-        elif atom.name == b"alac" and extra.name == b"alac":
-            self._parse_alac(extra, fileobj)
-        elif atom.name == b"ac-3" and extra.name == b"dac3":
-            self._parse_dac3(extra, fileobj)
-
-        if self.codec_description is None:
-            self.codec_description = self.codec.upper()
-
-    def _parse_dac3(self, atom, fileobj):
-        # ETSI TS 102 366
-
-        assert atom.name == b"dac3"
-
-        ok, data = atom.read(fileobj)
-        if not ok:
-            raise ASEntryError("truncated %s atom" % atom.name)
-        fileobj = cBytesIO(data)
-        r = BitReader(fileobj)
-
-        # sample_rate in AudioSampleEntry covers values in
-        # fscod2 and not just fscod, so ignore fscod here.
-        try:
-            r.skip(2 + 5 + 3)  # fscod, bsid, bsmod
-            acmod = r.bits(3)
-            lfeon = r.bits(1)
-            bit_rate_code = r.bits(5)
-            r.skip(5)  # reserved
-        except BitReaderError as e:
-            raise ASEntryError(e)
-
-        self.channels = [2, 1, 2, 3, 3, 4, 4, 5][acmod] + lfeon
-
-        try:
-            self.bitrate = [
-                32, 40, 48, 56, 64, 80, 96, 112, 128, 160, 192,
-                224, 256, 320, 384, 448, 512, 576, 640][bit_rate_code] * 1000
-        except IndexError:
-            pass
-
-    def _parse_alac(self, atom, fileobj):
-        # https://alac.macosforge.org/trac/browser/trunk/
-        #    ALACMagicCookieDescription.txt
-
-        assert atom.name == b"alac"
-
-        ok, data = atom.read(fileobj)
-        if not ok:
-            raise ASEntryError("truncated %s atom" % atom.name)
-
-        try:
-            version, flags, data = parse_full_atom(data)
-        except ValueError as e:
-            raise ASEntryError(e)
-
-        if version != 0:
-            raise ASEntryError("Unsupported version %d" % version)
-
-        fileobj = cBytesIO(data)
-        r = BitReader(fileobj)
-
-        try:
-            # for some files the AudioSampleEntry values default to 44100/2chan
-            # and the real info is in the alac cookie, so prefer it
-            r.skip(32)  # frameLength
-            compatibleVersion = r.bits(8)
-            if compatibleVersion != 0:
-                return
-            self.sample_size = r.bits(8)
-            r.skip(8 + 8 + 8)
-            self.channels = r.bits(8)
-            r.skip(16 + 32)
-            self.bitrate = r.bits(32)
-            self.sample_rate = r.bits(32)
-        except BitReaderError as e:
-            raise ASEntryError(e)
-
-    def _parse_esds(self, esds, fileobj):
-        assert esds.name == b"esds"
-
-        ok, data = esds.read(fileobj)
-        if not ok:
-            raise ASEntryError("truncated %s atom" % esds.name)
-
-        try:
-            version, flags, data = parse_full_atom(data)
-        except ValueError as e:
-            raise ASEntryError(e)
-
-        if version != 0:
-            raise ASEntryError("Unsupported version %d" % version)
-
-        fileobj = cBytesIO(data)
-        r = BitReader(fileobj)
-
-        try:
-            tag = r.bits(8)
-            if tag != ES_Descriptor.TAG:
-                raise ASEntryError("unexpected descriptor: %d" % tag)
-            assert r.is_aligned()
-        except BitReaderError as e:
-            raise ASEntryError(e)
-
-        try:
-            decSpecificInfo = ES_Descriptor.parse(fileobj)
-        except DescriptorError as e:
-            raise ASEntryError(e)
-        dec_conf_desc = decSpecificInfo.decConfigDescr
-
-        self.bitrate = dec_conf_desc.avgBitrate
-        self.codec += dec_conf_desc.codec_param
-        self.codec_description = dec_conf_desc.codec_desc
-
-        decSpecificInfo = dec_conf_desc.decSpecificInfo
-        if decSpecificInfo is not None:
-            if decSpecificInfo.channels != 0:
-                self.channels = decSpecificInfo.channels
-
-            if decSpecificInfo.sample_rate != 0:
-                self.sample_rate = decSpecificInfo.sample_rate
-
-
-class DescriptorError(Exception):
-    pass
-
-
-class BaseDescriptor(object):
-
-    TAG = None
-
-    @classmethod
-    def _parse_desc_length_file(cls, fileobj):
-        """May raise ValueError"""
-
-        value = 0
-        for i in xrange(4):
-            try:
-                b = cdata.uint8(fileobj.read(1))
-            except cdata.error as e:
-                raise ValueError(e)
-            value = (value << 7) | (b & 0x7f)
-            if not b >> 7:
-                break
-        else:
-            raise ValueError("invalid descriptor length")
-
-        return value
-
-    @classmethod
-    def parse(cls, fileobj):
-        """Returns a parsed instance of the called type.
-        The file position is right after the descriptor after this returns.
-
-        Raises DescriptorError
-        """
-
-        try:
-            length = cls._parse_desc_length_file(fileobj)
-        except ValueError as e:
-            raise DescriptorError(e)
-        pos = fileobj.tell()
-        instance = cls(fileobj, length)
-        left = length - (fileobj.tell() - pos)
-        if left < 0:
-            raise DescriptorError("descriptor parsing read too much data")
-        fileobj.seek(left, 1)
-        return instance
-
-
-class ES_Descriptor(BaseDescriptor):
-
-    TAG = 0x3
-
-    def __init__(self, fileobj, length):
-        """Raises DescriptorError"""
-
-        r = BitReader(fileobj)
-        try:
-            self.ES_ID = r.bits(16)
-            self.streamDependenceFlag = r.bits(1)
-            self.URL_Flag = r.bits(1)
-            self.OCRstreamFlag = r.bits(1)
-            self.streamPriority = r.bits(5)
-            if self.streamDependenceFlag:
-                self.dependsOn_ES_ID = r.bits(16)
-            if self.URL_Flag:
-                URLlength = r.bits(8)
-                self.URLstring = r.bytes(URLlength)
-            if self.OCRstreamFlag:
-                self.OCR_ES_Id = r.bits(16)
-
-            tag = r.bits(8)
-        except BitReaderError as e:
-            raise DescriptorError(e)
-
-        if tag != DecoderConfigDescriptor.TAG:
-            raise DescriptorError("unexpected DecoderConfigDescrTag %d" % tag)
-
-        assert r.is_aligned()
-        self.decConfigDescr = DecoderConfigDescriptor.parse(fileobj)
-
-
-class DecoderConfigDescriptor(BaseDescriptor):
-
-    TAG = 0x4
-
-    decSpecificInfo = None
-    """A DecoderSpecificInfo, optional"""
-
-    def __init__(self, fileobj, length):
-        """Raises DescriptorError"""
-
-        r = BitReader(fileobj)
-
-        try:
-            self.objectTypeIndication = r.bits(8)
-            self.streamType = r.bits(6)
-            self.upStream = r.bits(1)
-            self.reserved = r.bits(1)
-            self.bufferSizeDB = r.bits(24)
-            self.maxBitrate = r.bits(32)
-            self.avgBitrate = r.bits(32)
-
-            if (self.objectTypeIndication, self.streamType) != (0x40, 0x5):
-                return
-
-            # all from here is optional
-            if length * 8 == r.get_position():
-                return
-
-            tag = r.bits(8)
-        except BitReaderError as e:
-            raise DescriptorError(e)
-
-        if tag == DecoderSpecificInfo.TAG:
-            assert r.is_aligned()
-            self.decSpecificInfo = DecoderSpecificInfo.parse(fileobj)
-
-    @property
-    def codec_param(self):
-        """string"""
-
-        param = u".%X" % self.objectTypeIndication
-        info = self.decSpecificInfo
-        if info is not None:
-            param += u".%d" % info.audioObjectType
-        return param
-
-    @property
-    def codec_desc(self):
-        """string or None"""
-
-        info = self.decSpecificInfo
-        desc = None
-        if info is not None:
-            desc = info.description
-        return desc
-
-
-class DecoderSpecificInfo(BaseDescriptor):
-
-    TAG = 0x5
-
-    _TYPE_NAMES = [
-        None, "AAC MAIN", "AAC LC", "AAC SSR", "AAC LTP", "SBR",
-        "AAC scalable", "TwinVQ", "CELP", "HVXC", None, None, "TTSI",
-        "Main synthetic", "Wavetable synthesis", "General MIDI",
-        "Algorithmic Synthesis and Audio FX", "ER AAC LC", None, "ER AAC LTP",
-        "ER AAC scalable", "ER Twin VQ", "ER BSAC", "ER AAC LD", "ER CELP",
-        "ER HVXC", "ER HILN", "ER Parametric", "SSC", "PS", "MPEG Surround",
-        None, "Layer-1", "Layer-2", "Layer-3", "DST", "ALS", "SLS",
-        "SLS non-core", "ER AAC ELD", "SMR Simple", "SMR Main", "USAC",
-        "SAOC", "LD MPEG Surround", "USAC"
-    ]
-
-    _FREQS = [
-        96000, 88200, 64000, 48000, 44100, 32000, 24000, 22050, 16000,
-        12000, 11025, 8000, 7350,
-    ]
-
-    @property
-    def description(self):
-        """string or None if unknown"""
-
-        name = None
-        try:
-            name = self._TYPE_NAMES[self.audioObjectType]
-        except IndexError:
-            pass
-        if name is None:
-            return
-        if self.sbrPresentFlag == 1:
-            name += "+SBR"
-        if self.psPresentFlag == 1:
-            name += "+PS"
-        return text_type(name)
-
-    @property
-    def sample_rate(self):
-        """0 means unknown"""
-
-        if self.sbrPresentFlag == 1:
-            return self.extensionSamplingFrequency
-        elif self.sbrPresentFlag == 0:
-            return self.samplingFrequency
-        else:
-            # these are all types that support SBR
-            aot_can_sbr = (1, 2, 3, 4, 6, 17, 19, 20, 22)
-            if self.audioObjectType not in aot_can_sbr:
-                return self.samplingFrequency
-            # there shouldn't be SBR for > 48KHz
-            if self.samplingFrequency > 24000:
-                return self.samplingFrequency
-            # either samplingFrequency or samplingFrequency * 2
-            return 0
-
-    @property
-    def channels(self):
-        """channel count or 0 for unknown"""
-
-        # from ProgramConfigElement()
-        if hasattr(self, "pce_channels"):
-            return self.pce_channels
-
-        conf = getattr(
-            self, "extensionChannelConfiguration", self.channelConfiguration)
-
-        if conf == 1:
-            if self.psPresentFlag == -1:
-                return 0
-            elif self.psPresentFlag == 1:
-                return 2
-            else:
-                return 1
-        elif conf == 7:
-            return 8
-        elif conf > 7:
-            return 0
-        else:
-            return conf
-
-    def _get_audio_object_type(self, r):
-        """Raises BitReaderError"""
-
-        audioObjectType = r.bits(5)
-        if audioObjectType == 31:
-            audioObjectTypeExt = r.bits(6)
-            audioObjectType = 32 + audioObjectTypeExt
-        return audioObjectType
-
-    def _get_sampling_freq(self, r):
-        """Raises BitReaderError"""
-
-        samplingFrequencyIndex = r.bits(4)
-        if samplingFrequencyIndex == 0xf:
-            samplingFrequency = r.bits(24)
-        else:
-            try:
-                samplingFrequency = self._FREQS[samplingFrequencyIndex]
-            except IndexError:
-                samplingFrequency = 0
-        return samplingFrequency
-
-    def __init__(self, fileobj, length):
-        """Raises DescriptorError"""
-
-        r = BitReader(fileobj)
-        try:
-            self._parse(r, length)
-        except BitReaderError as e:
-            raise DescriptorError(e)
-
-    def _parse(self, r, length):
-        """Raises BitReaderError"""
-
-        def bits_left():
-            return length * 8 - r.get_position()
-
-        self.audioObjectType = self._get_audio_object_type(r)
-        self.samplingFrequency = self._get_sampling_freq(r)
-        self.channelConfiguration = r.bits(4)
-
-        self.sbrPresentFlag = -1
-        self.psPresentFlag = -1
-        if self.audioObjectType in (5, 29):
-            self.extensionAudioObjectType = 5
-            self.sbrPresentFlag = 1
-            if self.audioObjectType == 29:
-                self.psPresentFlag = 1
-            self.extensionSamplingFrequency = self._get_sampling_freq(r)
-            self.audioObjectType = self._get_audio_object_type(r)
-            if self.audioObjectType == 22:
-                self.extensionChannelConfiguration = r.bits(4)
-        else:
-            self.extensionAudioObjectType = 0
-
-        if self.audioObjectType in (1, 2, 3, 4, 6, 7, 17, 19, 20, 21, 22, 23):
-            try:
-                GASpecificConfig(r, self)
-            except NotImplementedError:
-                # unsupported, (warn?)
-                return
-        else:
-            # unsupported
-            return
-
-        if self.audioObjectType in (
-                17, 19, 20, 21, 22, 23, 24, 25, 26, 27, 39):
-            epConfig = r.bits(2)
-            if epConfig in (2, 3):
-                # unsupported
-                return
-
-        if self.extensionAudioObjectType != 5 and bits_left() >= 16:
-            syncExtensionType = r.bits(11)
-            if syncExtensionType == 0x2b7:
-                self.extensionAudioObjectType = self._get_audio_object_type(r)
-
-                if self.extensionAudioObjectType == 5:
-                    self.sbrPresentFlag = r.bits(1)
-                    if self.sbrPresentFlag == 1:
-                        self.extensionSamplingFrequency = \
-                            self._get_sampling_freq(r)
-                        if bits_left() >= 12:
-                            syncExtensionType = r.bits(11)
-                            if syncExtensionType == 0x548:
-                                self.psPresentFlag = r.bits(1)
-
-                if self.extensionAudioObjectType == 22:
-                    self.sbrPresentFlag = r.bits(1)
-                    if self.sbrPresentFlag == 1:
-                        self.extensionSamplingFrequency = \
-                            self._get_sampling_freq(r)
-                    self.extensionChannelConfiguration = r.bits(4)
-
-
-def GASpecificConfig(r, info):
-    """Reads GASpecificConfig which is needed to get the data after that
-    (there is no length defined to skip it) and to read program_config_element
-    which can contain channel counts.
-
-    May raise BitReaderError on error or
-    NotImplementedError if some reserved data was set.
-    """
-
-    assert isinstance(info, DecoderSpecificInfo)
-
-    r.skip(1)  # frameLengthFlag
-    dependsOnCoreCoder = r.bits(1)
-    if dependsOnCoreCoder:
-        r.skip(14)
-    extensionFlag = r.bits(1)
-    if not info.channelConfiguration:
-        pce = ProgramConfigElement(r)
-        info.pce_channels = pce.channels
-    if info.audioObjectType == 6 or info.audioObjectType == 20:
-        r.skip(3)
-    if extensionFlag:
-        if info.audioObjectType == 22:
-            r.skip(5 + 11)
-        if info.audioObjectType in (17, 19, 20, 23):
-            r.skip(1 + 1 + 1)
-        extensionFlag3 = r.bits(1)
-        if extensionFlag3 != 0:
-            raise NotImplementedError("extensionFlag3 set")
diff --git a/resources/lib/libraries/mutagen/mp4/_atom.py b/resources/lib/libraries/mutagen/mp4/_atom.py
deleted file mode 100644
index f73eb556..00000000
--- a/resources/lib/libraries/mutagen/mp4/_atom.py
+++ /dev/null
@@ -1,194 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-import struct
-
-from mutagen._compat import PY2
-
-# This is not an exhaustive list of container atoms, but just the
-# ones this module needs to peek inside.
-_CONTAINERS = [b"moov", b"udta", b"trak", b"mdia", b"meta", b"ilst",
-               b"stbl", b"minf", b"moof", b"traf"]
-_SKIP_SIZE = {b"meta": 4}
-
-
-class AtomError(Exception):
-    pass
-
-
-class Atom(object):
-    """An individual atom.
-
-    Attributes:
-    children -- list child atoms (or None for non-container atoms)
-    length -- length of this atom, including length and name
-    datalength = -- length of this atom without length, name
-    name -- four byte name of the atom, as a str
-    offset -- location in the constructor-given fileobj of this atom
-
-    This structure should only be used internally by Mutagen.
-    """
-
-    children = None
-
-    def __init__(self, fileobj, level=0):
-        """May raise AtomError"""
-
-        self.offset = fileobj.tell()
-        try:
-            self.length, self.name = struct.unpack(">I4s", fileobj.read(8))
-        except struct.error:
-            raise AtomError("truncated data")
-        self._dataoffset = self.offset + 8
-        if self.length == 1:
-            try:
-                self.length, = struct.unpack(">Q", fileobj.read(8))
-            except struct.error:
-                raise AtomError("truncated data")
-            self._dataoffset += 8
-            if self.length < 16:
-                raise AtomError(
-                    "64 bit atom length can only be 16 and higher")
-        elif self.length == 0:
-            if level != 0:
-                raise AtomError(
-                    "only a top-level atom can have zero length")
-            # Only the last atom is supposed to have a zero-length, meaning it
-            # extends to the end of file.
-            fileobj.seek(0, 2)
-            self.length = fileobj.tell() - self.offset
-            fileobj.seek(self.offset + 8, 0)
-        elif self.length < 8:
-            raise AtomError(
-                "atom length can only be 0, 1 or 8 and higher")
-
-        if self.name in _CONTAINERS:
-            self.children = []
-            fileobj.seek(_SKIP_SIZE.get(self.name, 0), 1)
-            while fileobj.tell() < self.offset + self.length:
-                self.children.append(Atom(fileobj, level + 1))
-        else:
-            fileobj.seek(self.offset + self.length, 0)
-
-    @property
-    def datalength(self):
-        return self.length - (self._dataoffset - self.offset)
-
-    def read(self, fileobj):
-        """Return if all data could be read and the atom payload"""
-
-        fileobj.seek(self._dataoffset, 0)
-        data = fileobj.read(self.datalength)
-        return len(data) == self.datalength, data
-
-    @staticmethod
-    def render(name, data):
-        """Render raw atom data."""
-        # this raises OverflowError if Py_ssize_t can't handle the atom data
-        size = len(data) + 8
-        if size <= 0xFFFFFFFF:
-            return struct.pack(">I4s", size, name) + data
-        else:
-            return struct.pack(">I4sQ", 1, name, size + 8) + data
-
-    def findall(self, name, recursive=False):
-        """Recursively find all child atoms by specified name."""
-        if self.children is not None:
-            for child in self.children:
-                if child.name == name:
-                    yield child
-                if recursive:
-                    for atom in child.findall(name, True):
-                        yield atom
-
-    def __getitem__(self, remaining):
-        """Look up a child atom, potentially recursively.
-
-        e.g. atom['udta', 'meta'] => <Atom name='meta' ...>
-        """
-        if not remaining:
-            return self
-        elif self.children is None:
-            raise KeyError("%r is not a container" % self.name)
-        for child in self.children:
-            if child.name == remaining[0]:
-                return child[remaining[1:]]
-        else:
-            raise KeyError("%r not found" % remaining[0])
-
-    def __repr__(self):
-        cls = self.__class__.__name__
-        if self.children is None:
-            return "<%s name=%r length=%r offset=%r>" % (
-                cls, self.name, self.length, self.offset)
-        else:
-            children = "\n".join([" " + line for child in self.children
-                                  for line in repr(child).splitlines()])
-            return "<%s name=%r length=%r offset=%r\n%s>" % (
-                cls, self.name, self.length, self.offset, children)
-
-
-class Atoms(object):
-    """Root atoms in a given file.
-
-    Attributes:
-    atoms -- a list of top-level atoms as Atom objects
-
-    This structure should only be used internally by Mutagen.
-    """
-
-    def __init__(self, fileobj):
-        self.atoms = []
-        fileobj.seek(0, 2)
-        end = fileobj.tell()
-        fileobj.seek(0)
-        while fileobj.tell() + 8 <= end:
-            self.atoms.append(Atom(fileobj))
-
-    def path(self, *names):
-        """Look up and return the complete path of an atom.
-
-        For example, atoms.path('moov', 'udta', 'meta') will return a
-        list of three atoms, corresponding to the moov, udta, and meta
-        atoms.
-        """
-
-        path = [self]
-        for name in names:
-            path.append(path[-1][name, ])
-        return path[1:]
-
-    def __contains__(self, names):
-        try:
-            self[names]
-        except KeyError:
-            return False
-        return True
-
-    def __getitem__(self, names):
-        """Look up a child atom.
-
-        'names' may be a list of atoms (['moov', 'udta']) or a string
-        specifying the complete path ('moov.udta').
-        """
-
-        if PY2:
-            if isinstance(names, basestring):
-                names = names.split(b".")
-        else:
-            if isinstance(names, bytes):
-                names = names.split(b".")
-
-        for child in self.atoms:
-            if child.name == names[0]:
-                return child[names[1:]]
-        else:
-            raise KeyError("%r not found" % names[0])
-
-    def __repr__(self):
-        return "\n".join([repr(child) for child in self.atoms])
diff --git a/resources/lib/libraries/mutagen/mp4/_util.py b/resources/lib/libraries/mutagen/mp4/_util.py
deleted file mode 100644
index 9583334a..00000000
--- a/resources/lib/libraries/mutagen/mp4/_util.py
+++ /dev/null
@@ -1,21 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (C) 2014 Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-from mutagen._util import cdata
-
-
-def parse_full_atom(data):
-    """Some atoms are versioned. Split them up in (version, flags, payload).
-    Can raise ValueError.
-    """
-
-    if len(data) < 4:
-        raise ValueError("not enough data")
-
-    version = ord(data[0:1])
-    flags = cdata.uint_be(b"\x00" + data[1:4])
-    return version, flags, data[4:]
diff --git a/resources/lib/libraries/mutagen/musepack.py b/resources/lib/libraries/mutagen/musepack.py
deleted file mode 100644
index 7880958b..00000000
--- a/resources/lib/libraries/mutagen/musepack.py
+++ /dev/null
@@ -1,270 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Lukas Lalinsky
-# Copyright (C) 2012  Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Musepack audio streams with APEv2 tags.
-
-Musepack is an audio format originally based on the MPEG-1 Layer-2
-algorithms. Stream versions 4 through 7 are supported.
-
-For more information, see http://www.musepack.net/.
-"""
-
-__all__ = ["Musepack", "Open", "delete"]
-
-import struct
-
-from ._compat import endswith, xrange
-from mutagen import StreamInfo
-from mutagen.apev2 import APEv2File, error, delete
-from mutagen.id3 import BitPaddedInt
-from mutagen._util import cdata
-
-
-class MusepackHeaderError(error):
-    pass
-
-
-RATES = [44100, 48000, 37800, 32000]
-
-
-def _parse_sv8_int(fileobj, limit=9):
-    """Reads (max limit) bytes from fileobj until the MSB is zero.
-    All 7 LSB will be merged to a big endian uint.
-
-    Raises ValueError in case not MSB is zero, or EOFError in
-    case the file ended before limit is reached.
-
-    Returns (parsed number, number of bytes read)
-    """
-
-    num = 0
-    for i in xrange(limit):
-        c = fileobj.read(1)
-        if len(c) != 1:
-            raise EOFError
-        c = bytearray(c)
-        num = (num << 7) | (c[0] & 0x7F)
-        if not c[0] & 0x80:
-            return num, i + 1
-    if limit > 0:
-        raise ValueError
-    return 0, 0
-
-
-def _calc_sv8_gain(gain):
-    # 64.82 taken from mpcdec
-    return 64.82 - gain / 256.0
-
-
-def _calc_sv8_peak(peak):
-    return (10 ** (peak / (256.0 * 20.0)) / 65535.0)
-
-
-class MusepackInfo(StreamInfo):
-    """Musepack stream information.
-
-    Attributes:
-
-    * channels -- number of audio channels
-    * length -- file length in seconds, as a float
-    * sample_rate -- audio sampling rate in Hz
-    * bitrate -- audio bitrate, in bits per second
-    * version -- Musepack stream version
-
-    Optional Attributes:
-
-    * title_gain, title_peak -- Replay Gain and peak data for this song
-    * album_gain, album_peak -- Replay Gain and peak data for this album
-
-    These attributes are only available in stream version 7/8. The
-    gains are a float, +/- some dB. The peaks are a percentage [0..1] of
-    the maximum amplitude. This means to get a number comparable to
-    VorbisGain, you must multiply the peak by 2.
-    """
-
-    def __init__(self, fileobj):
-        header = fileobj.read(4)
-        if len(header) != 4:
-            raise MusepackHeaderError("not a Musepack file")
-
-        # Skip ID3v2 tags
-        if header[:3] == b"ID3":
-            header = fileobj.read(6)
-            if len(header) != 6:
-                raise MusepackHeaderError("not a Musepack file")
-            size = 10 + BitPaddedInt(header[2:6])
-            fileobj.seek(size)
-            header = fileobj.read(4)
-            if len(header) != 4:
-                raise MusepackHeaderError("not a Musepack file")
-
-        if header.startswith(b"MPCK"):
-            self.__parse_sv8(fileobj)
-        else:
-            self.__parse_sv467(fileobj)
-
-        if not self.bitrate and self.length != 0:
-            fileobj.seek(0, 2)
-            self.bitrate = int(round(fileobj.tell() * 8 / self.length))
-
-    def __parse_sv8(self, fileobj):
-        # SV8 http://trac.musepack.net/trac/wiki/SV8Specification
-
-        key_size = 2
-        mandatory_packets = [b"SH", b"RG"]
-
-        def check_frame_key(key):
-            if ((len(frame_type) != key_size) or
-                    (not b'AA' <= frame_type <= b'ZZ')):
-                raise MusepackHeaderError("Invalid frame key.")
-
-        frame_type = fileobj.read(key_size)
-        check_frame_key(frame_type)
-
-        while frame_type not in (b"AP", b"SE") and mandatory_packets:
-            try:
-                frame_size, slen = _parse_sv8_int(fileobj)
-            except (EOFError, ValueError):
-                raise MusepackHeaderError("Invalid packet size.")
-            data_size = frame_size - key_size - slen
-            # packets can be at maximum data_size big and are padded with zeros
-
-            if frame_type == b"SH":
-                mandatory_packets.remove(frame_type)
-                self.__parse_stream_header(fileobj, data_size)
-            elif frame_type == b"RG":
-                mandatory_packets.remove(frame_type)
-                self.__parse_replaygain_packet(fileobj, data_size)
-            else:
-                fileobj.seek(data_size, 1)
-
-            frame_type = fileobj.read(key_size)
-            check_frame_key(frame_type)
-
-        if mandatory_packets:
-            raise MusepackHeaderError("Missing mandatory packets: %s." %
-                                      ", ".join(map(repr, mandatory_packets)))
-
-        self.length = float(self.samples) / self.sample_rate
-        self.bitrate = 0
-
-    def __parse_stream_header(self, fileobj, data_size):
-        # skip CRC
-        fileobj.seek(4, 1)
-        remaining_size = data_size - 4
-
-        try:
-            self.version = bytearray(fileobj.read(1))[0]
-        except TypeError:
-            raise MusepackHeaderError("SH packet ended unexpectedly.")
-
-        remaining_size -= 1
-
-        try:
-            samples, l1 = _parse_sv8_int(fileobj)
-            samples_skip, l2 = _parse_sv8_int(fileobj)
-        except (EOFError, ValueError):
-            raise MusepackHeaderError(
-                "SH packet: Invalid sample counts.")
-
-        self.samples = samples - samples_skip
-        remaining_size -= l1 + l2
-
-        data = fileobj.read(remaining_size)
-        if len(data) != remaining_size:
-            raise MusepackHeaderError("SH packet ended unexpectedly.")
-        self.sample_rate = RATES[bytearray(data)[0] >> 5]
-        self.channels = (bytearray(data)[1] >> 4) + 1
-
-    def __parse_replaygain_packet(self, fileobj, data_size):
-        data = fileobj.read(data_size)
-        if data_size < 9:
-            raise MusepackHeaderError("Invalid RG packet size.")
-        if len(data) != data_size:
-            raise MusepackHeaderError("RG packet ended unexpectedly.")
-        title_gain = cdata.short_be(data[1:3])
-        title_peak = cdata.short_be(data[3:5])
-        album_gain = cdata.short_be(data[5:7])
-        album_peak = cdata.short_be(data[7:9])
-        if title_gain:
-            self.title_gain = _calc_sv8_gain(title_gain)
-        if title_peak:
-            self.title_peak = _calc_sv8_peak(title_peak)
-        if album_gain:
-            self.album_gain = _calc_sv8_gain(album_gain)
-        if album_peak:
-            self.album_peak = _calc_sv8_peak(album_peak)
-
-    def __parse_sv467(self, fileobj):
-        fileobj.seek(-4, 1)
-        header = fileobj.read(32)
-        if len(header) != 32:
-            raise MusepackHeaderError("not a Musepack file")
-
-        # SV7
-        if header.startswith(b"MP+"):
-            self.version = bytearray(header)[3] & 0xF
-            if self.version < 7:
-                raise MusepackHeaderError("not a Musepack file")
-            frames = cdata.uint_le(header[4:8])
-            flags = cdata.uint_le(header[8:12])
-
-            self.title_peak, self.title_gain = struct.unpack(
-                "<Hh", header[12:16])
-            self.album_peak, self.album_gain = struct.unpack(
-                "<Hh", header[16:20])
-            self.title_gain /= 100.0
-            self.album_gain /= 100.0
-            self.title_peak /= 65535.0
-            self.album_peak /= 65535.0
-
-            self.sample_rate = RATES[(flags >> 16) & 0x0003]
-            self.bitrate = 0
-        # SV4-SV6
-        else:
-            header_dword = cdata.uint_le(header[0:4])
-            self.version = (header_dword >> 11) & 0x03FF
-            if self.version < 4 or self.version > 6:
-                raise MusepackHeaderError("not a Musepack file")
-            self.bitrate = (header_dword >> 23) & 0x01FF
-            self.sample_rate = 44100
-            if self.version >= 5:
-                frames = cdata.uint_le(header[4:8])
-            else:
-                frames = cdata.ushort_le(header[6:8])
-            if self.version < 6:
-                frames -= 1
-        self.channels = 2
-        self.length = float(frames * 1152 - 576) / self.sample_rate
-
-    def pprint(self):
-        rg_data = []
-        if hasattr(self, "title_gain"):
-            rg_data.append(u"%+0.2f (title)" % self.title_gain)
-        if hasattr(self, "album_gain"):
-            rg_data.append(u"%+0.2f (album)" % self.album_gain)
-        rg_data = (rg_data and ", Gain: " + ", ".join(rg_data)) or ""
-
-        return u"Musepack SV%d, %.2f seconds, %d Hz, %d bps%s" % (
-            self.version, self.length, self.sample_rate, self.bitrate, rg_data)
-
-
-class Musepack(APEv2File):
-    _Info = MusepackInfo
-    _mimes = ["audio/x-musepack", "audio/x-mpc"]
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        filename = filename.lower()
-
-        return (header.startswith(b"MP+") + header.startswith(b"MPCK") +
-                endswith(filename, b".mpc"))
-
-
-Open = Musepack
diff --git a/resources/lib/libraries/mutagen/ogg.py b/resources/lib/libraries/mutagen/ogg.py
deleted file mode 100644
index 9961a966..00000000
--- a/resources/lib/libraries/mutagen/ogg.py
+++ /dev/null
@@ -1,548 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write Ogg bitstreams and pages.
-
-This module reads and writes a subset of the Ogg bitstream format
-version 0. It does *not* read or write Ogg Vorbis files! For that,
-you should use mutagen.oggvorbis.
-
-This implementation is based on the RFC 3533 standard found at
-http://www.xiph.org/ogg/doc/rfc3533.txt.
-"""
-
-import struct
-import sys
-import zlib
-
-from mutagen import FileType
-from mutagen._util import cdata, resize_bytes, MutagenError
-from ._compat import cBytesIO, reraise, chr_, izip, xrange
-
-
-class error(IOError, MutagenError):
-    """Ogg stream parsing errors."""
-
-    pass
-
-
-class OggPage(object):
-    """A single Ogg page (not necessarily a single encoded packet).
-
-    A page is a header of 26 bytes, followed by the length of the
-    data, followed by the data.
-
-    The constructor is givin a file-like object pointing to the start
-    of an Ogg page. After the constructor is finished it is pointing
-    to the start of the next page.
-
-    Attributes:
-
-    * version -- stream structure version (currently always 0)
-    * position -- absolute stream position (default -1)
-    * serial -- logical stream serial number (default 0)
-    * sequence -- page sequence number within logical stream (default 0)
-    * offset -- offset this page was read from (default None)
-    * complete -- if the last packet on this page is complete (default True)
-    * packets -- list of raw packet data (default [])
-
-    Note that if 'complete' is false, the next page's 'continued'
-    property must be true (so set both when constructing pages).
-
-    If a file-like object is supplied to the constructor, the above
-    attributes will be filled in based on it.
-    """
-
-    version = 0
-    __type_flags = 0
-    position = 0
-    serial = 0
-    sequence = 0
-    offset = None
-    complete = True
-
-    def __init__(self, fileobj=None):
-        self.packets = []
-
-        if fileobj is None:
-            return
-
-        self.offset = fileobj.tell()
-
-        header = fileobj.read(27)
-        if len(header) == 0:
-            raise EOFError
-
-        try:
-            (oggs, self.version, self.__type_flags,
-             self.position, self.serial, self.sequence,
-             crc, segments) = struct.unpack("<4sBBqIIiB", header)
-        except struct.error:
-            raise error("unable to read full header; got %r" % header)
-
-        if oggs != b"OggS":
-            raise error("read %r, expected %r, at 0x%x" % (
-                oggs, b"OggS", fileobj.tell() - 27))
-
-        if self.version != 0:
-            raise error("version %r unsupported" % self.version)
-
-        total = 0
-        lacings = []
-        lacing_bytes = fileobj.read(segments)
-        if len(lacing_bytes) != segments:
-            raise error("unable to read %r lacing bytes" % segments)
-        for c in bytearray(lacing_bytes):
-            total += c
-            if c < 255:
-                lacings.append(total)
-                total = 0
-        if total:
-            lacings.append(total)
-            self.complete = False
-
-        self.packets = [fileobj.read(l) for l in lacings]
-        if [len(p) for p in self.packets] != lacings:
-            raise error("unable to read full data")
-
-    def __eq__(self, other):
-        """Two Ogg pages are the same if they write the same data."""
-        try:
-            return (self.write() == other.write())
-        except AttributeError:
-            return False
-
-    __hash__ = object.__hash__
-
-    def __repr__(self):
-        attrs = ['version', 'position', 'serial', 'sequence', 'offset',
-                 'complete', 'continued', 'first', 'last']
-        values = ["%s=%r" % (attr, getattr(self, attr)) for attr in attrs]
-        return "<%s %s, %d bytes in %d packets>" % (
-            type(self).__name__, " ".join(values), sum(map(len, self.packets)),
-            len(self.packets))
-
-    def write(self):
-        """Return a string encoding of the page header and data.
-
-        A ValueError is raised if the data is too big to fit in a
-        single page.
-        """
-
-        data = [
-            struct.pack("<4sBBqIIi", b"OggS", self.version, self.__type_flags,
-                        self.position, self.serial, self.sequence, 0)
-        ]
-
-        lacing_data = []
-        for datum in self.packets:
-            quot, rem = divmod(len(datum), 255)
-            lacing_data.append(b"\xff" * quot + chr_(rem))
-        lacing_data = b"".join(lacing_data)
-        if not self.complete and lacing_data.endswith(b"\x00"):
-            lacing_data = lacing_data[:-1]
-        data.append(chr_(len(lacing_data)))
-        data.append(lacing_data)
-        data.extend(self.packets)
-        data = b"".join(data)
-
-        # Python's CRC is swapped relative to Ogg's needs.
-        # crc32 returns uint prior to py2.6 on some platforms, so force uint
-        crc = (~zlib.crc32(data.translate(cdata.bitswap), -1)) & 0xffffffff
-        # Although we're using to_uint_be, this actually makes the CRC
-        # a proper le integer, since Python's CRC is byteswapped.
-        crc = cdata.to_uint_be(crc).translate(cdata.bitswap)
-        data = data[:22] + crc + data[26:]
-        return data
-
-    @property
-    def size(self):
-        """Total frame size."""
-
-        size = 27  # Initial header size
-        for datum in self.packets:
-            quot, rem = divmod(len(datum), 255)
-            size += quot + 1
-        if not self.complete and rem == 0:
-            # Packet contains a multiple of 255 bytes and is not
-            # terminated, so we don't have a \x00 at the end.
-            size -= 1
-        size += sum(map(len, self.packets))
-        return size
-
-    def __set_flag(self, bit, val):
-        mask = 1 << bit
-        if val:
-            self.__type_flags |= mask
-        else:
-            self.__type_flags &= ~mask
-
-    continued = property(
-        lambda self: cdata.test_bit(self.__type_flags, 0),
-        lambda self, v: self.__set_flag(0, v),
-        doc="The first packet is continued from the previous page.")
-
-    first = property(
-        lambda self: cdata.test_bit(self.__type_flags, 1),
-        lambda self, v: self.__set_flag(1, v),
-        doc="This is the first page of a logical bitstream.")
-
-    last = property(
-        lambda self: cdata.test_bit(self.__type_flags, 2),
-        lambda self, v: self.__set_flag(2, v),
-        doc="This is the last page of a logical bitstream.")
-
-    @staticmethod
-    def renumber(fileobj, serial, start):
-        """Renumber pages belonging to a specified logical stream.
-
-        fileobj must be opened with mode r+b or w+b.
-
-        Starting at page number 'start', renumber all pages belonging
-        to logical stream 'serial'. Other pages will be ignored.
-
-        fileobj must point to the start of a valid Ogg page; any
-        occuring after it and part of the specified logical stream
-        will be numbered. No adjustment will be made to the data in
-        the pages nor the granule position; only the page number, and
-        so also the CRC.
-
-        If an error occurs (e.g. non-Ogg data is found), fileobj will
-        be left pointing to the place in the stream the error occured,
-        but the invalid data will be left intact (since this function
-        does not change the total file size).
-        """
-
-        number = start
-        while True:
-            try:
-                page = OggPage(fileobj)
-            except EOFError:
-                break
-            else:
-                if page.serial != serial:
-                    # Wrong stream, skip this page.
-                    continue
-                # Changing the number can't change the page size,
-                # so seeking back based on the current size is safe.
-                fileobj.seek(-page.size, 1)
-            page.sequence = number
-            fileobj.write(page.write())
-            fileobj.seek(page.offset + page.size, 0)
-            number += 1
-
-    @staticmethod
-    def to_packets(pages, strict=False):
-        """Construct a list of packet data from a list of Ogg pages.
-
-        If strict is true, the first page must start a new packet,
-        and the last page must end the last packet.
-        """
-
-        serial = pages[0].serial
-        sequence = pages[0].sequence
-        packets = []
-
-        if strict:
-            if pages[0].continued:
-                raise ValueError("first packet is continued")
-            if not pages[-1].complete:
-                raise ValueError("last packet does not complete")
-        elif pages and pages[0].continued:
-            packets.append([b""])
-
-        for page in pages:
-            if serial != page.serial:
-                raise ValueError("invalid serial number in %r" % page)
-            elif sequence != page.sequence:
-                raise ValueError("bad sequence number in %r" % page)
-            else:
-                sequence += 1
-
-            if page.continued:
-                packets[-1].append(page.packets[0])
-            else:
-                packets.append([page.packets[0]])
-            packets.extend([p] for p in page.packets[1:])
-
-        return [b"".join(p) for p in packets]
-
-    @classmethod
-    def _from_packets_try_preserve(cls, packets, old_pages):
-        """Like from_packets but in case the size and number of the packets
-        is the same as in the given pages the layout of the pages will
-        be copied (the page size and number will match).
-
-        If the packets don't match this behaves like::
-
-            OggPage.from_packets(packets, sequence=old_pages[0].sequence)
-        """
-
-        old_packets = cls.to_packets(old_pages)
-
-        if [len(p) for p in packets] != [len(p) for p in old_packets]:
-            # doesn't match, fall back
-            return cls.from_packets(packets, old_pages[0].sequence)
-
-        new_data = b"".join(packets)
-        new_pages = []
-        for old in old_pages:
-            new = OggPage()
-            new.sequence = old.sequence
-            new.complete = old.complete
-            new.continued = old.continued
-            new.position = old.position
-            for p in old.packets:
-                data, new_data = new_data[:len(p)], new_data[len(p):]
-                new.packets.append(data)
-            new_pages.append(new)
-        assert not new_data
-
-        return new_pages
-
-    @staticmethod
-    def from_packets(packets, sequence=0, default_size=4096,
-                     wiggle_room=2048):
-        """Construct a list of Ogg pages from a list of packet data.
-
-        The algorithm will generate pages of approximately
-        default_size in size (rounded down to the nearest multiple of
-        255). However, it will also allow pages to increase to
-        approximately default_size + wiggle_room if allowing the
-        wiggle room would finish a packet (only one packet will be
-        finished in this way per page; if the next packet would fit
-        into the wiggle room, it still starts on a new page).
-
-        This method reduces packet fragmentation when packet sizes are
-        slightly larger than the default page size, while still
-        ensuring most pages are of the average size.
-
-        Pages are numbered started at 'sequence'; other information is
-        uninitialized.
-        """
-
-        chunk_size = (default_size // 255) * 255
-
-        pages = []
-
-        page = OggPage()
-        page.sequence = sequence
-
-        for packet in packets:
-            page.packets.append(b"")
-            while packet:
-                data, packet = packet[:chunk_size], packet[chunk_size:]
-                if page.size < default_size and len(page.packets) < 255:
-                    page.packets[-1] += data
-                else:
-                    # If we've put any packet data into this page yet,
-                    # we need to mark it incomplete. However, we can
-                    # also have just started this packet on an already
-                    # full page, in which case, just start the new
-                    # page with this packet.
-                    if page.packets[-1]:
-                        page.complete = False
-                        if len(page.packets) == 1:
-                            page.position = -1
-                    else:
-                        page.packets.pop(-1)
-                    pages.append(page)
-                    page = OggPage()
-                    page.continued = not pages[-1].complete
-                    page.sequence = pages[-1].sequence + 1
-                    page.packets.append(data)
-
-                if len(packet) < wiggle_room:
-                    page.packets[-1] += packet
-                    packet = b""
-
-        if page.packets:
-            pages.append(page)
-
-        return pages
-
-    @classmethod
-    def replace(cls, fileobj, old_pages, new_pages):
-        """Replace old_pages with new_pages within fileobj.
-
-        old_pages must have come from reading fileobj originally.
-        new_pages are assumed to have the 'same' data as old_pages,
-        and so the serial and sequence numbers will be copied, as will
-        the flags for the first and last pages.
-
-        fileobj will be resized and pages renumbered as necessary. As
-        such, it must be opened r+b or w+b.
-        """
-
-        if not len(old_pages) or not len(new_pages):
-            raise ValueError("empty pages list not allowed")
-
-        # Number the new pages starting from the first old page.
-        first = old_pages[0].sequence
-        for page, seq in izip(new_pages,
-                              xrange(first, first + len(new_pages))):
-            page.sequence = seq
-            page.serial = old_pages[0].serial
-
-        new_pages[0].first = old_pages[0].first
-        new_pages[0].last = old_pages[0].last
-        new_pages[0].continued = old_pages[0].continued
-
-        new_pages[-1].first = old_pages[-1].first
-        new_pages[-1].last = old_pages[-1].last
-        new_pages[-1].complete = old_pages[-1].complete
-        if not new_pages[-1].complete and len(new_pages[-1].packets) == 1:
-            new_pages[-1].position = -1
-
-        new_data = [cls.write(p) for p in new_pages]
-
-        # Add dummy data or merge the remaining data together so multiple
-        # new pages replace an old one
-        pages_diff = len(old_pages) - len(new_data)
-        if pages_diff > 0:
-            new_data.extend([b""] * pages_diff)
-        elif pages_diff < 0:
-            new_data[pages_diff - 1:] = [b"".join(new_data[pages_diff - 1:])]
-
-        # Replace pages one by one. If the sizes match no resize happens.
-        offset_adjust = 0
-        new_data_end = None
-        assert len(old_pages) == len(new_data)
-        for old_page, data in izip(old_pages, new_data):
-            offset = old_page.offset + offset_adjust
-            data_size = len(data)
-            resize_bytes(fileobj, old_page.size, data_size, offset)
-            fileobj.seek(offset, 0)
-            fileobj.write(data)
-            new_data_end = offset + data_size
-            offset_adjust += (data_size - old_page.size)
-
-        # Finally, if there's any discrepency in length, we need to
-        # renumber the pages for the logical stream.
-        if len(old_pages) != len(new_pages):
-            fileobj.seek(new_data_end, 0)
-            serial = new_pages[-1].serial
-            sequence = new_pages[-1].sequence + 1
-            cls.renumber(fileobj, serial, sequence)
-
-    @staticmethod
-    def find_last(fileobj, serial):
-        """Find the last page of the stream 'serial'.
-
-        If the file is not multiplexed this function is fast. If it is,
-        it must read the whole the stream.
-
-        This finds the last page in the actual file object, or the last
-        page in the stream (with eos set), whichever comes first.
-        """
-
-        # For non-muxed streams, look at the last page.
-        try:
-            fileobj.seek(-256 * 256, 2)
-        except IOError:
-            # The file is less than 64k in length.
-            fileobj.seek(0)
-        data = fileobj.read()
-        try:
-            index = data.rindex(b"OggS")
-        except ValueError:
-            raise error("unable to find final Ogg header")
-        bytesobj = cBytesIO(data[index:])
-        best_page = None
-        try:
-            page = OggPage(bytesobj)
-        except error:
-            pass
-        else:
-            if page.serial == serial:
-                if page.last:
-                    return page
-                else:
-                    best_page = page
-            else:
-                best_page = None
-
-        # The stream is muxed, so use the slow way.
-        fileobj.seek(0)
-        try:
-            page = OggPage(fileobj)
-            while not page.last:
-                page = OggPage(fileobj)
-                while page.serial != serial:
-                    page = OggPage(fileobj)
-                best_page = page
-            return page
-        except error:
-            return best_page
-        except EOFError:
-            return best_page
-
-
-class OggFileType(FileType):
-    """An generic Ogg file."""
-
-    _Info = None
-    _Tags = None
-    _Error = None
-    _mimes = ["application/ogg", "application/x-ogg"]
-
-    def load(self, filename):
-        """Load file information from a filename."""
-
-        self.filename = filename
-        with open(filename, "rb") as fileobj:
-            try:
-                self.info = self._Info(fileobj)
-                self.tags = self._Tags(fileobj, self.info)
-                self.info._post_tags(fileobj)
-            except error as e:
-                reraise(self._Error, e, sys.exc_info()[2])
-            except EOFError:
-                raise self._Error("no appropriate stream found")
-
-    def delete(self, filename=None):
-        """Remove tags from a file.
-
-        If no filename is given, the one most recently loaded is used.
-        """
-
-        if filename is None:
-            filename = self.filename
-
-        self.tags.clear()
-        # TODO: we should delegate the deletion to the subclass and not through
-        # _inject.
-        with open(filename, "rb+") as fileobj:
-            try:
-                self.tags._inject(fileobj, lambda x: 0)
-            except error as e:
-                reraise(self._Error, e, sys.exc_info()[2])
-            except EOFError:
-                raise self._Error("no appropriate stream found")
-
-    def add_tags(self):
-        raise self._Error
-
-    def save(self, filename=None, padding=None):
-        """Save a tag to a file.
-
-        If no filename is given, the one most recently loaded is used.
-        """
-
-        if filename is None:
-            filename = self.filename
-        fileobj = open(filename, "rb+")
-        try:
-            try:
-                self.tags._inject(fileobj, padding)
-            except error as e:
-                reraise(self._Error, e, sys.exc_info()[2])
-            except EOFError:
-                raise self._Error("no appropriate stream found")
-        finally:
-            fileobj.close()
diff --git a/resources/lib/libraries/mutagen/oggflac.py b/resources/lib/libraries/mutagen/oggflac.py
deleted file mode 100644
index b86226ca..00000000
--- a/resources/lib/libraries/mutagen/oggflac.py
+++ /dev/null
@@ -1,161 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write Ogg FLAC comments.
-
-This module handles FLAC files wrapped in an Ogg bitstream. The first
-FLAC stream found is used. For 'naked' FLACs, see mutagen.flac.
-
-This module is based off the specification at
-http://flac.sourceforge.net/ogg_mapping.html.
-"""
-
-__all__ = ["OggFLAC", "Open", "delete"]
-
-import struct
-
-from ._compat import cBytesIO
-
-from mutagen import StreamInfo
-from mutagen.flac import StreamInfo as FLACStreamInfo, error as FLACError
-from mutagen._vorbis import VCommentDict
-from mutagen.ogg import OggPage, OggFileType, error as OggError
-
-
-class error(OggError):
-    pass
-
-
-class OggFLACHeaderError(error):
-    pass
-
-
-class OggFLACStreamInfo(StreamInfo):
-    """Ogg FLAC stream info."""
-
-    length = 0
-    """File length in seconds, as a float"""
-
-    channels = 0
-    """Number of channels"""
-
-    sample_rate = 0
-    """Sample rate in Hz"""
-
-    def __init__(self, fileobj):
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"\x7FFLAC"):
-            page = OggPage(fileobj)
-        major, minor, self.packets, flac = struct.unpack(
-            ">BBH4s", page.packets[0][5:13])
-        if flac != b"fLaC":
-            raise OggFLACHeaderError("invalid FLAC marker (%r)" % flac)
-        elif (major, minor) != (1, 0):
-            raise OggFLACHeaderError(
-                "unknown mapping version: %d.%d" % (major, minor))
-        self.serial = page.serial
-
-        # Skip over the block header.
-        stringobj = cBytesIO(page.packets[0][17:])
-
-        try:
-            flac_info = FLACStreamInfo(stringobj)
-        except FLACError as e:
-            raise OggFLACHeaderError(e)
-
-        for attr in ["min_blocksize", "max_blocksize", "sample_rate",
-                     "channels", "bits_per_sample", "total_samples", "length"]:
-            setattr(self, attr, getattr(flac_info, attr))
-
-    def _post_tags(self, fileobj):
-        if self.length:
-            return
-        page = OggPage.find_last(fileobj, self.serial)
-        self.length = page.position / float(self.sample_rate)
-
-    def pprint(self):
-        return u"Ogg FLAC, %.2f seconds, %d Hz" % (
-            self.length, self.sample_rate)
-
-
-class OggFLACVComment(VCommentDict):
-
-    def __init__(self, fileobj, info):
-        # data should be pointing at the start of an Ogg page, after
-        # the first FLAC page.
-        pages = []
-        complete = False
-        while not complete:
-            page = OggPage(fileobj)
-            if page.serial == info.serial:
-                pages.append(page)
-                complete = page.complete or (len(page.packets) > 1)
-        comment = cBytesIO(OggPage.to_packets(pages)[0][4:])
-        super(OggFLACVComment, self).__init__(comment, framing=False)
-
-    def _inject(self, fileobj, padding_func):
-        """Write tag data into the FLAC Vorbis comment packet/page."""
-
-        # Ogg FLAC has no convenient data marker like Vorbis, but the
-        # second packet - and second page - must be the comment data.
-        fileobj.seek(0)
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"\x7FFLAC"):
-            page = OggPage(fileobj)
-
-        first_page = page
-        while not (page.sequence == 1 and page.serial == first_page.serial):
-            page = OggPage(fileobj)
-
-        old_pages = [page]
-        while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1):
-            page = OggPage(fileobj)
-            if page.serial == first_page.serial:
-                old_pages.append(page)
-
-        packets = OggPage.to_packets(old_pages, strict=False)
-
-        # Set the new comment block.
-        data = self.write(framing=False)
-        data = packets[0][:1] + struct.pack(">I", len(data))[-3:] + data
-        packets[0] = data
-
-        new_pages = OggPage.from_packets(packets, old_pages[0].sequence)
-        OggPage.replace(fileobj, old_pages, new_pages)
-
-
-class OggFLAC(OggFileType):
-    """An Ogg FLAC file."""
-
-    _Info = OggFLACStreamInfo
-    _Tags = OggFLACVComment
-    _Error = OggFLACHeaderError
-    _mimes = ["audio/x-oggflac"]
-
-    info = None
-    """A `OggFLACStreamInfo`"""
-
-    tags = None
-    """A `VCommentDict`"""
-
-    def save(self, filename=None):
-        return super(OggFLAC, self).save(filename)
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return (header.startswith(b"OggS") * (
-            (b"FLAC" in header) + (b"fLaC" in header)))
-
-
-Open = OggFLAC
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    OggFLAC(filename).delete()
diff --git a/resources/lib/libraries/mutagen/oggopus.py b/resources/lib/libraries/mutagen/oggopus.py
deleted file mode 100644
index 7154e479..00000000
--- a/resources/lib/libraries/mutagen/oggopus.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2012, 2013  Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write Ogg Opus comments.
-
-This module handles Opus files wrapped in an Ogg bitstream. The
-first Opus stream found is used.
-
-Based on http://tools.ietf.org/html/draft-terriberry-oggopus-01
-"""
-
-__all__ = ["OggOpus", "Open", "delete"]
-
-import struct
-
-from mutagen import StreamInfo
-from mutagen._compat import BytesIO
-from mutagen._util import get_size
-from mutagen._tags import PaddingInfo
-from mutagen._vorbis import VCommentDict
-from mutagen.ogg import OggPage, OggFileType, error as OggError
-
-
-class error(OggError):
-    pass
-
-
-class OggOpusHeaderError(error):
-    pass
-
-
-class OggOpusInfo(StreamInfo):
-    """Ogg Opus stream information."""
-
-    length = 0
-    """File length in seconds, as a float"""
-
-    channels = 0
-    """Number of channels"""
-
-    def __init__(self, fileobj):
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"OpusHead"):
-            page = OggPage(fileobj)
-
-        self.serial = page.serial
-
-        if not page.first:
-            raise OggOpusHeaderError(
-                "page has ID header, but doesn't start a stream")
-
-        (version, self.channels, pre_skip, orig_sample_rate, output_gain,
-         channel_map) = struct.unpack("<BBHIhB", page.packets[0][8:19])
-
-        self.__pre_skip = pre_skip
-
-        # only the higher 4 bits change on incombatible changes
-        major = version >> 4
-        if major != 0:
-            raise OggOpusHeaderError("version %r unsupported" % major)
-
-    def _post_tags(self, fileobj):
-        page = OggPage.find_last(fileobj, self.serial)
-        self.length = (page.position - self.__pre_skip) / float(48000)
-
-    def pprint(self):
-        return u"Ogg Opus, %.2f seconds" % (self.length)
-
-
-class OggOpusVComment(VCommentDict):
-    """Opus comments embedded in an Ogg bitstream."""
-
-    def __get_comment_pages(self, fileobj, info):
-        # find the first tags page with the right serial
-        page = OggPage(fileobj)
-        while ((info.serial != page.serial) or
-                not page.packets[0].startswith(b"OpusTags")):
-            page = OggPage(fileobj)
-
-        # get all comment pages
-        pages = [page]
-        while not (pages[-1].complete or len(pages[-1].packets) > 1):
-            page = OggPage(fileobj)
-            if page.serial == pages[0].serial:
-                pages.append(page)
-
-        return pages
-
-    def __init__(self, fileobj, info):
-        pages = self.__get_comment_pages(fileobj, info)
-        data = OggPage.to_packets(pages)[0][8:]  # Strip OpusTags
-        fileobj = BytesIO(data)
-        super(OggOpusVComment, self).__init__(fileobj, framing=False)
-        self._padding = len(data) - self._size
-
-        # in case the LSB of the first byte after v-comment is 1, preserve the
-        # following data
-        padding_flag = fileobj.read(1)
-        if padding_flag and ord(padding_flag) & 0x1:
-            self._pad_data = padding_flag + fileobj.read()
-            self._padding = 0  # we have to preserve, so no padding
-        else:
-            self._pad_data = b""
-
-    def _inject(self, fileobj, padding_func):
-        fileobj.seek(0)
-        info = OggOpusInfo(fileobj)
-        old_pages = self.__get_comment_pages(fileobj, info)
-
-        packets = OggPage.to_packets(old_pages)
-        vcomment_data = b"OpusTags" + self.write(framing=False)
-
-        if self._pad_data:
-            # if we have padding data to preserver we can't add more padding
-            # as long as we don't know the structure of what follows
-            packets[0] = vcomment_data + self._pad_data
-        else:
-            content_size = get_size(fileobj) - len(packets[0])  # approx
-            padding_left = len(packets[0]) - len(vcomment_data)
-            info = PaddingInfo(padding_left, content_size)
-            new_padding = info._get_padding(padding_func)
-            packets[0] = vcomment_data + b"\x00" * new_padding
-
-        new_pages = OggPage._from_packets_try_preserve(packets, old_pages)
-        OggPage.replace(fileobj, old_pages, new_pages)
-
-
-class OggOpus(OggFileType):
-    """An Ogg Opus file."""
-
-    _Info = OggOpusInfo
-    _Tags = OggOpusVComment
-    _Error = OggOpusHeaderError
-    _mimes = ["audio/ogg", "audio/ogg; codecs=opus"]
-
-    info = None
-    """A `OggOpusInfo`"""
-
-    tags = None
-    """A `VCommentDict`"""
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return (header.startswith(b"OggS") * (b"OpusHead" in header))
-
-
-Open = OggOpus
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    OggOpus(filename).delete()
diff --git a/resources/lib/libraries/mutagen/oggspeex.py b/resources/lib/libraries/mutagen/oggspeex.py
deleted file mode 100644
index 9b16930b..00000000
--- a/resources/lib/libraries/mutagen/oggspeex.py
+++ /dev/null
@@ -1,154 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2006 Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write Ogg Speex comments.
-
-This module handles Speex files wrapped in an Ogg bitstream. The
-first Speex stream found is used.
-
-Read more about Ogg Speex at http://www.speex.org/. This module is
-based on the specification at http://www.speex.org/manual2/node7.html
-and clarifications after personal communication with Jean-Marc,
-http://lists.xiph.org/pipermail/speex-dev/2006-July/004676.html.
-"""
-
-__all__ = ["OggSpeex", "Open", "delete"]
-
-from mutagen import StreamInfo
-from mutagen._vorbis import VCommentDict
-from mutagen.ogg import OggPage, OggFileType, error as OggError
-from mutagen._util import cdata, get_size
-from mutagen._tags import PaddingInfo
-
-
-class error(OggError):
-    pass
-
-
-class OggSpeexHeaderError(error):
-    pass
-
-
-class OggSpeexInfo(StreamInfo):
-    """Ogg Speex stream information."""
-
-    length = 0
-    """file length in seconds, as a float"""
-
-    channels = 0
-    """number of channels"""
-
-    bitrate = 0
-    """nominal bitrate in bits per second.
-
-    The reference encoder does not set the bitrate; in this case,
-    the bitrate will be 0.
-    """
-
-    def __init__(self, fileobj):
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"Speex   "):
-            page = OggPage(fileobj)
-        if not page.first:
-            raise OggSpeexHeaderError(
-                "page has ID header, but doesn't start a stream")
-        self.sample_rate = cdata.uint_le(page.packets[0][36:40])
-        self.channels = cdata.uint_le(page.packets[0][48:52])
-        self.bitrate = max(0, cdata.int_le(page.packets[0][52:56]))
-        self.serial = page.serial
-
-    def _post_tags(self, fileobj):
-        page = OggPage.find_last(fileobj, self.serial)
-        self.length = page.position / float(self.sample_rate)
-
-    def pprint(self):
-        return u"Ogg Speex, %.2f seconds" % self.length
-
-
-class OggSpeexVComment(VCommentDict):
-    """Speex comments embedded in an Ogg bitstream."""
-
-    def __init__(self, fileobj, info):
-        pages = []
-        complete = False
-        while not complete:
-            page = OggPage(fileobj)
-            if page.serial == info.serial:
-                pages.append(page)
-                complete = page.complete or (len(page.packets) > 1)
-        data = OggPage.to_packets(pages)[0]
-        super(OggSpeexVComment, self).__init__(data, framing=False)
-        self._padding = len(data) - self._size
-
-    def _inject(self, fileobj, padding_func):
-        """Write tag data into the Speex comment packet/page."""
-
-        fileobj.seek(0)
-
-        # Find the first header page, with the stream info.
-        # Use it to get the serial number.
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"Speex   "):
-            page = OggPage(fileobj)
-
-        # Look for the next page with that serial number, it'll start
-        # the comment packet.
-        serial = page.serial
-        page = OggPage(fileobj)
-        while page.serial != serial:
-            page = OggPage(fileobj)
-
-        # Then find all the pages with the comment packet.
-        old_pages = [page]
-        while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1):
-            page = OggPage(fileobj)
-            if page.serial == old_pages[0].serial:
-                old_pages.append(page)
-
-        packets = OggPage.to_packets(old_pages, strict=False)
-
-        content_size = get_size(fileobj) - len(packets[0])  # approx
-        vcomment_data = self.write(framing=False)
-        padding_left = len(packets[0]) - len(vcomment_data)
-
-        info = PaddingInfo(padding_left, content_size)
-        new_padding = info._get_padding(padding_func)
-
-        # Set the new comment packet.
-        packets[0] = vcomment_data + b"\x00" * new_padding
-
-        new_pages = OggPage._from_packets_try_preserve(packets, old_pages)
-        OggPage.replace(fileobj, old_pages, new_pages)
-
-
-class OggSpeex(OggFileType):
-    """An Ogg Speex file."""
-
-    _Info = OggSpeexInfo
-    _Tags = OggSpeexVComment
-    _Error = OggSpeexHeaderError
-    _mimes = ["audio/x-speex"]
-
-    info = None
-    """A `OggSpeexInfo`"""
-
-    tags = None
-    """A `VCommentDict`"""
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return (header.startswith(b"OggS") * (b"Speex   " in header))
-
-
-Open = OggSpeex
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    OggSpeex(filename).delete()
diff --git a/resources/lib/libraries/mutagen/oggtheora.py b/resources/lib/libraries/mutagen/oggtheora.py
deleted file mode 100644
index 122e7d4b..00000000
--- a/resources/lib/libraries/mutagen/oggtheora.py
+++ /dev/null
@@ -1,148 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2006 Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write Ogg Theora comments.
-
-This module handles Theora files wrapped in an Ogg bitstream. The
-first Theora stream found is used.
-
-Based on the specification at http://theora.org/doc/Theora_I_spec.pdf.
-"""
-
-__all__ = ["OggTheora", "Open", "delete"]
-
-import struct
-
-from mutagen import StreamInfo
-from mutagen._vorbis import VCommentDict
-from mutagen._util import cdata, get_size
-from mutagen._tags import PaddingInfo
-from mutagen.ogg import OggPage, OggFileType, error as OggError
-
-
-class error(OggError):
-    pass
-
-
-class OggTheoraHeaderError(error):
-    pass
-
-
-class OggTheoraInfo(StreamInfo):
-    """Ogg Theora stream information."""
-
-    length = 0
-    """File length in seconds, as a float"""
-
-    fps = 0
-    """Video frames per second, as a float"""
-
-    bitrate = 0
-    """Bitrate in bps (int)"""
-
-    def __init__(self, fileobj):
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"\x80theora"):
-            page = OggPage(fileobj)
-        if not page.first:
-            raise OggTheoraHeaderError(
-                "page has ID header, but doesn't start a stream")
-        data = page.packets[0]
-        vmaj, vmin = struct.unpack("2B", data[7:9])
-        if (vmaj, vmin) != (3, 2):
-            raise OggTheoraHeaderError(
-                "found Theora version %d.%d != 3.2" % (vmaj, vmin))
-        fps_num, fps_den = struct.unpack(">2I", data[22:30])
-        self.fps = fps_num / float(fps_den)
-        self.bitrate = cdata.uint_be(b"\x00" + data[37:40])
-        self.granule_shift = (cdata.ushort_be(data[40:42]) >> 5) & 0x1F
-        self.serial = page.serial
-
-    def _post_tags(self, fileobj):
-        page = OggPage.find_last(fileobj, self.serial)
-        position = page.position
-        mask = (1 << self.granule_shift) - 1
-        frames = (position >> self.granule_shift) + (position & mask)
-        self.length = frames / float(self.fps)
-
-    def pprint(self):
-        return u"Ogg Theora, %.2f seconds, %d bps" % (self.length,
-                                                      self.bitrate)
-
-
-class OggTheoraCommentDict(VCommentDict):
-    """Theora comments embedded in an Ogg bitstream."""
-
-    def __init__(self, fileobj, info):
-        pages = []
-        complete = False
-        while not complete:
-            page = OggPage(fileobj)
-            if page.serial == info.serial:
-                pages.append(page)
-                complete = page.complete or (len(page.packets) > 1)
-        data = OggPage.to_packets(pages)[0][7:]
-        super(OggTheoraCommentDict, self).__init__(data, framing=False)
-        self._padding = len(data) - self._size
-
-    def _inject(self, fileobj, padding_func):
-        """Write tag data into the Theora comment packet/page."""
-
-        fileobj.seek(0)
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"\x81theora"):
-            page = OggPage(fileobj)
-
-        old_pages = [page]
-        while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1):
-            page = OggPage(fileobj)
-            if page.serial == old_pages[0].serial:
-                old_pages.append(page)
-
-        packets = OggPage.to_packets(old_pages, strict=False)
-
-        content_size = get_size(fileobj) - len(packets[0])  # approx
-        vcomment_data = b"\x81theora" + self.write(framing=False)
-        padding_left = len(packets[0]) - len(vcomment_data)
-
-        info = PaddingInfo(padding_left, content_size)
-        new_padding = info._get_padding(padding_func)
-
-        packets[0] = vcomment_data + b"\x00" * new_padding
-
-        new_pages = OggPage._from_packets_try_preserve(packets, old_pages)
-        OggPage.replace(fileobj, old_pages, new_pages)
-
-
-class OggTheora(OggFileType):
-    """An Ogg Theora file."""
-
-    _Info = OggTheoraInfo
-    _Tags = OggTheoraCommentDict
-    _Error = OggTheoraHeaderError
-    _mimes = ["video/x-theora"]
-
-    info = None
-    """A `OggTheoraInfo`"""
-
-    tags = None
-    """A `VCommentDict`"""
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return (header.startswith(b"OggS") *
-                ((b"\x80theora" in header) + (b"\x81theora" in header)) * 2)
-
-
-Open = OggTheora
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    OggTheora(filename).delete()
diff --git a/resources/lib/libraries/mutagen/oggvorbis.py b/resources/lib/libraries/mutagen/oggvorbis.py
deleted file mode 100644
index b058a0c1..00000000
--- a/resources/lib/libraries/mutagen/oggvorbis.py
+++ /dev/null
@@ -1,159 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2006 Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""Read and write Ogg Vorbis comments.
-
-This module handles Vorbis files wrapped in an Ogg bitstream. The
-first Vorbis stream found is used.
-
-Read more about Ogg Vorbis at http://vorbis.com/. This module is based
-on the specification at http://www.xiph.org/vorbis/doc/Vorbis_I_spec.html.
-"""
-
-__all__ = ["OggVorbis", "Open", "delete"]
-
-import struct
-
-from mutagen import StreamInfo
-from mutagen._vorbis import VCommentDict
-from mutagen._util import get_size
-from mutagen._tags import PaddingInfo
-from mutagen.ogg import OggPage, OggFileType, error as OggError
-
-
-class error(OggError):
-    pass
-
-
-class OggVorbisHeaderError(error):
-    pass
-
-
-class OggVorbisInfo(StreamInfo):
-    """Ogg Vorbis stream information."""
-
-    length = 0
-    """File length in seconds, as a float"""
-
-    channels = 0
-    """Number of channels"""
-
-    bitrate = 0
-    """Nominal ('average') bitrate in bits per second, as an int"""
-
-    sample_rate = 0
-    """Sample rate in Hz"""
-
-    def __init__(self, fileobj):
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"\x01vorbis"):
-            page = OggPage(fileobj)
-        if not page.first:
-            raise OggVorbisHeaderError(
-                "page has ID header, but doesn't start a stream")
-        (self.channels, self.sample_rate, max_bitrate, nominal_bitrate,
-         min_bitrate) = struct.unpack("<B4i", page.packets[0][11:28])
-        self.serial = page.serial
-
-        max_bitrate = max(0, max_bitrate)
-        min_bitrate = max(0, min_bitrate)
-        nominal_bitrate = max(0, nominal_bitrate)
-
-        if nominal_bitrate == 0:
-            self.bitrate = (max_bitrate + min_bitrate) // 2
-        elif max_bitrate and max_bitrate < nominal_bitrate:
-            # If the max bitrate is less than the nominal, we know
-            # the nominal is wrong.
-            self.bitrate = max_bitrate
-        elif min_bitrate > nominal_bitrate:
-            self.bitrate = min_bitrate
-        else:
-            self.bitrate = nominal_bitrate
-
-    def _post_tags(self, fileobj):
-        page = OggPage.find_last(fileobj, self.serial)
-        self.length = page.position / float(self.sample_rate)
-
-    def pprint(self):
-        return u"Ogg Vorbis, %.2f seconds, %d bps" % (
-            self.length, self.bitrate)
-
-
-class OggVCommentDict(VCommentDict):
-    """Vorbis comments embedded in an Ogg bitstream."""
-
-    def __init__(self, fileobj, info):
-        pages = []
-        complete = False
-        while not complete:
-            page = OggPage(fileobj)
-            if page.serial == info.serial:
-                pages.append(page)
-                complete = page.complete or (len(page.packets) > 1)
-        data = OggPage.to_packets(pages)[0][7:]  # Strip off "\x03vorbis".
-        super(OggVCommentDict, self).__init__(data)
-        self._padding = len(data) - self._size
-
-    def _inject(self, fileobj, padding_func):
-        """Write tag data into the Vorbis comment packet/page."""
-
-        # Find the old pages in the file; we'll need to remove them,
-        # plus grab any stray setup packet data out of them.
-        fileobj.seek(0)
-        page = OggPage(fileobj)
-        while not page.packets[0].startswith(b"\x03vorbis"):
-            page = OggPage(fileobj)
-
-        old_pages = [page]
-        while not (old_pages[-1].complete or len(old_pages[-1].packets) > 1):
-            page = OggPage(fileobj)
-            if page.serial == old_pages[0].serial:
-                old_pages.append(page)
-
-        packets = OggPage.to_packets(old_pages, strict=False)
-
-        content_size = get_size(fileobj) - len(packets[0])  # approx
-        vcomment_data = b"\x03vorbis" + self.write()
-        padding_left = len(packets[0]) - len(vcomment_data)
-
-        info = PaddingInfo(padding_left, content_size)
-        new_padding = info._get_padding(padding_func)
-
-        # Set the new comment packet.
-        packets[0] = vcomment_data + b"\x00" * new_padding
-
-        new_pages = OggPage._from_packets_try_preserve(packets, old_pages)
-        OggPage.replace(fileobj, old_pages, new_pages)
-
-
-class OggVorbis(OggFileType):
-    """An Ogg Vorbis file."""
-
-    _Info = OggVorbisInfo
-    _Tags = OggVCommentDict
-    _Error = OggVorbisHeaderError
-    _mimes = ["audio/vorbis", "audio/x-vorbis"]
-
-    info = None
-    """A `OggVorbisInfo`"""
-
-    tags = None
-    """A `VCommentDict`"""
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return (header.startswith(b"OggS") * (b"\x01vorbis" in header))
-
-
-Open = OggVorbis
-
-
-def delete(filename):
-    """Remove tags from a file."""
-
-    OggVorbis(filename).delete()
diff --git a/resources/lib/libraries/mutagen/optimfrog.py b/resources/lib/libraries/mutagen/optimfrog.py
deleted file mode 100644
index 0d85a818..00000000
--- a/resources/lib/libraries/mutagen/optimfrog.py
+++ /dev/null
@@ -1,74 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Lukas Lalinsky
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""OptimFROG audio streams with APEv2 tags.
-
-OptimFROG is a lossless audio compression program. Its main goal is to
-reduce at maximum the size of audio files, while permitting bit
-identical restoration for all input. It is similar with the ZIP
-compression, but it is highly specialized to compress audio data.
-
-Only versions 4.5 and higher are supported.
-
-For more information, see http://www.losslessaudio.org/
-"""
-
-__all__ = ["OptimFROG", "Open", "delete"]
-
-import struct
-
-from ._compat import endswith
-from mutagen import StreamInfo
-from mutagen.apev2 import APEv2File, error, delete
-
-
-class OptimFROGHeaderError(error):
-    pass
-
-
-class OptimFROGInfo(StreamInfo):
-    """OptimFROG stream information.
-
-    Attributes:
-
-    * channels - number of audio channels
-    * length - file length in seconds, as a float
-    * sample_rate - audio sampling rate in Hz
-    """
-
-    def __init__(self, fileobj):
-        header = fileobj.read(76)
-        if (len(header) != 76 or not header.startswith(b"OFR ") or
-                struct.unpack("<I", header[4:8])[0] not in [12, 15]):
-            raise OptimFROGHeaderError("not an OptimFROG file")
-        (total_samples, total_samples_high, sample_type, self.channels,
-         self.sample_rate) = struct.unpack("<IHBBI", header[8:20])
-        total_samples += total_samples_high << 32
-        self.channels += 1
-        if self.sample_rate:
-            self.length = float(total_samples) / (self.channels *
-                                                  self.sample_rate)
-        else:
-            self.length = 0.0
-
-    def pprint(self):
-        return u"OptimFROG, %.2f seconds, %d Hz" % (self.length,
-                                                    self.sample_rate)
-
-
-class OptimFROG(APEv2File):
-    _Info = OptimFROGInfo
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        filename = filename.lower()
-
-        return (header.startswith(b"OFR") + endswith(filename, b".ofr") +
-                endswith(filename, b".ofs"))
-
-Open = OptimFROG
diff --git a/resources/lib/libraries/mutagen/trueaudio.py b/resources/lib/libraries/mutagen/trueaudio.py
deleted file mode 100644
index 1c8d56c4..00000000
--- a/resources/lib/libraries/mutagen/trueaudio.py
+++ /dev/null
@@ -1,84 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright (C) 2006  Joe Wreschnig
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of version 2 of the GNU General Public License as
-# published by the Free Software Foundation.
-
-"""True Audio audio stream information and tags.
-
-True Audio is a lossless format designed for real-time encoding and
-decoding. This module is based on the documentation at
-http://www.true-audio.com/TTA_Lossless_Audio_Codec\_-_Format_Description
-
-True Audio files use ID3 tags.
-"""
-
-__all__ = ["TrueAudio", "Open", "delete", "EasyTrueAudio"]
-
-from ._compat import endswith
-from mutagen import StreamInfo
-from mutagen.id3 import ID3FileType, delete
-from mutagen._util import cdata, MutagenError
-
-
-class error(RuntimeError, MutagenError):
-    pass
-
-
-class TrueAudioHeaderError(error, IOError):
-    pass
-
-
-class TrueAudioInfo(StreamInfo):
-    """True Audio stream information.
-
-    Attributes:
-
-    * length - audio length, in seconds
-    * sample_rate - audio sample rate, in Hz
-    """
-
-    def __init__(self, fileobj, offset):
-        fileobj.seek(offset or 0)
-        header = fileobj.read(18)
-        if len(header) != 18 or not header.startswith(b"TTA"):
-            raise TrueAudioHeaderError("TTA header not found")
-        self.sample_rate = cdata.int_le(header[10:14])
-        samples = cdata.uint_le(header[14:18])
-        self.length = float(samples) / self.sample_rate
-
-    def pprint(self):
-        return u"True Audio, %.2f seconds, %d Hz." % (
-            self.length, self.sample_rate)
-
-
-class TrueAudio(ID3FileType):
-    """A True Audio file.
-
-    :ivar info: :class:`TrueAudioInfo`
-    :ivar tags: :class:`ID3 <mutagen.id3.ID3>`
-    """
-
-    _Info = TrueAudioInfo
-    _mimes = ["audio/x-tta"]
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return (header.startswith(b"ID3") + header.startswith(b"TTA") +
-                endswith(filename.lower(), b".tta") * 2)
-
-
-Open = TrueAudio
-
-
-class EasyTrueAudio(TrueAudio):
-    """Like MP3, but uses EasyID3 for tags.
-
-    :ivar info: :class:`TrueAudioInfo`
-    :ivar tags: :class:`EasyID3 <mutagen.easyid3.EasyID3>`
-    """
-
-    from mutagen.easyid3 import EasyID3 as ID3
-    ID3 = ID3
diff --git a/resources/lib/libraries/mutagen/wavpack.py b/resources/lib/libraries/mutagen/wavpack.py
deleted file mode 100644
index 80710f6d..00000000
--- a/resources/lib/libraries/mutagen/wavpack.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# -*- coding: utf-8 -*-
-
-# Copyright 2006 Joe Wreschnig
-#           2014 Christoph Reiter
-#
-# This program is free software; you can redistribute it and/or modify
-# it under the terms of the GNU General Public License version 2 as
-# published by the Free Software Foundation.
-
-"""WavPack reading and writing.
-
-WavPack is a lossless format that uses APEv2 tags. Read
-
-* http://www.wavpack.com/
-* http://www.wavpack.com/file_format.txt
-
-for more information.
-"""
-
-__all__ = ["WavPack", "Open", "delete"]
-
-from mutagen import StreamInfo
-from mutagen.apev2 import APEv2File, error, delete
-from mutagen._util import cdata
-
-
-class WavPackHeaderError(error):
-    pass
-
-RATES = [6000, 8000, 9600, 11025, 12000, 16000, 22050, 24000, 32000, 44100,
-         48000, 64000, 88200, 96000, 192000]
-
-
-class _WavPackHeader(object):
-
-    def __init__(self, block_size, version, track_no, index_no, total_samples,
-                 block_index, block_samples, flags, crc):
-
-        self.block_size = block_size
-        self.version = version
-        self.track_no = track_no
-        self.index_no = index_no
-        self.total_samples = total_samples
-        self.block_index = block_index
-        self.block_samples = block_samples
-        self.flags = flags
-        self.crc = crc
-
-    @classmethod
-    def from_fileobj(cls, fileobj):
-        """A new _WavPackHeader or raises WavPackHeaderError"""
-
-        header = fileobj.read(32)
-        if len(header) != 32 or not header.startswith(b"wvpk"):
-            raise WavPackHeaderError("not a WavPack header: %r" % header)
-
-        block_size = cdata.uint_le(header[4:8])
-        version = cdata.ushort_le(header[8:10])
-        track_no = ord(header[10:11])
-        index_no = ord(header[11:12])
-        samples = cdata.uint_le(header[12:16])
-        if samples == 2 ** 32 - 1:
-            samples = -1
-        block_index = cdata.uint_le(header[16:20])
-        block_samples = cdata.uint_le(header[20:24])
-        flags = cdata.uint_le(header[24:28])
-        crc = cdata.uint_le(header[28:32])
-
-        return _WavPackHeader(block_size, version, track_no, index_no,
-                              samples, block_index, block_samples, flags, crc)
-
-
-class WavPackInfo(StreamInfo):
-    """WavPack stream information.
-
-    Attributes:
-
-    * channels - number of audio channels (1 or 2)
-    * length - file length in seconds, as a float
-    * sample_rate - audio sampling rate in Hz
-    * version - WavPack stream version
-    """
-
-    def __init__(self, fileobj):
-        try:
-            header = _WavPackHeader.from_fileobj(fileobj)
-        except WavPackHeaderError:
-            raise WavPackHeaderError("not a WavPack file")
-
-        self.version = header.version
-        self.channels = bool(header.flags & 4) or 2
-        self.sample_rate = RATES[(header.flags >> 23) & 0xF]
-
-        if header.total_samples == -1 or header.block_index != 0:
-            # TODO: we could make this faster by using the tag size
-            # and search backwards for the last block, then do
-            # last.block_index + last.block_samples - initial.block_index
-            samples = header.block_samples
-            while 1:
-                fileobj.seek(header.block_size - 32 + 8, 1)
-                try:
-                    header = _WavPackHeader.from_fileobj(fileobj)
-                except WavPackHeaderError:
-                    break
-                samples += header.block_samples
-        else:
-            samples = header.total_samples
-
-        self.length = float(samples) / self.sample_rate
-
-    def pprint(self):
-        return u"WavPack, %.2f seconds, %d Hz" % (self.length,
-                                                  self.sample_rate)
-
-
-class WavPack(APEv2File):
-    _Info = WavPackInfo
-    _mimes = ["audio/x-wavpack"]
-
-    @staticmethod
-    def score(filename, fileobj, header):
-        return header.startswith(b"wvpk") * 2
-
-
-Open = WavPack
diff --git a/service.py b/service.py
index 5844cb99..3292a122 100644
--- a/service.py
+++ b/service.py
@@ -15,9 +15,12 @@ import xbmcaddon
 
 __addon__ = xbmcaddon.Addon(id='plugin.video.emby')
 __base__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'resources', 'lib')).decode('utf-8')
+__libraries__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('path'), 'libraries')).decode('utf-8')
 __pcache__ = xbmc.translatePath(os.path.join(__addon__.getAddonInfo('profile'), 'emby')).decode('utf-8')
 __cache__ = xbmc.translatePath('special://temp/emby').decode('utf-8')
 
+sys.path.insert(0, __libraries__)
+
 if not xbmcvfs.exists(__pcache__ + '/'):
     from resources.lib.helper.utils import copytree
 

From 158a736360b6419fa20f0b48e2fb2cf16e83a992 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Wed, 30 Jan 2019 06:43:14 -0600
Subject: [PATCH 06/12] Update webservice with cherrypy

Fix playback issues that was causing Kodi to hang up
---
 libraries/backports/__init__.py               |    1 +
 libraries/backports/functools_lru_cache.py    |  184 ++
 libraries/cheroot/__init__.py                 |    6 +
 libraries/cheroot/__main__.py                 |    6 +
 libraries/cheroot/_compat.py                  |   66 +
 libraries/cheroot/cli.py                      |  233 ++
 libraries/cheroot/errors.py                   |   58 +
 libraries/cheroot/makefile.py                 |  387 +++
 libraries/cheroot/server.py                   | 2001 +++++++++++++++
 libraries/cheroot/ssl/__init__.py             |   51 +
 libraries/cheroot/ssl/builtin.py              |  162 ++
 libraries/cheroot/ssl/pyopenssl.py            |  267 ++
 libraries/cheroot/test/__init__.py            |    1 +
 libraries/cheroot/test/conftest.py            |   27 +
 libraries/cheroot/test/helper.py              |  169 ++
 libraries/cheroot/test/test.pem               |   38 +
 libraries/cheroot/test/test__compat.py        |   49 +
 libraries/cheroot/test/test_conn.py           |  897 +++++++
 libraries/cheroot/test/test_core.py           |  405 +++
 libraries/cheroot/test/test_server.py         |  193 ++
 libraries/cheroot/test/webtest.py             |  581 +++++
 libraries/cheroot/testing.py                  |  144 ++
 libraries/cheroot/workers/__init__.py         |    1 +
 libraries/cheroot/workers/threadpool.py       |  271 ++
 libraries/cheroot/wsgi.py                     |  423 ++++
 libraries/cherrypy/__init__.py                |  362 +++
 libraries/cherrypy/__main__.py                |    5 +
 libraries/cherrypy/_cpchecker.py              |  325 +++
 libraries/cherrypy/_cpcompat.py               |  162 ++
 libraries/cherrypy/_cpconfig.py               |  300 +++
 libraries/cherrypy/_cpdispatch.py             |  686 +++++
 libraries/cherrypy/_cperror.py                |  619 +++++
 libraries/cherrypy/_cplogging.py              |  482 ++++
 libraries/cherrypy/_cpmodpy.py                |  356 +++
 libraries/cherrypy/_cpnative_server.py        |  160 ++
 libraries/cherrypy/_cpreqbody.py              | 1000 ++++++++
 libraries/cherrypy/_cprequest.py              |  930 +++++++
 libraries/cherrypy/_cpserver.py               |  252 ++
 libraries/cherrypy/_cptools.py                |  509 ++++
 libraries/cherrypy/_cptree.py                 |  313 +++
 libraries/cherrypy/_cpwsgi.py                 |  467 ++++
 libraries/cherrypy/_cpwsgi_server.py          |  110 +
 libraries/cherrypy/_helper.py                 |  344 +++
 libraries/cherrypy/daemon.py                  |  107 +
 libraries/cherrypy/favicon.ico                |  Bin 0 -> 1406 bytes
 libraries/cherrypy/lib/__init__.py            |   96 +
 libraries/cherrypy/lib/auth_basic.py          |  120 +
 libraries/cherrypy/lib/auth_digest.py         |  464 ++++
 libraries/cherrypy/lib/caching.py             |  482 ++++
 libraries/cherrypy/lib/covercp.py             |  391 +++
 libraries/cherrypy/lib/cpstats.py             |  696 ++++++
 libraries/cherrypy/lib/cptools.py             |  640 +++++
 libraries/cherrypy/lib/encoding.py            |  436 ++++
 libraries/cherrypy/lib/gctools.py             |  218 ++
 libraries/cherrypy/lib/httputil.py            |  581 +++++
 libraries/cherrypy/lib/jsontools.py           |   88 +
 libraries/cherrypy/lib/locking.py             |   47 +
 libraries/cherrypy/lib/profiler.py            |  221 ++
 libraries/cherrypy/lib/reprconf.py            |  514 ++++
 libraries/cherrypy/lib/sessions.py            |  919 +++++++
 libraries/cherrypy/lib/static.py              |  390 +++
 libraries/cherrypy/lib/xmlrpcutil.py          |   61 +
 libraries/cherrypy/process/__init__.py        |   17 +
 libraries/cherrypy/process/plugins.py         |  752 ++++++
 libraries/cherrypy/process/servers.py         |  416 ++++
 libraries/cherrypy/process/win32.py           |  183 ++
 libraries/cherrypy/process/wspbus.py          |  590 +++++
 libraries/cherrypy/scaffold/__init__.py       |   63 +
 libraries/cherrypy/scaffold/apache-fcgi.conf  |   22 +
 libraries/cherrypy/scaffold/example.conf      |    3 +
 libraries/cherrypy/scaffold/site.conf         |   14 +
 .../static/made_with_cherrypy_small.png       |  Bin 0 -> 6347 bytes
 libraries/cherrypy/test/__init__.py           |   24 +
 libraries/cherrypy/test/_test_decorators.py   |   39 +
 libraries/cherrypy/test/_test_states_demo.py  |   69 +
 libraries/cherrypy/test/benchmark.py          |  425 ++++
 libraries/cherrypy/test/checkerdemo.py        |   49 +
 libraries/cherrypy/test/fastcgi.conf          |   18 +
 libraries/cherrypy/test/fcgi.conf             |   14 +
 libraries/cherrypy/test/helper.py             |  542 ++++
 libraries/cherrypy/test/logtest.py            |  228 ++
 libraries/cherrypy/test/modfastcgi.py         |  136 +
 libraries/cherrypy/test/modfcgid.py           |  124 +
 libraries/cherrypy/test/modpy.py              |  164 ++
 libraries/cherrypy/test/modwsgi.py            |  154 ++
 libraries/cherrypy/test/sessiondemo.py        |  161 ++
 libraries/cherrypy/test/static/404.html       |    5 +
 libraries/cherrypy/test/static/dirback.jpg    |  Bin 0 -> 16585 bytes
 libraries/cherrypy/test/static/index.html     |    1 +
 libraries/cherrypy/test/style.css             |    1 +
 libraries/cherrypy/test/test.pem              |   38 +
 libraries/cherrypy/test/test_auth_basic.py    |  135 +
 libraries/cherrypy/test/test_auth_digest.py   |  134 +
 libraries/cherrypy/test/test_bus.py           |  274 ++
 libraries/cherrypy/test/test_caching.py       |  392 +++
 libraries/cherrypy/test/test_compat.py        |   34 +
 libraries/cherrypy/test/test_config.py        |  303 +++
 libraries/cherrypy/test/test_config_server.py |  126 +
 libraries/cherrypy/test/test_conn.py          |  873 +++++++
 libraries/cherrypy/test/test_core.py          |  823 ++++++
 .../test/test_dynamicobjectmapping.py         |  424 ++++
 libraries/cherrypy/test/test_encoding.py      |  426 ++++
 libraries/cherrypy/test/test_etags.py         |   84 +
 libraries/cherrypy/test/test_http.py          |  307 +++
 libraries/cherrypy/test/test_httputil.py      |   80 +
 libraries/cherrypy/test/test_iterator.py      |  196 ++
 libraries/cherrypy/test/test_json.py          |  102 +
 libraries/cherrypy/test/test_logging.py       |  209 ++
 libraries/cherrypy/test/test_mime.py          |  134 +
 libraries/cherrypy/test/test_misc_tools.py    |  210 ++
 libraries/cherrypy/test/test_native.py        |   35 +
 libraries/cherrypy/test/test_objectmapping.py |  430 ++++
 libraries/cherrypy/test/test_params.py        |   61 +
 libraries/cherrypy/test/test_plugins.py       |   14 +
 libraries/cherrypy/test/test_proxy.py         |  154 ++
 libraries/cherrypy/test/test_refleaks.py      |   66 +
 libraries/cherrypy/test/test_request_obj.py   |  932 +++++++
 libraries/cherrypy/test/test_routes.py        |   80 +
 libraries/cherrypy/test/test_session.py       |  512 ++++
 .../cherrypy/test/test_sessionauthenticate.py |   61 +
 libraries/cherrypy/test/test_states.py        |  473 ++++
 libraries/cherrypy/test/test_static.py        |  434 ++++
 libraries/cherrypy/test/test_tools.py         |  468 ++++
 libraries/cherrypy/test/test_tutorials.py     |  210 ++
 libraries/cherrypy/test/test_virtualhost.py   |  113 +
 libraries/cherrypy/test/test_wsgi_ns.py       |   93 +
 .../cherrypy/test/test_wsgi_unix_socket.py    |   93 +
 libraries/cherrypy/test/test_wsgi_vhost.py    |   35 +
 libraries/cherrypy/test/test_wsgiapps.py      |  120 +
 libraries/cherrypy/test/test_xmlrpc.py        |  183 ++
 libraries/cherrypy/test/webtest.py            |   11 +
 libraries/cherrypy/tutorial/README.rst        |   16 +
 libraries/cherrypy/tutorial/__init__.py       |    3 +
 libraries/cherrypy/tutorial/custom_error.html |   14 +
 libraries/cherrypy/tutorial/pdf_file.pdf      |  Bin 0 -> 85698 bytes
 .../cherrypy/tutorial/tut01_helloworld.py     |   34 +
 .../cherrypy/tutorial/tut02_expose_methods.py |   32 +
 .../cherrypy/tutorial/tut03_get_and_post.py   |   51 +
 .../cherrypy/tutorial/tut04_complex_site.py   |  103 +
 .../tutorial/tut05_derived_objects.py         |   80 +
 .../cherrypy/tutorial/tut06_default_method.py |   61 +
 libraries/cherrypy/tutorial/tut07_sessions.py |   41 +
 .../tutorial/tut08_generators_and_yield.py    |   44 +
 libraries/cherrypy/tutorial/tut09_files.py    |  105 +
 .../cherrypy/tutorial/tut10_http_errors.py    |   84 +
 libraries/cherrypy/tutorial/tutorial.conf     |    4 +
 libraries/contextlib2.py                      |  436 ++++
 libraries/more_itertools/__init__.py          |    2 +
 libraries/more_itertools/more.py              | 2211 +++++++++++++++++
 libraries/more_itertools/recipes.py           |  565 +++++
 libraries/more_itertools/tests/__init__.py    |    0
 libraries/more_itertools/tests/test_more.py   | 2074 ++++++++++++++++
 .../more_itertools/tests/test_recipes.py      |  616 +++++
 libraries/portend.py                          |  212 ++
 libraries/tempora/__init__.py                 |  505 ++++
 libraries/tempora/schedule.py                 |  202 ++
 libraries/tempora/tests/test_schedule.py      |  118 +
 libraries/tempora/timing.py                   |  219 ++
 libraries/tempora/utc.py                      |   36 +
 libraries/zc/__init__.py                      |    1 +
 libraries/zc/lockfile/README.txt              |   70 +
 libraries/zc/lockfile/__init__.py             |  104 +
 libraries/zc/lockfile/tests.py                |  193 ++
 resources/lib/webservice.py                   |  223 +-
 164 files changed, 42855 insertions(+), 174 deletions(-)
 create mode 100644 libraries/backports/__init__.py
 create mode 100644 libraries/backports/functools_lru_cache.py
 create mode 100644 libraries/cheroot/__init__.py
 create mode 100644 libraries/cheroot/__main__.py
 create mode 100644 libraries/cheroot/_compat.py
 create mode 100644 libraries/cheroot/cli.py
 create mode 100644 libraries/cheroot/errors.py
 create mode 100644 libraries/cheroot/makefile.py
 create mode 100644 libraries/cheroot/server.py
 create mode 100644 libraries/cheroot/ssl/__init__.py
 create mode 100644 libraries/cheroot/ssl/builtin.py
 create mode 100644 libraries/cheroot/ssl/pyopenssl.py
 create mode 100644 libraries/cheroot/test/__init__.py
 create mode 100644 libraries/cheroot/test/conftest.py
 create mode 100644 libraries/cheroot/test/helper.py
 create mode 100644 libraries/cheroot/test/test.pem
 create mode 100644 libraries/cheroot/test/test__compat.py
 create mode 100644 libraries/cheroot/test/test_conn.py
 create mode 100644 libraries/cheroot/test/test_core.py
 create mode 100644 libraries/cheroot/test/test_server.py
 create mode 100644 libraries/cheroot/test/webtest.py
 create mode 100644 libraries/cheroot/testing.py
 create mode 100644 libraries/cheroot/workers/__init__.py
 create mode 100644 libraries/cheroot/workers/threadpool.py
 create mode 100644 libraries/cheroot/wsgi.py
 create mode 100644 libraries/cherrypy/__init__.py
 create mode 100644 libraries/cherrypy/__main__.py
 create mode 100644 libraries/cherrypy/_cpchecker.py
 create mode 100644 libraries/cherrypy/_cpcompat.py
 create mode 100644 libraries/cherrypy/_cpconfig.py
 create mode 100644 libraries/cherrypy/_cpdispatch.py
 create mode 100644 libraries/cherrypy/_cperror.py
 create mode 100644 libraries/cherrypy/_cplogging.py
 create mode 100644 libraries/cherrypy/_cpmodpy.py
 create mode 100644 libraries/cherrypy/_cpnative_server.py
 create mode 100644 libraries/cherrypy/_cpreqbody.py
 create mode 100644 libraries/cherrypy/_cprequest.py
 create mode 100644 libraries/cherrypy/_cpserver.py
 create mode 100644 libraries/cherrypy/_cptools.py
 create mode 100644 libraries/cherrypy/_cptree.py
 create mode 100644 libraries/cherrypy/_cpwsgi.py
 create mode 100644 libraries/cherrypy/_cpwsgi_server.py
 create mode 100644 libraries/cherrypy/_helper.py
 create mode 100644 libraries/cherrypy/daemon.py
 create mode 100644 libraries/cherrypy/favicon.ico
 create mode 100644 libraries/cherrypy/lib/__init__.py
 create mode 100644 libraries/cherrypy/lib/auth_basic.py
 create mode 100644 libraries/cherrypy/lib/auth_digest.py
 create mode 100644 libraries/cherrypy/lib/caching.py
 create mode 100644 libraries/cherrypy/lib/covercp.py
 create mode 100644 libraries/cherrypy/lib/cpstats.py
 create mode 100644 libraries/cherrypy/lib/cptools.py
 create mode 100644 libraries/cherrypy/lib/encoding.py
 create mode 100644 libraries/cherrypy/lib/gctools.py
 create mode 100644 libraries/cherrypy/lib/httputil.py
 create mode 100644 libraries/cherrypy/lib/jsontools.py
 create mode 100644 libraries/cherrypy/lib/locking.py
 create mode 100644 libraries/cherrypy/lib/profiler.py
 create mode 100644 libraries/cherrypy/lib/reprconf.py
 create mode 100644 libraries/cherrypy/lib/sessions.py
 create mode 100644 libraries/cherrypy/lib/static.py
 create mode 100644 libraries/cherrypy/lib/xmlrpcutil.py
 create mode 100644 libraries/cherrypy/process/__init__.py
 create mode 100644 libraries/cherrypy/process/plugins.py
 create mode 100644 libraries/cherrypy/process/servers.py
 create mode 100644 libraries/cherrypy/process/win32.py
 create mode 100644 libraries/cherrypy/process/wspbus.py
 create mode 100644 libraries/cherrypy/scaffold/__init__.py
 create mode 100644 libraries/cherrypy/scaffold/apache-fcgi.conf
 create mode 100644 libraries/cherrypy/scaffold/example.conf
 create mode 100644 libraries/cherrypy/scaffold/site.conf
 create mode 100644 libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png
 create mode 100644 libraries/cherrypy/test/__init__.py
 create mode 100644 libraries/cherrypy/test/_test_decorators.py
 create mode 100644 libraries/cherrypy/test/_test_states_demo.py
 create mode 100644 libraries/cherrypy/test/benchmark.py
 create mode 100644 libraries/cherrypy/test/checkerdemo.py
 create mode 100644 libraries/cherrypy/test/fastcgi.conf
 create mode 100644 libraries/cherrypy/test/fcgi.conf
 create mode 100644 libraries/cherrypy/test/helper.py
 create mode 100644 libraries/cherrypy/test/logtest.py
 create mode 100644 libraries/cherrypy/test/modfastcgi.py
 create mode 100644 libraries/cherrypy/test/modfcgid.py
 create mode 100644 libraries/cherrypy/test/modpy.py
 create mode 100644 libraries/cherrypy/test/modwsgi.py
 create mode 100644 libraries/cherrypy/test/sessiondemo.py
 create mode 100644 libraries/cherrypy/test/static/404.html
 create mode 100644 libraries/cherrypy/test/static/dirback.jpg
 create mode 100644 libraries/cherrypy/test/static/index.html
 create mode 100644 libraries/cherrypy/test/style.css
 create mode 100644 libraries/cherrypy/test/test.pem
 create mode 100644 libraries/cherrypy/test/test_auth_basic.py
 create mode 100644 libraries/cherrypy/test/test_auth_digest.py
 create mode 100644 libraries/cherrypy/test/test_bus.py
 create mode 100644 libraries/cherrypy/test/test_caching.py
 create mode 100644 libraries/cherrypy/test/test_compat.py
 create mode 100644 libraries/cherrypy/test/test_config.py
 create mode 100644 libraries/cherrypy/test/test_config_server.py
 create mode 100644 libraries/cherrypy/test/test_conn.py
 create mode 100644 libraries/cherrypy/test/test_core.py
 create mode 100644 libraries/cherrypy/test/test_dynamicobjectmapping.py
 create mode 100644 libraries/cherrypy/test/test_encoding.py
 create mode 100644 libraries/cherrypy/test/test_etags.py
 create mode 100644 libraries/cherrypy/test/test_http.py
 create mode 100644 libraries/cherrypy/test/test_httputil.py
 create mode 100644 libraries/cherrypy/test/test_iterator.py
 create mode 100644 libraries/cherrypy/test/test_json.py
 create mode 100644 libraries/cherrypy/test/test_logging.py
 create mode 100644 libraries/cherrypy/test/test_mime.py
 create mode 100644 libraries/cherrypy/test/test_misc_tools.py
 create mode 100644 libraries/cherrypy/test/test_native.py
 create mode 100644 libraries/cherrypy/test/test_objectmapping.py
 create mode 100644 libraries/cherrypy/test/test_params.py
 create mode 100644 libraries/cherrypy/test/test_plugins.py
 create mode 100644 libraries/cherrypy/test/test_proxy.py
 create mode 100644 libraries/cherrypy/test/test_refleaks.py
 create mode 100644 libraries/cherrypy/test/test_request_obj.py
 create mode 100644 libraries/cherrypy/test/test_routes.py
 create mode 100644 libraries/cherrypy/test/test_session.py
 create mode 100644 libraries/cherrypy/test/test_sessionauthenticate.py
 create mode 100644 libraries/cherrypy/test/test_states.py
 create mode 100644 libraries/cherrypy/test/test_static.py
 create mode 100644 libraries/cherrypy/test/test_tools.py
 create mode 100644 libraries/cherrypy/test/test_tutorials.py
 create mode 100644 libraries/cherrypy/test/test_virtualhost.py
 create mode 100644 libraries/cherrypy/test/test_wsgi_ns.py
 create mode 100644 libraries/cherrypy/test/test_wsgi_unix_socket.py
 create mode 100644 libraries/cherrypy/test/test_wsgi_vhost.py
 create mode 100644 libraries/cherrypy/test/test_wsgiapps.py
 create mode 100644 libraries/cherrypy/test/test_xmlrpc.py
 create mode 100644 libraries/cherrypy/test/webtest.py
 create mode 100644 libraries/cherrypy/tutorial/README.rst
 create mode 100644 libraries/cherrypy/tutorial/__init__.py
 create mode 100644 libraries/cherrypy/tutorial/custom_error.html
 create mode 100644 libraries/cherrypy/tutorial/pdf_file.pdf
 create mode 100644 libraries/cherrypy/tutorial/tut01_helloworld.py
 create mode 100644 libraries/cherrypy/tutorial/tut02_expose_methods.py
 create mode 100644 libraries/cherrypy/tutorial/tut03_get_and_post.py
 create mode 100644 libraries/cherrypy/tutorial/tut04_complex_site.py
 create mode 100644 libraries/cherrypy/tutorial/tut05_derived_objects.py
 create mode 100644 libraries/cherrypy/tutorial/tut06_default_method.py
 create mode 100644 libraries/cherrypy/tutorial/tut07_sessions.py
 create mode 100644 libraries/cherrypy/tutorial/tut08_generators_and_yield.py
 create mode 100644 libraries/cherrypy/tutorial/tut09_files.py
 create mode 100644 libraries/cherrypy/tutorial/tut10_http_errors.py
 create mode 100644 libraries/cherrypy/tutorial/tutorial.conf
 create mode 100644 libraries/contextlib2.py
 create mode 100644 libraries/more_itertools/__init__.py
 create mode 100644 libraries/more_itertools/more.py
 create mode 100644 libraries/more_itertools/recipes.py
 create mode 100644 libraries/more_itertools/tests/__init__.py
 create mode 100644 libraries/more_itertools/tests/test_more.py
 create mode 100644 libraries/more_itertools/tests/test_recipes.py
 create mode 100644 libraries/portend.py
 create mode 100644 libraries/tempora/__init__.py
 create mode 100644 libraries/tempora/schedule.py
 create mode 100644 libraries/tempora/tests/test_schedule.py
 create mode 100644 libraries/tempora/timing.py
 create mode 100644 libraries/tempora/utc.py
 create mode 100644 libraries/zc/__init__.py
 create mode 100644 libraries/zc/lockfile/README.txt
 create mode 100644 libraries/zc/lockfile/__init__.py
 create mode 100644 libraries/zc/lockfile/tests.py

diff --git a/libraries/backports/__init__.py b/libraries/backports/__init__.py
new file mode 100644
index 00000000..69e3be50
--- /dev/null
+++ b/libraries/backports/__init__.py
@@ -0,0 +1 @@
+__path__ = __import__('pkgutil').extend_path(__path__, __name__)
diff --git a/libraries/backports/functools_lru_cache.py b/libraries/backports/functools_lru_cache.py
new file mode 100644
index 00000000..707c6c76
--- /dev/null
+++ b/libraries/backports/functools_lru_cache.py
@@ -0,0 +1,184 @@
+from __future__ import absolute_import
+
+import functools
+from collections import namedtuple
+from threading import RLock
+
+_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
+
+
+@functools.wraps(functools.update_wrapper)
+def update_wrapper(wrapper,
+                   wrapped,
+                   assigned = functools.WRAPPER_ASSIGNMENTS,
+                   updated = functools.WRAPPER_UPDATES):
+    """
+    Patch two bugs in functools.update_wrapper.
+    """
+    # workaround for http://bugs.python.org/issue3445
+    assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr))
+    wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated)
+    # workaround for https://bugs.python.org/issue17482
+    wrapper.__wrapped__ = wrapped
+    return wrapper
+
+
+class _HashedSeq(list):
+    __slots__ = 'hashvalue'
+
+    def __init__(self, tup, hash=hash):
+        self[:] = tup
+        self.hashvalue = hash(tup)
+
+    def __hash__(self):
+        return self.hashvalue
+
+
+def _make_key(args, kwds, typed,
+              kwd_mark=(object(),),
+              fasttypes=set([int, str, frozenset, type(None)]),
+              sorted=sorted, tuple=tuple, type=type, len=len):
+    'Make a cache key from optionally typed positional and keyword arguments'
+    key = args
+    if kwds:
+        sorted_items = sorted(kwds.items())
+        key += kwd_mark
+        for item in sorted_items:
+            key += item
+    if typed:
+        key += tuple(type(v) for v in args)
+        if kwds:
+            key += tuple(type(v) for k, v in sorted_items)
+    elif len(key) == 1 and type(key[0]) in fasttypes:
+        return key[0]
+    return _HashedSeq(key)
+
+
+def lru_cache(maxsize=100, typed=False):
+    """Least-recently-used cache decorator.
+
+    If *maxsize* is set to None, the LRU features are disabled and the cache
+    can grow without bound.
+
+    If *typed* is True, arguments of different types will be cached separately.
+    For example, f(3.0) and f(3) will be treated as distinct calls with
+    distinct results.
+
+    Arguments to the cached function must be hashable.
+
+    View the cache statistics named tuple (hits, misses, maxsize, currsize) with
+    f.cache_info().  Clear the cache and statistics with f.cache_clear().
+    Access the underlying function with f.__wrapped__.
+
+    See:  http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
+
+    """
+
+    # Users should only access the lru_cache through its public API:
+    #       cache_info, cache_clear, and f.__wrapped__
+    # The internals of the lru_cache are encapsulated for thread safety and
+    # to allow the implementation to change (including a possible C version).
+
+    def decorating_function(user_function):
+
+        cache = dict()
+        stats = [0, 0]                  # make statistics updateable non-locally
+        HITS, MISSES = 0, 1             # names for the stats fields
+        make_key = _make_key
+        cache_get = cache.get           # bound method to lookup key or return None
+        _len = len                      # localize the global len() function
+        lock = RLock()                  # because linkedlist updates aren't threadsafe
+        root = []                       # root of the circular doubly linked list
+        root[:] = [root, root, None, None]      # initialize by pointing to self
+        nonlocal_root = [root]                  # make updateable non-locally
+        PREV, NEXT, KEY, RESULT = 0, 1, 2, 3    # names for the link fields
+
+        if maxsize == 0:
+
+            def wrapper(*args, **kwds):
+                # no caching, just do a statistics update after a successful call
+                result = user_function(*args, **kwds)
+                stats[MISSES] += 1
+                return result
+
+        elif maxsize is None:
+
+            def wrapper(*args, **kwds):
+                # simple caching without ordering or size limit
+                key = make_key(args, kwds, typed)
+                result = cache_get(key, root)   # root used here as a unique not-found sentinel
+                if result is not root:
+                    stats[HITS] += 1
+                    return result
+                result = user_function(*args, **kwds)
+                cache[key] = result
+                stats[MISSES] += 1
+                return result
+
+        else:
+
+            def wrapper(*args, **kwds):
+                # size limited caching that tracks accesses by recency
+                key = make_key(args, kwds, typed) if kwds or typed else args
+                with lock:
+                    link = cache_get(key)
+                    if link is not None:
+                        # record recent use of the key by moving it to the front of the list
+                        root, = nonlocal_root
+                        link_prev, link_next, key, result = link
+                        link_prev[NEXT] = link_next
+                        link_next[PREV] = link_prev
+                        last = root[PREV]
+                        last[NEXT] = root[PREV] = link
+                        link[PREV] = last
+                        link[NEXT] = root
+                        stats[HITS] += 1
+                        return result
+                result = user_function(*args, **kwds)
+                with lock:
+                    root, = nonlocal_root
+                    if key in cache:
+                        # getting here means that this same key was added to the
+                        # cache while the lock was released.  since the link
+                        # update is already done, we need only return the
+                        # computed result and update the count of misses.
+                        pass
+                    elif _len(cache) >= maxsize:
+                        # use the old root to store the new key and result
+                        oldroot = root
+                        oldroot[KEY] = key
+                        oldroot[RESULT] = result
+                        # empty the oldest link and make it the new root
+                        root = nonlocal_root[0] = oldroot[NEXT]
+                        oldkey = root[KEY]
+                        root[KEY] = root[RESULT] = None
+                        # now update the cache dictionary for the new links
+                        del cache[oldkey]
+                        cache[key] = oldroot
+                    else:
+                        # put result in a new link at the front of the list
+                        last = root[PREV]
+                        link = [last, root, key, result]
+                        last[NEXT] = root[PREV] = cache[key] = link
+                    stats[MISSES] += 1
+                return result
+
+        def cache_info():
+            """Report cache statistics"""
+            with lock:
+                return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
+
+        def cache_clear():
+            """Clear the cache and cache statistics"""
+            with lock:
+                cache.clear()
+                root = nonlocal_root[0]
+                root[:] = [root, root, None, None]
+                stats[:] = [0, 0]
+
+        wrapper.__wrapped__ = user_function
+        wrapper.cache_info = cache_info
+        wrapper.cache_clear = cache_clear
+        return update_wrapper(wrapper, user_function)
+
+    return decorating_function
diff --git a/libraries/cheroot/__init__.py b/libraries/cheroot/__init__.py
new file mode 100644
index 00000000..a313660e
--- /dev/null
+++ b/libraries/cheroot/__init__.py
@@ -0,0 +1,6 @@
+"""High-performance, pure-Python HTTP server used by CherryPy."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+__version__ = '6.4.0'
diff --git a/libraries/cheroot/__main__.py b/libraries/cheroot/__main__.py
new file mode 100644
index 00000000..d2e27c10
--- /dev/null
+++ b/libraries/cheroot/__main__.py
@@ -0,0 +1,6 @@
+"""Stub for accessing the Cheroot CLI tool."""
+
+from .cli import main
+
+if __name__ == '__main__':
+    main()
diff --git a/libraries/cheroot/_compat.py b/libraries/cheroot/_compat.py
new file mode 100644
index 00000000..e98f91f9
--- /dev/null
+++ b/libraries/cheroot/_compat.py
@@ -0,0 +1,66 @@
+"""Compatibility code for using Cheroot with various versions of Python."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import re
+
+import six
+
+if six.PY3:
+    def ntob(n, encoding='ISO-8859-1'):
+        """Return the native string as bytes in the given encoding."""
+        assert_native(n)
+        # In Python 3, the native string type is unicode
+        return n.encode(encoding)
+
+    def ntou(n, encoding='ISO-8859-1'):
+        """Return the native string as unicode with the given encoding."""
+        assert_native(n)
+        # In Python 3, the native string type is unicode
+        return n
+
+    def bton(b, encoding='ISO-8859-1'):
+        """Return the byte string as native string in the given encoding."""
+        return b.decode(encoding)
+else:
+    # Python 2
+    def ntob(n, encoding='ISO-8859-1'):
+        """Return the native string as bytes in the given encoding."""
+        assert_native(n)
+        # In Python 2, the native string type is bytes. Assume it's already
+        # in the given encoding, which for ISO-8859-1 is almost always what
+        # was intended.
+        return n
+
+    def ntou(n, encoding='ISO-8859-1'):
+        """Return the native string as unicode with the given encoding."""
+        assert_native(n)
+        # In Python 2, the native string type is bytes.
+        # First, check for the special encoding 'escape'. The test suite uses
+        # this to signal that it wants to pass a string with embedded \uXXXX
+        # escapes, but without having to prefix it with u'' for Python 2,
+        # but no prefix for Python 3.
+        if encoding == 'escape':
+            return six.u(
+                re.sub(r'\\u([0-9a-zA-Z]{4})',
+                       lambda m: six.unichr(int(m.group(1), 16)),
+                       n.decode('ISO-8859-1')))
+        # Assume it's already in the given encoding, which for ISO-8859-1
+        # is almost always what was intended.
+        return n.decode(encoding)
+
+    def bton(b, encoding='ISO-8859-1'):
+        """Return the byte string as native string in the given encoding."""
+        return b
+
+
+def assert_native(n):
+    """Check whether the input is of nativ ``str`` type.
+
+    Raises:
+        TypeError: in case of failed check
+
+    """
+    if not isinstance(n, str):
+        raise TypeError('n must be a native str (got %s)' % type(n).__name__)
diff --git a/libraries/cheroot/cli.py b/libraries/cheroot/cli.py
new file mode 100644
index 00000000..6d59fb5c
--- /dev/null
+++ b/libraries/cheroot/cli.py
@@ -0,0 +1,233 @@
+"""Command line tool for starting a Cheroot WSGI/HTTP server instance.
+
+Basic usage::
+
+    # Start a server on 127.0.0.1:8000 with the default settings
+    # for the WSGI app myapp/wsgi.py:application()
+    cheroot myapp.wsgi
+
+    # Start a server on 0.0.0.0:9000 with 8 threads
+    # for the WSGI app myapp/wsgi.py:main_app()
+    cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8
+
+    # Start a server for the cheroot.server.Gateway subclass
+    # myapp/gateway.py:HTTPGateway
+    cheroot myapp.gateway:HTTPGateway
+
+    # Start a server on the UNIX socket /var/spool/myapp.sock
+    cheroot myapp.wsgi --bind /var/spool/myapp.sock
+
+    # Start a server on the abstract UNIX socket CherootServer
+    cheroot myapp.wsgi --bind @CherootServer
+"""
+
+import argparse
+from importlib import import_module
+import os
+import sys
+import contextlib
+
+import six
+
+from . import server
+from . import wsgi
+
+
+__metaclass__ = type
+
+
+class BindLocation:
+    """A class for storing the bind location for a Cheroot instance."""
+
+
+class TCPSocket(BindLocation):
+    """TCPSocket."""
+
+    def __init__(self, address, port):
+        """Initialize.
+
+        Args:
+            address (str): Host name or IP address
+            port (int): TCP port number
+        """
+        self.bind_addr = address, port
+
+
+class UnixSocket(BindLocation):
+    """UnixSocket."""
+
+    def __init__(self, path):
+        """Initialize."""
+        self.bind_addr = path
+
+
+class AbstractSocket(BindLocation):
+    """AbstractSocket."""
+
+    def __init__(self, addr):
+        """Initialize."""
+        self.bind_addr = '\0{}'.format(self.abstract_socket)
+
+
+class Application:
+    """Application."""
+
+    @classmethod
+    def resolve(cls, full_path):
+        """Read WSGI app/Gateway path string and import application module."""
+        mod_path, _, app_path = full_path.partition(':')
+        app = getattr(import_module(mod_path), app_path or 'application')
+
+        with contextlib.suppress(TypeError):
+            if issubclass(app, server.Gateway):
+                return GatewayYo(app)
+
+        return cls(app)
+
+    def __init__(self, wsgi_app):
+        """Initialize."""
+        if not callable(wsgi_app):
+            raise TypeError(
+                'Application must be a callable object or '
+                'cheroot.server.Gateway subclass'
+            )
+        self.wsgi_app = wsgi_app
+
+    def server_args(self, parsed_args):
+        """Return keyword args for Server class."""
+        args = {
+            arg: value
+            for arg, value in vars(parsed_args).items()
+            if not arg.startswith('_') and value is not None
+        }
+        args.update(vars(self))
+        return args
+
+    def server(self, parsed_args):
+        """Server."""
+        return wsgi.Server(**self.server_args(parsed_args))
+
+
+class GatewayYo:
+    """Gateway."""
+
+    def __init__(self, gateway):
+        """Init."""
+        self.gateway = gateway
+
+    def server(self, parsed_args):
+        """Server."""
+        server_args = vars(self)
+        server_args['bind_addr'] = parsed_args['bind_addr']
+        if parsed_args.max is not None:
+            server_args['maxthreads'] = parsed_args.max
+        if parsed_args.numthreads is not None:
+            server_args['minthreads'] = parsed_args.numthreads
+        return server.HTTPServer(**server_args)
+
+
+def parse_wsgi_bind_location(bind_addr_string):
+    """Convert bind address string to a BindLocation."""
+    # try and match for an IP/hostname and port
+    match = six.moves.urllib.parse.urlparse('//{}'.format(bind_addr_string))
+    try:
+        addr = match.hostname
+        port = match.port
+        if addr is not None or port is not None:
+            return TCPSocket(addr, port)
+    except ValueError:
+        pass
+
+    # else, assume a UNIX socket path
+    # if the string begins with an @ symbol, use an abstract socket
+    if bind_addr_string.startswith('@'):
+        return AbstractSocket(bind_addr_string[1:])
+    return UnixSocket(path=bind_addr_string)
+
+
+def parse_wsgi_bind_addr(bind_addr_string):
+    """Convert bind address string to bind address parameter."""
+    return parse_wsgi_bind_location(bind_addr_string).bind_addr
+
+
+_arg_spec = {
+    '_wsgi_app': dict(
+        metavar='APP_MODULE',
+        type=Application.resolve,
+        help='WSGI application callable or cheroot.server.Gateway subclass',
+    ),
+    '--bind': dict(
+        metavar='ADDRESS',
+        dest='bind_addr',
+        type=parse_wsgi_bind_addr,
+        default='[::1]:8000',
+        help='Network interface to listen on (default: [::1]:8000)',
+    ),
+    '--chdir': dict(
+        metavar='PATH',
+        type=os.chdir,
+        help='Set the working directory',
+    ),
+    '--server-name': dict(
+        dest='server_name',
+        type=str,
+        help='Web server name to be advertised via Server HTTP header',
+    ),
+    '--threads': dict(
+        metavar='INT',
+        dest='numthreads',
+        type=int,
+        help='Minimum number of worker threads',
+    ),
+    '--max-threads': dict(
+        metavar='INT',
+        dest='max',
+        type=int,
+        help='Maximum number of worker threads',
+    ),
+    '--timeout': dict(
+        metavar='INT',
+        dest='timeout',
+        type=int,
+        help='Timeout in seconds for accepted connections',
+    ),
+    '--shutdown-timeout': dict(
+        metavar='INT',
+        dest='shutdown_timeout',
+        type=int,
+        help='Time in seconds to wait for worker threads to cleanly exit',
+    ),
+    '--request-queue-size': dict(
+        metavar='INT',
+        dest='request_queue_size',
+        type=int,
+        help='Maximum number of queued connections',
+    ),
+    '--accepted-queue-size': dict(
+        metavar='INT',
+        dest='accepted_queue_size',
+        type=int,
+        help='Maximum number of active requests in queue',
+    ),
+    '--accepted-queue-timeout': dict(
+        metavar='INT',
+        dest='accepted_queue_timeout',
+        type=int,
+        help='Timeout in seconds for putting requests into queue',
+    ),
+}
+
+
+def main():
+    """Create a new Cheroot instance with arguments from the command line."""
+    parser = argparse.ArgumentParser(
+        description='Start an instance of the Cheroot WSGI/HTTP server.')
+    for arg, spec in _arg_spec.items():
+        parser.add_argument(arg, **spec)
+    raw_args = parser.parse_args()
+
+    # ensure cwd in sys.path
+    '' in sys.path or sys.path.insert(0, '')
+
+    # create a server based on the arguments provided
+    raw_args._wsgi_app.server(raw_args).safe_start()
diff --git a/libraries/cheroot/errors.py b/libraries/cheroot/errors.py
new file mode 100644
index 00000000..82412b42
--- /dev/null
+++ b/libraries/cheroot/errors.py
@@ -0,0 +1,58 @@
+"""Collection of exceptions raised and/or processed by Cheroot."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import errno
+import sys
+
+
+class MaxSizeExceeded(Exception):
+    """Exception raised when a client sends more data then acceptable within limit.
+
+    Depends on ``request.body.maxbytes`` config option if used within CherryPy
+    """
+
+
+class NoSSLError(Exception):
+    """Exception raised when a client speaks HTTP to an HTTPS socket."""
+
+
+class FatalSSLAlert(Exception):
+    """Exception raised when the SSL implementation signals a fatal alert."""
+
+
+def plat_specific_errors(*errnames):
+    """Return error numbers for all errors in errnames on this platform.
+
+    The 'errno' module contains different global constants depending on
+    the specific platform (OS). This function will return the list of
+    numeric values for a given list of potential names.
+    """
+    errno_names = dir(errno)
+    nums = [getattr(errno, k) for k in errnames if k in errno_names]
+    # de-dupe the list
+    return list(dict.fromkeys(nums).keys())
+
+
+socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR')
+
+socket_errors_to_ignore = plat_specific_errors(
+    'EPIPE',
+    'EBADF', 'WSAEBADF',
+    'ENOTSOCK', 'WSAENOTSOCK',
+    'ETIMEDOUT', 'WSAETIMEDOUT',
+    'ECONNREFUSED', 'WSAECONNREFUSED',
+    'ECONNRESET', 'WSAECONNRESET',
+    'ECONNABORTED', 'WSAECONNABORTED',
+    'ENETRESET', 'WSAENETRESET',
+    'EHOSTDOWN', 'EHOSTUNREACH',
+)
+socket_errors_to_ignore.append('timed out')
+socket_errors_to_ignore.append('The read operation timed out')
+socket_errors_nonblocking = plat_specific_errors(
+    'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
+
+if sys.platform == 'darwin':
+    socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE'))
+    socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE'))
diff --git a/libraries/cheroot/makefile.py b/libraries/cheroot/makefile.py
new file mode 100644
index 00000000..a76f2eda
--- /dev/null
+++ b/libraries/cheroot/makefile.py
@@ -0,0 +1,387 @@
+"""Socket file object."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import socket
+
+try:
+    # prefer slower Python-based io module
+    import _pyio as io
+except ImportError:
+    # Python 2.6
+    import io
+
+import six
+
+from . import errors
+
+
+class BufferedWriter(io.BufferedWriter):
+    """Faux file object attached to a socket object."""
+
+    def write(self, b):
+        """Write bytes to buffer."""
+        self._checkClosed()
+        if isinstance(b, str):
+            raise TypeError("can't write str to binary stream")
+
+        with self._write_lock:
+            self._write_buf.extend(b)
+            self._flush_unlocked()
+            return len(b)
+
+    def _flush_unlocked(self):
+        self._checkClosed('flush of closed file')
+        while self._write_buf:
+            try:
+                # ssl sockets only except 'bytes', not bytearrays
+                # so perhaps we should conditionally wrap this for perf?
+                n = self.raw.write(bytes(self._write_buf))
+            except io.BlockingIOError as e:
+                n = e.characters_written
+            del self._write_buf[:n]
+
+
+def MakeFile_PY3(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
+    """File object attached to a socket object."""
+    if 'r' in mode:
+        return io.BufferedReader(socket.SocketIO(sock, mode), bufsize)
+    else:
+        return BufferedWriter(socket.SocketIO(sock, mode), bufsize)
+
+
+class MakeFile_PY2(getattr(socket, '_fileobject', object)):
+    """Faux file object attached to a socket object."""
+
+    def __init__(self, *args, **kwargs):
+        """Initialize faux file object."""
+        self.bytes_read = 0
+        self.bytes_written = 0
+        socket._fileobject.__init__(self, *args, **kwargs)
+
+    def write(self, data):
+        """Sendall for non-blocking sockets."""
+        while data:
+            try:
+                bytes_sent = self.send(data)
+                data = data[bytes_sent:]
+            except socket.error as e:
+                if e.args[0] not in errors.socket_errors_nonblocking:
+                    raise
+
+    def send(self, data):
+        """Send some part of message to the socket."""
+        bytes_sent = self._sock.send(data)
+        self.bytes_written += bytes_sent
+        return bytes_sent
+
+    def flush(self):
+        """Write all data from buffer to socket and reset write buffer."""
+        if self._wbuf:
+            buffer = ''.join(self._wbuf)
+            self._wbuf = []
+            self.write(buffer)
+
+    def recv(self, size):
+        """Receive message of a size from the socket."""
+        while True:
+            try:
+                data = self._sock.recv(size)
+                self.bytes_read += len(data)
+                return data
+            except socket.error as e:
+                what = (
+                    e.args[0] not in errors.socket_errors_nonblocking
+                    and e.args[0] not in errors.socket_error_eintr
+                )
+                if what:
+                    raise
+
+    class FauxSocket:
+        """Faux socket with the minimal interface required by pypy."""
+
+        def _reuse(self):
+            pass
+
+    _fileobject_uses_str_type = six.PY2 and isinstance(
+        socket._fileobject(FauxSocket())._rbuf, six.string_types)
+
+    # FauxSocket is no longer needed
+    del FauxSocket
+
+    if not _fileobject_uses_str_type:
+        def read(self, size=-1):
+            """Read data from the socket to buffer."""
+            # Use max, disallow tiny reads in a loop as they are very
+            # inefficient.
+            # We never leave read() with any leftover data from a new recv()
+            # call in our internal buffer.
+            rbufsize = max(self._rbufsize, self.default_bufsize)
+            # Our use of StringIO rather than lists of string objects returned
+            # by recv() minimizes memory usage and fragmentation that occurs
+            # when rbufsize is large compared to the typical return value of
+            # recv().
+            buf = self._rbuf
+            buf.seek(0, 2)  # seek end
+            if size < 0:
+                # Read until EOF
+                # reset _rbuf.  we consume it via buf.
+                self._rbuf = io.BytesIO()
+                while True:
+                    data = self.recv(rbufsize)
+                    if not data:
+                        break
+                    buf.write(data)
+                return buf.getvalue()
+            else:
+                # Read until size bytes or EOF seen, whichever comes first
+                buf_len = buf.tell()
+                if buf_len >= size:
+                    # Already have size bytes in our buffer?  Extract and
+                    # return.
+                    buf.seek(0)
+                    rv = buf.read(size)
+                    self._rbuf = io.BytesIO()
+                    self._rbuf.write(buf.read())
+                    return rv
+
+                # reset _rbuf.  we consume it via buf.
+                self._rbuf = io.BytesIO()
+                while True:
+                    left = size - buf_len
+                    # recv() will malloc the amount of memory given as its
+                    # parameter even though it often returns much less data
+                    # than that.  The returned data string is short lived
+                    # as we copy it into a StringIO and free it.  This avoids
+                    # fragmentation issues on many platforms.
+                    data = self.recv(left)
+                    if not data:
+                        break
+                    n = len(data)
+                    if n == size and not buf_len:
+                        # Shortcut.  Avoid buffer data copies when:
+                        # - We have no data in our buffer.
+                        # AND
+                        # - Our call to recv returned exactly the
+                        #   number of bytes we were asked to read.
+                        return data
+                    if n == left:
+                        buf.write(data)
+                        del data  # explicit free
+                        break
+                    assert n <= left, 'recv(%d) returned %d bytes' % (left, n)
+                    buf.write(data)
+                    buf_len += n
+                    del data  # explicit free
+                    # assert buf_len == buf.tell()
+                return buf.getvalue()
+
+        def readline(self, size=-1):
+            """Read line from the socket to buffer."""
+            buf = self._rbuf
+            buf.seek(0, 2)  # seek end
+            if buf.tell() > 0:
+                # check if we already have it in our buffer
+                buf.seek(0)
+                bline = buf.readline(size)
+                if bline.endswith('\n') or len(bline) == size:
+                    self._rbuf = io.BytesIO()
+                    self._rbuf.write(buf.read())
+                    return bline
+                del bline
+            if size < 0:
+                # Read until \n or EOF, whichever comes first
+                if self._rbufsize <= 1:
+                    # Speed up unbuffered case
+                    buf.seek(0)
+                    buffers = [buf.read()]
+                    # reset _rbuf.  we consume it via buf.
+                    self._rbuf = io.BytesIO()
+                    data = None
+                    recv = self.recv
+                    while data != '\n':
+                        data = recv(1)
+                        if not data:
+                            break
+                        buffers.append(data)
+                    return ''.join(buffers)
+
+                buf.seek(0, 2)  # seek end
+                # reset _rbuf.  we consume it via buf.
+                self._rbuf = io.BytesIO()
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    nl = data.find('\n')
+                    if nl >= 0:
+                        nl += 1
+                        buf.write(data[:nl])
+                        self._rbuf.write(data[nl:])
+                        del data
+                        break
+                    buf.write(data)
+                return buf.getvalue()
+            else:
+                # Read until size bytes or \n or EOF seen, whichever comes
+                # first
+                buf.seek(0, 2)  # seek end
+                buf_len = buf.tell()
+                if buf_len >= size:
+                    buf.seek(0)
+                    rv = buf.read(size)
+                    self._rbuf = io.BytesIO()
+                    self._rbuf.write(buf.read())
+                    return rv
+                # reset _rbuf.  we consume it via buf.
+                self._rbuf = io.BytesIO()
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    left = size - buf_len
+                    # did we just receive a newline?
+                    nl = data.find('\n', 0, left)
+                    if nl >= 0:
+                        nl += 1
+                        # save the excess data to _rbuf
+                        self._rbuf.write(data[nl:])
+                        if buf_len:
+                            buf.write(data[:nl])
+                            break
+                        else:
+                            # Shortcut.  Avoid data copy through buf when
+                            # returning a substring of our first recv().
+                            return data[:nl]
+                    n = len(data)
+                    if n == size and not buf_len:
+                        # Shortcut.  Avoid data copy through buf when
+                        # returning exactly all of our first recv().
+                        return data
+                    if n >= left:
+                        buf.write(data[:left])
+                        self._rbuf.write(data[left:])
+                        break
+                    buf.write(data)
+                    buf_len += n
+                    # assert buf_len == buf.tell()
+                return buf.getvalue()
+    else:
+        def read(self, size=-1):
+            """Read data from the socket to buffer."""
+            if size < 0:
+                # Read until EOF
+                buffers = [self._rbuf]
+                self._rbuf = ''
+                if self._rbufsize <= 1:
+                    recv_size = self.default_bufsize
+                else:
+                    recv_size = self._rbufsize
+
+                while True:
+                    data = self.recv(recv_size)
+                    if not data:
+                        break
+                    buffers.append(data)
+                return ''.join(buffers)
+            else:
+                # Read until size bytes or EOF seen, whichever comes first
+                data = self._rbuf
+                buf_len = len(data)
+                if buf_len >= size:
+                    self._rbuf = data[size:]
+                    return data[:size]
+                buffers = []
+                if data:
+                    buffers.append(data)
+                self._rbuf = ''
+                while True:
+                    left = size - buf_len
+                    recv_size = max(self._rbufsize, left)
+                    data = self.recv(recv_size)
+                    if not data:
+                        break
+                    buffers.append(data)
+                    n = len(data)
+                    if n >= left:
+                        self._rbuf = data[left:]
+                        buffers[-1] = data[:left]
+                        break
+                    buf_len += n
+                return ''.join(buffers)
+
+        def readline(self, size=-1):
+            """Read line from the socket to buffer."""
+            data = self._rbuf
+            if size < 0:
+                # Read until \n or EOF, whichever comes first
+                if self._rbufsize <= 1:
+                    # Speed up unbuffered case
+                    assert data == ''
+                    buffers = []
+                    while data != '\n':
+                        data = self.recv(1)
+                        if not data:
+                            break
+                        buffers.append(data)
+                    return ''.join(buffers)
+                nl = data.find('\n')
+                if nl >= 0:
+                    nl += 1
+                    self._rbuf = data[nl:]
+                    return data[:nl]
+                buffers = []
+                if data:
+                    buffers.append(data)
+                self._rbuf = ''
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    buffers.append(data)
+                    nl = data.find('\n')
+                    if nl >= 0:
+                        nl += 1
+                        self._rbuf = data[nl:]
+                        buffers[-1] = data[:nl]
+                        break
+                return ''.join(buffers)
+            else:
+                # Read until size bytes or \n or EOF seen, whichever comes
+                # first
+                nl = data.find('\n', 0, size)
+                if nl >= 0:
+                    nl += 1
+                    self._rbuf = data[nl:]
+                    return data[:nl]
+                buf_len = len(data)
+                if buf_len >= size:
+                    self._rbuf = data[size:]
+                    return data[:size]
+                buffers = []
+                if data:
+                    buffers.append(data)
+                self._rbuf = ''
+                while True:
+                    data = self.recv(self._rbufsize)
+                    if not data:
+                        break
+                    buffers.append(data)
+                    left = size - buf_len
+                    nl = data.find('\n', 0, left)
+                    if nl >= 0:
+                        nl += 1
+                        self._rbuf = data[nl:]
+                        buffers[-1] = data[:nl]
+                        break
+                    n = len(data)
+                    if n >= left:
+                        self._rbuf = data[left:]
+                        buffers[-1] = data[:left]
+                        break
+                    buf_len += n
+                return ''.join(buffers)
+
+
+MakeFile = MakeFile_PY2 if six.PY2 else MakeFile_PY3
diff --git a/libraries/cheroot/server.py b/libraries/cheroot/server.py
new file mode 100644
index 00000000..44070490
--- /dev/null
+++ b/libraries/cheroot/server.py
@@ -0,0 +1,2001 @@
+"""
+A high-speed, production ready, thread pooled, generic HTTP server.
+
+For those of you wanting to understand internals of this module, here's the
+basic call flow. The server's listening thread runs a very tight loop,
+sticking incoming connections onto a Queue::
+
+    server = HTTPServer(...)
+    server.start()
+    ->  while True:
+            tick()
+            # This blocks until a request comes in:
+            child = socket.accept()
+            conn = HTTPConnection(child, ...)
+            server.requests.put(conn)
+
+Worker threads are kept in a pool and poll the Queue, popping off and then
+handling each connection in turn. Each connection can consist of an arbitrary
+number of requests and their responses, so we run a nested loop::
+
+    while True:
+        conn = server.requests.get()
+        conn.communicate()
+        ->  while True:
+                req = HTTPRequest(...)
+                req.parse_request()
+                ->  # Read the Request-Line, e.g. "GET /page HTTP/1.1"
+                    req.rfile.readline()
+                    read_headers(req.rfile, req.inheaders)
+                req.respond()
+                ->  response = app(...)
+                    try:
+                        for chunk in response:
+                            if chunk:
+                                req.write(chunk)
+                    finally:
+                        if hasattr(response, "close"):
+                            response.close()
+                if req.close_connection:
+                    return
+
+For running a server you can invoke :func:`start() <HTTPServer.start()>` (it
+will run the server forever) or use invoking :func:`prepare()
+<HTTPServer.prepare()>` and :func:`serve() <HTTPServer.serve()>` like this::
+
+    server = HTTPServer(...)
+    server.prepare()
+    try:
+        threading.Thread(target=server.serve).start()
+
+        # waiting/detecting some appropriate stop condition here
+        ...
+
+    finally:
+        server.stop()
+
+And now for a trivial doctest to exercise the test suite
+
+>>> 'HTTPServer' in globals()
+True
+
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+import io
+import re
+import email.utils
+import socket
+import sys
+import time
+import traceback as traceback_
+import logging
+import platform
+import xbmc
+
+try:
+    from functools import lru_cache
+except ImportError:
+    from backports.functools_lru_cache import lru_cache
+
+import six
+from six.moves import queue
+from six.moves import urllib
+
+from . import errors, __version__
+from ._compat import bton, ntou
+from .workers import threadpool
+from .makefile import MakeFile
+
+
+__all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer',
+           'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile',
+           'Gateway', 'get_ssl_adapter_class')
+
+"""
+Special KODI case:
+Android does not have support for grp and pwd
+But Python has issues reporting that this is running on Android (it shows as Linux2).
+We're instead using xbmc library to detect that.
+"""
+IS_WINDOWS = platform.system() == 'Windows'
+IS_ANDROID = xbmc.getCondVisibility('system.platform.linux') and xbmc.getCondVisibility('system.platform.android')
+
+if not (IS_WINDOWS or IS_ANDROID):
+    import grp
+    import pwd
+    import struct
+
+
+if IS_WINDOWS and hasattr(socket, 'AF_INET6'):
+    if not hasattr(socket, 'IPPROTO_IPV6'):
+        socket.IPPROTO_IPV6 = 41
+    if not hasattr(socket, 'IPV6_V6ONLY'):
+        socket.IPV6_V6ONLY = 27
+
+
+if not hasattr(socket, 'SO_PEERCRED'):
+    """
+    NOTE: the value for SO_PEERCRED can be architecture specific, in
+    which case the getsockopt() will hopefully fail. The arch
+    specific value could be derived from platform.processor()
+    """
+    socket.SO_PEERCRED = 17
+
+
+LF = b'\n'
+CRLF = b'\r\n'
+TAB = b'\t'
+SPACE = b' '
+COLON = b':'
+SEMICOLON = b';'
+EMPTY = b''
+ASTERISK = b'*'
+FORWARD_SLASH = b'/'
+QUOTED_SLASH = b'%2F'
+QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH)
+
+
+comma_separated_headers = [
+    b'Accept', b'Accept-Charset', b'Accept-Encoding',
+    b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control',
+    b'Connection', b'Content-Encoding', b'Content-Language', b'Expect',
+    b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE',
+    b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning',
+    b'WWW-Authenticate',
+]
+
+
+if not hasattr(logging, 'statistics'):
+    logging.statistics = {}
+
+
+class HeaderReader:
+    """Object for reading headers from an HTTP request.
+
+    Interface and default implementation.
+    """
+
+    def __call__(self, rfile, hdict=None):
+        """
+        Read headers from the given stream into the given header dict.
+
+        If hdict is None, a new header dict is created. Returns the populated
+        header dict.
+
+        Headers which are repeated are folded together using a comma if their
+        specification so dictates.
+
+        This function raises ValueError when the read bytes violate the HTTP
+        spec.
+        You should probably return "400 Bad Request" if this happens.
+        """
+        if hdict is None:
+            hdict = {}
+
+        while True:
+            line = rfile.readline()
+            if not line:
+                # No more data--illegal end of headers
+                raise ValueError('Illegal end of headers.')
+
+            if line == CRLF:
+                # Normal end of headers
+                break
+            if not line.endswith(CRLF):
+                raise ValueError('HTTP requires CRLF terminators')
+
+            if line[0] in (SPACE, TAB):
+                # It's a continuation line.
+                v = line.strip()
+            else:
+                try:
+                    k, v = line.split(COLON, 1)
+                except ValueError:
+                    raise ValueError('Illegal header line.')
+                v = v.strip()
+                k = self._transform_key(k)
+                hname = k
+
+            if not self._allow_header(k):
+                continue
+
+            if k in comma_separated_headers:
+                existing = hdict.get(hname)
+                if existing:
+                    v = b', '.join((existing, v))
+            hdict[hname] = v
+
+        return hdict
+
+    def _allow_header(self, key_name):
+        return True
+
+    def _transform_key(self, key_name):
+        # TODO: what about TE and WWW-Authenticate?
+        return key_name.strip().title()
+
+
+class DropUnderscoreHeaderReader(HeaderReader):
+    """Custom HeaderReader to exclude any headers with underscores in them."""
+
+    def _allow_header(self, key_name):
+        orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name)
+        return orig and '_' not in key_name
+
+
+class SizeCheckWrapper:
+    """Wraps a file-like object, raising MaxSizeExceeded if too large."""
+
+    def __init__(self, rfile, maxlen):
+        """Initialize SizeCheckWrapper instance.
+
+        Args:
+            rfile (file): file of a limited size
+            maxlen (int): maximum length of the file being read
+        """
+        self.rfile = rfile
+        self.maxlen = maxlen
+        self.bytes_read = 0
+
+    def _check_length(self):
+        if self.maxlen and self.bytes_read > self.maxlen:
+            raise errors.MaxSizeExceeded()
+
+    def read(self, size=None):
+        """Read a chunk from rfile buffer and return it.
+
+        Args:
+            size (int): amount of data to read
+
+        Returns:
+            bytes: Chunk from rfile, limited by size if specified.
+
+        """
+        data = self.rfile.read(size)
+        self.bytes_read += len(data)
+        self._check_length()
+        return data
+
+    def readline(self, size=None):
+        """Read a single line from rfile buffer and return it.
+
+        Args:
+            size (int): minimum amount of data to read
+
+        Returns:
+            bytes: One line from rfile.
+
+        """
+        if size is not None:
+            data = self.rfile.readline(size)
+            self.bytes_read += len(data)
+            self._check_length()
+            return data
+
+        # User didn't specify a size ...
+        # We read the line in chunks to make sure it's not a 100MB line !
+        res = []
+        while True:
+            data = self.rfile.readline(256)
+            self.bytes_read += len(data)
+            self._check_length()
+            res.append(data)
+            # See https://github.com/cherrypy/cherrypy/issues/421
+            if len(data) < 256 or data[-1:] == LF:
+                return EMPTY.join(res)
+
+    def readlines(self, sizehint=0):
+        """Read all lines from rfile buffer and return them.
+
+        Args:
+            sizehint (int): hint of minimum amount of data to read
+
+        Returns:
+            list[bytes]: Lines of bytes read from rfile.
+
+        """
+        # Shamelessly stolen from StringIO
+        total = 0
+        lines = []
+        line = self.readline(sizehint)
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline(sizehint)
+        return lines
+
+    def close(self):
+        """Release resources allocated for rfile."""
+        self.rfile.close()
+
+    def __iter__(self):
+        """Return file iterator."""
+        return self
+
+    def __next__(self):
+        """Generate next file chunk."""
+        data = next(self.rfile)
+        self.bytes_read += len(data)
+        self._check_length()
+        return data
+
+    next = __next__
+
+
+class KnownLengthRFile:
+    """Wraps a file-like object, returning an empty string when exhausted."""
+
+    def __init__(self, rfile, content_length):
+        """Initialize KnownLengthRFile instance.
+
+        Args:
+            rfile (file): file of a known size
+            content_length (int): length of the file being read
+
+        """
+        self.rfile = rfile
+        self.remaining = content_length
+
+    def read(self, size=None):
+        """Read a chunk from rfile buffer and return it.
+
+        Args:
+            size (int): amount of data to read
+
+        Returns:
+            bytes: Chunk from rfile, limited by size if specified.
+
+        """
+        if self.remaining == 0:
+            return b''
+        if size is None:
+            size = self.remaining
+        else:
+            size = min(size, self.remaining)
+
+        data = self.rfile.read(size)
+        self.remaining -= len(data)
+        return data
+
+    def readline(self, size=None):
+        """Read a single line from rfile buffer and return it.
+
+        Args:
+            size (int): minimum amount of data to read
+
+        Returns:
+            bytes: One line from rfile.
+
+        """
+        if self.remaining == 0:
+            return b''
+        if size is None:
+            size = self.remaining
+        else:
+            size = min(size, self.remaining)
+
+        data = self.rfile.readline(size)
+        self.remaining -= len(data)
+        return data
+
+    def readlines(self, sizehint=0):
+        """Read all lines from rfile buffer and return them.
+
+        Args:
+            sizehint (int): hint of minimum amount of data to read
+
+        Returns:
+            list[bytes]: Lines of bytes read from rfile.
+
+        """
+        # Shamelessly stolen from StringIO
+        total = 0
+        lines = []
+        line = self.readline(sizehint)
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline(sizehint)
+        return lines
+
+    def close(self):
+        """Release resources allocated for rfile."""
+        self.rfile.close()
+
+    def __iter__(self):
+        """Return file iterator."""
+        return self
+
+    def __next__(self):
+        """Generate next file chunk."""
+        data = next(self.rfile)
+        self.remaining -= len(data)
+        return data
+
+    next = __next__
+
+
+class ChunkedRFile:
+    """Wraps a file-like object, returning an empty string when exhausted.
+
+    This class is intended to provide a conforming wsgi.input value for
+    request entities that have been encoded with the 'chunked' transfer
+    encoding.
+    """
+
+    def __init__(self, rfile, maxlen, bufsize=8192):
+        """Initialize ChunkedRFile instance.
+
+        Args:
+            rfile (file): file encoded with the 'chunked' transfer encoding
+            maxlen (int): maximum length of the file being read
+            bufsize (int): size of the buffer used to read the file
+        """
+        self.rfile = rfile
+        self.maxlen = maxlen
+        self.bytes_read = 0
+        self.buffer = EMPTY
+        self.bufsize = bufsize
+        self.closed = False
+
+    def _fetch(self):
+        if self.closed:
+            return
+
+        line = self.rfile.readline()
+        self.bytes_read += len(line)
+
+        if self.maxlen and self.bytes_read > self.maxlen:
+            raise errors.MaxSizeExceeded(
+                'Request Entity Too Large', self.maxlen)
+
+        line = line.strip().split(SEMICOLON, 1)
+
+        try:
+            chunk_size = line.pop(0)
+            chunk_size = int(chunk_size, 16)
+        except ValueError:
+            raise ValueError('Bad chunked transfer size: ' + repr(chunk_size))
+
+        if chunk_size <= 0:
+            self.closed = True
+            return
+
+#            if line: chunk_extension = line[0]
+
+        if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
+            raise IOError('Request Entity Too Large')
+
+        chunk = self.rfile.read(chunk_size)
+        self.bytes_read += len(chunk)
+        self.buffer += chunk
+
+        crlf = self.rfile.read(2)
+        if crlf != CRLF:
+            raise ValueError(
+                "Bad chunked transfer coding (expected '\\r\\n', "
+                'got ' + repr(crlf) + ')')
+
+    def read(self, size=None):
+        """Read a chunk from rfile buffer and return it.
+
+        Args:
+            size (int): amount of data to read
+
+        Returns:
+            bytes: Chunk from rfile, limited by size if specified.
+
+        """
+        data = EMPTY
+
+        if size == 0:
+            return data
+
+        while True:
+            if size and len(data) >= size:
+                return data
+
+            if not self.buffer:
+                self._fetch()
+                if not self.buffer:
+                    # EOF
+                    return data
+
+            if size:
+                remaining = size - len(data)
+                data += self.buffer[:remaining]
+                self.buffer = self.buffer[remaining:]
+            else:
+                data += self.buffer
+                self.buffer = EMPTY
+
+    def readline(self, size=None):
+        """Read a single line from rfile buffer and return it.
+
+        Args:
+            size (int): minimum amount of data to read
+
+        Returns:
+            bytes: One line from rfile.
+
+        """
+        data = EMPTY
+
+        if size == 0:
+            return data
+
+        while True:
+            if size and len(data) >= size:
+                return data
+
+            if not self.buffer:
+                self._fetch()
+                if not self.buffer:
+                    # EOF
+                    return data
+
+            newline_pos = self.buffer.find(LF)
+            if size:
+                if newline_pos == -1:
+                    remaining = size - len(data)
+                    data += self.buffer[:remaining]
+                    self.buffer = self.buffer[remaining:]
+                else:
+                    remaining = min(size - len(data), newline_pos)
+                    data += self.buffer[:remaining]
+                    self.buffer = self.buffer[remaining:]
+            else:
+                if newline_pos == -1:
+                    data += self.buffer
+                    self.buffer = EMPTY
+                else:
+                    data += self.buffer[:newline_pos]
+                    self.buffer = self.buffer[newline_pos:]
+
+    def readlines(self, sizehint=0):
+        """Read all lines from rfile buffer and return them.
+
+        Args:
+            sizehint (int): hint of minimum amount of data to read
+
+        Returns:
+            list[bytes]: Lines of bytes read from rfile.
+
+        """
+        # Shamelessly stolen from StringIO
+        total = 0
+        lines = []
+        line = self.readline(sizehint)
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline(sizehint)
+        return lines
+
+    def read_trailer_lines(self):
+        """Read HTTP headers and yield them.
+
+        Returns:
+            Generator: yields CRLF separated lines.
+
+        """
+        if not self.closed:
+            raise ValueError(
+                'Cannot read trailers until the request body has been read.')
+
+        while True:
+            line = self.rfile.readline()
+            if not line:
+                # No more data--illegal end of headers
+                raise ValueError('Illegal end of headers.')
+
+            self.bytes_read += len(line)
+            if self.maxlen and self.bytes_read > self.maxlen:
+                raise IOError('Request Entity Too Large')
+
+            if line == CRLF:
+                # Normal end of headers
+                break
+            if not line.endswith(CRLF):
+                raise ValueError('HTTP requires CRLF terminators')
+
+            yield line
+
+    def close(self):
+        """Release resources allocated for rfile."""
+        self.rfile.close()
+
+
+class HTTPRequest:
+    """An HTTP Request (and response).
+
+    A single HTTP connection may consist of multiple request/response pairs.
+    """
+
+    server = None
+    """The HTTPServer object which is receiving this request."""
+
+    conn = None
+    """The HTTPConnection object on which this request connected."""
+
+    inheaders = {}
+    """A dict of request headers."""
+
+    outheaders = []
+    """A list of header tuples to write in the response."""
+
+    ready = False
+    """When True, the request has been parsed and is ready to begin generating
+    the response. When False, signals the calling Connection that the response
+    should not be generated and the connection should close."""
+
+    close_connection = False
+    """Signals the calling Connection that the request should close. This does
+    not imply an error! The client and/or server may each request that the
+    connection be closed."""
+
+    chunked_write = False
+    """If True, output will be encoded with the "chunked" transfer-coding.
+
+    This value is set automatically inside send_headers."""
+
+    header_reader = HeaderReader()
+    """
+    A HeaderReader instance or compatible reader.
+    """
+
+    def __init__(self, server, conn, proxy_mode=False, strict_mode=True):
+        """Initialize HTTP request container instance.
+
+        Args:
+            server (HTTPServer): web server object receiving this request
+            conn (HTTPConnection): HTTP connection object for this request
+            proxy_mode (bool): whether this HTTPServer should behave as a PROXY
+            server for certain requests
+            strict_mode (bool): whether we should return a 400 Bad Request when
+            we encounter a request that a HTTP compliant client should not be
+            making
+        """
+        self.server = server
+        self.conn = conn
+
+        self.ready = False
+        self.started_request = False
+        self.scheme = b'http'
+        if self.server.ssl_adapter is not None:
+            self.scheme = b'https'
+        # Use the lowest-common protocol in case read_request_line errors.
+        self.response_protocol = 'HTTP/1.0'
+        self.inheaders = {}
+
+        self.status = ''
+        self.outheaders = []
+        self.sent_headers = False
+        self.close_connection = self.__class__.close_connection
+        self.chunked_read = False
+        self.chunked_write = self.__class__.chunked_write
+        self.proxy_mode = proxy_mode
+        self.strict_mode = strict_mode
+
+    def parse_request(self):
+        """Parse the next HTTP request start-line and message-headers."""
+        self.rfile = SizeCheckWrapper(self.conn.rfile,
+                                      self.server.max_request_header_size)
+        try:
+            success = self.read_request_line()
+        except errors.MaxSizeExceeded:
+            self.simple_response(
+                '414 Request-URI Too Long',
+                'The Request-URI sent with the request exceeds the maximum '
+                'allowed bytes.')
+            return
+        else:
+            if not success:
+                return
+
+        try:
+            success = self.read_request_headers()
+        except errors.MaxSizeExceeded:
+            self.simple_response(
+                '413 Request Entity Too Large',
+                'The headers sent with the request exceed the maximum '
+                'allowed bytes.')
+            return
+        else:
+            if not success:
+                return
+
+        self.ready = True
+
+    def read_request_line(self):
+        """Read and parse first line of the HTTP request.
+
+        Returns:
+            bool: True if the request line is valid or False if it's malformed.
+
+        """
+        # HTTP/1.1 connections are persistent by default. If a client
+        # requests a page, then idles (leaves the connection open),
+        # then rfile.readline() will raise socket.error("timed out").
+        # Note that it does this based on the value given to settimeout(),
+        # and doesn't need the client to request or acknowledge the close
+        # (although your TCP stack might suffer for it: cf Apache's history
+        # with FIN_WAIT_2).
+        request_line = self.rfile.readline()
+
+        # Set started_request to True so communicate() knows to send 408
+        # from here on out.
+        self.started_request = True
+        if not request_line:
+            return False
+
+        if request_line == CRLF:
+            # RFC 2616 sec 4.1: "...if the server is reading the protocol
+            # stream at the beginning of a message and receives a CRLF
+            # first, it should ignore the CRLF."
+            # But only ignore one leading line! else we enable a DoS.
+            request_line = self.rfile.readline()
+            if not request_line:
+                return False
+
+        if not request_line.endswith(CRLF):
+            self.simple_response(
+                '400 Bad Request', 'HTTP requires CRLF terminators')
+            return False
+
+        try:
+            method, uri, req_protocol = request_line.strip().split(SPACE, 2)
+            if not req_protocol.startswith(b'HTTP/'):
+                self.simple_response(
+                    '400 Bad Request', 'Malformed Request-Line: bad protocol'
+                )
+                return False
+            rp = req_protocol[5:].split(b'.', 1)
+            rp = tuple(map(int, rp))  # Minor.Major must be threat as integers
+            if rp > (1, 1):
+                self.simple_response(
+                    '505 HTTP Version Not Supported', 'Cannot fulfill request'
+                )
+                return False
+        except (ValueError, IndexError):
+            self.simple_response('400 Bad Request', 'Malformed Request-Line')
+            return False
+
+        self.uri = uri
+        self.method = method.upper()
+
+        if self.strict_mode and method != self.method:
+            resp = (
+                'Malformed method name: According to RFC 2616 '
+                '(section 5.1.1) and its successors '
+                'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) '
+                'method names are case-sensitive and uppercase.'
+            )
+            self.simple_response('400 Bad Request', resp)
+            return False
+
+        try:
+            if six.PY2:  # FIXME: Figure out better way to do this
+                # Ref: https://stackoverflow.com/a/196392/595220 (like this?)
+                """This is a dummy check for unicode in URI."""
+                ntou(bton(uri, 'ascii'), 'ascii')
+            scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri)
+        except UnicodeError:
+            self.simple_response('400 Bad Request', 'Malformed Request-URI')
+            return False
+
+        if self.method == b'OPTIONS':
+            # TODO: cover this branch with tests
+            path = (uri
+                    # https://tools.ietf.org/html/rfc7230#section-5.3.4
+                    if self.proxy_mode or uri == ASTERISK
+                    else path)
+        elif self.method == b'CONNECT':
+            # TODO: cover this branch with tests
+            if not self.proxy_mode:
+                self.simple_response('405 Method Not Allowed')
+                return False
+
+            # `urlsplit()` above parses "example.com:3128" as path part of URI.
+            # this is a workaround, which makes it detect netloc correctly
+            uri_split = urllib.parse.urlsplit(b'//' + uri)
+            _scheme, _authority, _path, _qs, _fragment = uri_split
+            _port = EMPTY
+            try:
+                _port = uri_split.port
+            except ValueError:
+                pass
+
+            # FIXME: use third-party validation to make checks against RFC
+            # the validation doesn't take into account, that urllib parses
+            # invalid URIs without raising errors
+            # https://tools.ietf.org/html/rfc7230#section-5.3.3
+            invalid_path = (
+                _authority != uri
+                or not _port
+                or any((_scheme, _path, _qs, _fragment))
+            )
+            if invalid_path:
+                self.simple_response('400 Bad Request',
+                                     'Invalid path in Request-URI: request-'
+                                     'target must match authority-form.')
+                return False
+
+            authority = path = _authority
+            scheme = qs = fragment = EMPTY
+        else:
+            uri_is_absolute_form = (scheme or authority)
+
+            disallowed_absolute = (
+                self.strict_mode
+                and not self.proxy_mode
+                and uri_is_absolute_form
+            )
+            if disallowed_absolute:
+                # https://tools.ietf.org/html/rfc7230#section-5.3.2
+                # (absolute form)
+                """Absolute URI is only allowed within proxies."""
+                self.simple_response(
+                    '400 Bad Request',
+                    'Absolute URI not allowed if server is not a proxy.',
+                )
+                return False
+
+            invalid_path = (
+                self.strict_mode
+                and not uri.startswith(FORWARD_SLASH)
+                and not uri_is_absolute_form
+            )
+            if invalid_path:
+                # https://tools.ietf.org/html/rfc7230#section-5.3.1
+                # (origin_form) and
+                """Path should start with a forward slash."""
+                resp = (
+                    'Invalid path in Request-URI: request-target must contain '
+                    'origin-form which starts with absolute-path (URI '
+                    'starting with a slash "/").'
+                )
+                self.simple_response('400 Bad Request', resp)
+                return False
+
+            if fragment:
+                self.simple_response('400 Bad Request',
+                                     'Illegal #fragment in Request-URI.')
+                return False
+
+            if path is None:
+                # FIXME: It looks like this case cannot happen
+                self.simple_response('400 Bad Request',
+                                     'Invalid path in Request-URI.')
+                return False
+
+            # Unquote the path+params (e.g. "/this%20path" -> "/this path").
+            # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
+            #
+            # But note that "...a URI must be separated into its components
+            # before the escaped characters within those components can be
+            # safely decoded." https://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
+            # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not
+            # "/this/path".
+            try:
+                # TODO: Figure out whether exception can really happen here.
+                # It looks like it's caught on urlsplit() call above.
+                atoms = [
+                    urllib.parse.unquote_to_bytes(x)
+                    for x in QUOTED_SLASH_REGEX.split(path)
+                ]
+            except ValueError as ex:
+                self.simple_response('400 Bad Request', ex.args[0])
+                return False
+            path = QUOTED_SLASH.join(atoms)
+
+        if not path.startswith(FORWARD_SLASH):
+            path = FORWARD_SLASH + path
+
+        if scheme is not EMPTY:
+            self.scheme = scheme
+        self.authority = authority
+        self.path = path
+
+        # Note that, like wsgiref and most other HTTP servers,
+        # we "% HEX HEX"-unquote the path but not the query string.
+        self.qs = qs
+
+        # Compare request and server HTTP protocol versions, in case our
+        # server does not support the requested protocol. Limit our output
+        # to min(req, server). We want the following output:
+        #     request    server     actual written   supported response
+        #     protocol   protocol  response protocol    feature set
+        # a     1.0        1.0           1.0                1.0
+        # b     1.0        1.1           1.1                1.0
+        # c     1.1        1.0           1.0                1.0
+        # d     1.1        1.1           1.1                1.1
+        # Notice that, in (b), the response will be "HTTP/1.1" even though
+        # the client only understands 1.0. RFC 2616 10.5.6 says we should
+        # only return 505 if the _major_ version is different.
+        sp = int(self.server.protocol[5]), int(self.server.protocol[7])
+
+        if sp[0] != rp[0]:
+            self.simple_response('505 HTTP Version Not Supported')
+            return False
+
+        self.request_protocol = req_protocol
+        self.response_protocol = 'HTTP/%s.%s' % min(rp, sp)
+
+        return True
+
+    def read_request_headers(self):
+        """Read self.rfile into self.inheaders. Return success."""
+        # then all the http headers
+        try:
+            self.header_reader(self.rfile, self.inheaders)
+        except ValueError as ex:
+            self.simple_response('400 Bad Request', ex.args[0])
+            return False
+
+        mrbs = self.server.max_request_body_size
+
+        try:
+            cl = int(self.inheaders.get(b'Content-Length', 0))
+        except ValueError:
+            self.simple_response(
+                '400 Bad Request',
+                'Malformed Content-Length Header.')
+            return False
+
+        if mrbs and cl > mrbs:
+            self.simple_response(
+                '413 Request Entity Too Large',
+                'The entity sent with the request exceeds the maximum '
+                'allowed bytes.')
+            return False
+
+        # Persistent connection support
+        if self.response_protocol == 'HTTP/1.1':
+            # Both server and client are HTTP/1.1
+            if self.inheaders.get(b'Connection', b'') == b'close':
+                self.close_connection = True
+        else:
+            # Either the server or client (or both) are HTTP/1.0
+            if self.inheaders.get(b'Connection', b'') != b'Keep-Alive':
+                self.close_connection = True
+
+        # Transfer-Encoding support
+        te = None
+        if self.response_protocol == 'HTTP/1.1':
+            te = self.inheaders.get(b'Transfer-Encoding')
+            if te:
+                te = [x.strip().lower() for x in te.split(b',') if x.strip()]
+
+        self.chunked_read = False
+
+        if te:
+            for enc in te:
+                if enc == b'chunked':
+                    self.chunked_read = True
+                else:
+                    # Note that, even if we see "chunked", we must reject
+                    # if there is an extension we don't recognize.
+                    self.simple_response('501 Unimplemented')
+                    self.close_connection = True
+                    return False
+
+        # From PEP 333:
+        # "Servers and gateways that implement HTTP 1.1 must provide
+        # transparent support for HTTP 1.1's "expect/continue" mechanism.
+        # This may be done in any of several ways:
+        #   1. Respond to requests containing an Expect: 100-continue request
+        #      with an immediate "100 Continue" response, and proceed normally.
+        #   2. Proceed with the request normally, but provide the application
+        #      with a wsgi.input stream that will send the "100 Continue"
+        #      response if/when the application first attempts to read from
+        #      the input stream. The read request must then remain blocked
+        #      until the client responds.
+        #   3. Wait until the client decides that the server does not support
+        #      expect/continue, and sends the request body on its own.
+        #      (This is suboptimal, and is not recommended.)
+        #
+        # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
+        # but it seems like it would be a big slowdown for such a rare case.
+        if self.inheaders.get(b'Expect', b'') == b'100-continue':
+            # Don't use simple_response here, because it emits headers
+            # we don't want. See
+            # https://github.com/cherrypy/cherrypy/issues/951
+            msg = self.server.protocol.encode('ascii')
+            msg += b' 100 Continue\r\n\r\n'
+            try:
+                self.conn.wfile.write(msg)
+            except socket.error as ex:
+                if ex.args[0] not in errors.socket_errors_to_ignore:
+                    raise
+        return True
+
+    def respond(self):
+        """Call the gateway and write its iterable output."""
+        mrbs = self.server.max_request_body_size
+        if self.chunked_read:
+            self.rfile = ChunkedRFile(self.conn.rfile, mrbs)
+        else:
+            cl = int(self.inheaders.get(b'Content-Length', 0))
+            if mrbs and mrbs < cl:
+                if not self.sent_headers:
+                    self.simple_response(
+                        '413 Request Entity Too Large',
+                        'The entity sent with the request exceeds the '
+                        'maximum allowed bytes.')
+                return
+            self.rfile = KnownLengthRFile(self.conn.rfile, cl)
+
+        self.server.gateway(self).respond()
+        self.ready and self.ensure_headers_sent()
+
+        if self.chunked_write:
+            self.conn.wfile.write(b'0\r\n\r\n')
+
+    def simple_response(self, status, msg=''):
+        """Write a simple response back to the client."""
+        status = str(status)
+        proto_status = '%s %s\r\n' % (self.server.protocol, status)
+        content_length = 'Content-Length: %s\r\n' % len(msg)
+        content_type = 'Content-Type: text/plain\r\n'
+        buf = [
+            proto_status.encode('ISO-8859-1'),
+            content_length.encode('ISO-8859-1'),
+            content_type.encode('ISO-8859-1'),
+        ]
+
+        if status[:3] in ('413', '414'):
+            # Request Entity Too Large / Request-URI Too Long
+            self.close_connection = True
+            if self.response_protocol == 'HTTP/1.1':
+                # This will not be true for 414, since read_request_line
+                # usually raises 414 before reading the whole line, and we
+                # therefore cannot know the proper response_protocol.
+                buf.append(b'Connection: close\r\n')
+            else:
+                # HTTP/1.0 had no 413/414 status nor Connection header.
+                # Emit 400 instead and trust the message body is enough.
+                status = '400 Bad Request'
+
+        buf.append(CRLF)
+        if msg:
+            if isinstance(msg, six.text_type):
+                msg = msg.encode('ISO-8859-1')
+            buf.append(msg)
+
+        try:
+            self.conn.wfile.write(EMPTY.join(buf))
+        except socket.error as ex:
+            if ex.args[0] not in errors.socket_errors_to_ignore:
+                raise
+
+    def ensure_headers_sent(self):
+        """Ensure headers are sent to the client if not already sent."""
+        if not self.sent_headers:
+            self.sent_headers = True
+            self.send_headers()
+
+    def write(self, chunk):
+        """Write unbuffered data to the client."""
+        if self.chunked_write and chunk:
+            chunk_size_hex = hex(len(chunk))[2:].encode('ascii')
+            buf = [chunk_size_hex, CRLF, chunk, CRLF]
+            self.conn.wfile.write(EMPTY.join(buf))
+        else:
+            self.conn.wfile.write(chunk)
+
+    def send_headers(self):
+        """Assert, process, and send the HTTP response message-headers.
+
+        You must set self.status, and self.outheaders before calling this.
+        """
+        hkeys = [key.lower() for key, value in self.outheaders]
+        status = int(self.status[:3])
+
+        if status == 413:
+            # Request Entity Too Large. Close conn to avoid garbage.
+            self.close_connection = True
+        elif b'content-length' not in hkeys:
+            # "All 1xx (informational), 204 (no content),
+            # and 304 (not modified) responses MUST NOT
+            # include a message-body." So no point chunking.
+            if status < 200 or status in (204, 205, 304):
+                pass
+            else:
+                needs_chunked = (
+                    self.response_protocol == 'HTTP/1.1'
+                    and self.method != b'HEAD'
+                )
+                if needs_chunked:
+                    # Use the chunked transfer-coding
+                    self.chunked_write = True
+                    self.outheaders.append((b'Transfer-Encoding', b'chunked'))
+                else:
+                    # Closing the conn is the only way to determine len.
+                    self.close_connection = True
+
+        if b'connection' not in hkeys:
+            if self.response_protocol == 'HTTP/1.1':
+                # Both server and client are HTTP/1.1 or better
+                if self.close_connection:
+                    self.outheaders.append((b'Connection', b'close'))
+            else:
+                # Server and/or client are HTTP/1.0
+                if not self.close_connection:
+                    self.outheaders.append((b'Connection', b'Keep-Alive'))
+
+        if (not self.close_connection) and (not self.chunked_read):
+            # Read any remaining request body data on the socket.
+            # "If an origin server receives a request that does not include an
+            # Expect request-header field with the "100-continue" expectation,
+            # the request includes a request body, and the server responds
+            # with a final status code before reading the entire request body
+            # from the transport connection, then the server SHOULD NOT close
+            # the transport connection until it has read the entire request,
+            # or until the client closes the connection. Otherwise, the client
+            # might not reliably receive the response message. However, this
+            # requirement is not be construed as preventing a server from
+            # defending itself against denial-of-service attacks, or from
+            # badly broken client implementations."
+            remaining = getattr(self.rfile, 'remaining', 0)
+            if remaining > 0:
+                self.rfile.read(remaining)
+
+        if b'date' not in hkeys:
+            self.outheaders.append((
+                b'Date',
+                email.utils.formatdate(usegmt=True).encode('ISO-8859-1'),
+            ))
+
+        if b'server' not in hkeys:
+            self.outheaders.append((
+                b'Server',
+                self.server.server_name.encode('ISO-8859-1'),
+            ))
+
+        proto = self.server.protocol.encode('ascii')
+        buf = [proto + SPACE + self.status + CRLF]
+        for k, v in self.outheaders:
+            buf.append(k + COLON + SPACE + v + CRLF)
+        buf.append(CRLF)
+        self.conn.wfile.write(EMPTY.join(buf))
+
+
+class HTTPConnection:
+    """An HTTP connection (active socket)."""
+
+    remote_addr = None
+    remote_port = None
+    ssl_env = None
+    rbufsize = io.DEFAULT_BUFFER_SIZE
+    wbufsize = io.DEFAULT_BUFFER_SIZE
+    RequestHandlerClass = HTTPRequest
+    peercreds_enabled = False
+    peercreds_resolve_enabled = False
+
+    def __init__(self, server, sock, makefile=MakeFile):
+        """Initialize HTTPConnection instance.
+
+        Args:
+            server (HTTPServer): web server object receiving this request
+            socket (socket._socketobject): the raw socket object (usually
+                TCP) for this connection
+            makefile (file): a fileobject class for reading from the socket
+        """
+        self.server = server
+        self.socket = sock
+        self.rfile = makefile(sock, 'rb', self.rbufsize)
+        self.wfile = makefile(sock, 'wb', self.wbufsize)
+        self.requests_seen = 0
+
+        self.peercreds_enabled = self.server.peercreds_enabled
+        self.peercreds_resolve_enabled = self.server.peercreds_resolve_enabled
+
+        # LRU cached methods:
+        # Ref: https://stackoverflow.com/a/14946506/595220
+        self.resolve_peer_creds = (
+            lru_cache(maxsize=1)(self.resolve_peer_creds)
+        )
+        self.get_peer_creds = (
+            lru_cache(maxsize=1)(self.get_peer_creds)
+        )
+
+    def communicate(self):
+        """Read each request and respond appropriately."""
+        request_seen = False
+        try:
+            while True:
+                # (re)set req to None so that if something goes wrong in
+                # the RequestHandlerClass constructor, the error doesn't
+                # get written to the previous request.
+                req = None
+                req = self.RequestHandlerClass(self.server, self)
+
+                # This order of operations should guarantee correct pipelining.
+                req.parse_request()
+                if self.server.stats['Enabled']:
+                    self.requests_seen += 1
+                if not req.ready:
+                    # Something went wrong in the parsing (and the server has
+                    # probably already made a simple_response). Return and
+                    # let the conn close.
+                    return
+
+                request_seen = True
+                req.respond()
+                if req.close_connection:
+                    return
+        except socket.error as ex:
+            errnum = ex.args[0]
+            # sadly SSL sockets return a different (longer) time out string
+            timeout_errs = 'timed out', 'The read operation timed out'
+            if errnum in timeout_errs:
+                # Don't error if we're between requests; only error
+                # if 1) no request has been started at all, or 2) we're
+                # in the middle of a request.
+                # See https://github.com/cherrypy/cherrypy/issues/853
+                if (not request_seen) or (req and req.started_request):
+                    self._conditional_error(req, '408 Request Timeout')
+            elif errnum not in errors.socket_errors_to_ignore:
+                self.server.error_log('socket.error %s' % repr(errnum),
+                                      level=logging.WARNING, traceback=True)
+                self._conditional_error(req, '500 Internal Server Error')
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except errors.FatalSSLAlert:
+            pass
+        except errors.NoSSLError:
+            self._handle_no_ssl(req)
+        except Exception as ex:
+            self.server.error_log(
+                repr(ex), level=logging.ERROR, traceback=True)
+            self._conditional_error(req, '500 Internal Server Error')
+
+    linger = False
+
+    def _handle_no_ssl(self, req):
+        if not req or req.sent_headers:
+            return
+        # Unwrap wfile
+        self.wfile = MakeFile(self.socket._sock, 'wb', self.wbufsize)
+        msg = (
+            'The client sent a plain HTTP request, but '
+            'this server only speaks HTTPS on this port.'
+        )
+        req.simple_response('400 Bad Request', msg)
+        self.linger = True
+
+    def _conditional_error(self, req, response):
+        """Respond with an error.
+
+        Don't bother writing if a response
+        has already started being written.
+        """
+        if not req or req.sent_headers:
+            return
+
+        try:
+            req.simple_response(response)
+        except errors.FatalSSLAlert:
+            pass
+        except errors.NoSSLError:
+            self._handle_no_ssl(req)
+
+    def close(self):
+        """Close the socket underlying this connection."""
+        self.rfile.close()
+
+        if not self.linger:
+            self._close_kernel_socket()
+            self.socket.close()
+        else:
+            # On the other hand, sometimes we want to hang around for a bit
+            # to make sure the client has a chance to read our entire
+            # response. Skipping the close() calls here delays the FIN
+            # packet until the socket object is garbage-collected later.
+            # Someday, perhaps, we'll do the full lingering_close that
+            # Apache does, but not today.
+            pass
+
+    def get_peer_creds(self):  # LRU cached on per-instance basis, see __init__
+        """Return the PID/UID/GID tuple of the peer socket for UNIX sockets.
+
+        This function uses SO_PEERCRED to query the UNIX PID, UID, GID
+        of the peer, which is only available if the bind address is
+        a UNIX domain socket.
+
+        Raises:
+            NotImplementedError: in case of unsupported socket type
+            RuntimeError: in case of SO_PEERCRED lookup unsupported or disabled
+
+        """
+        PEERCRED_STRUCT_DEF = '3i'
+
+        if IS_WINDOWS or self.socket.family != socket.AF_UNIX:
+            raise NotImplementedError(
+                'SO_PEERCRED is only supported in Linux kernel and WSL'
+            )
+        elif not self.peercreds_enabled:
+            raise RuntimeError(
+                'Peer creds lookup is disabled within this server'
+            )
+
+        try:
+            peer_creds = self.socket.getsockopt(
+                socket.SOL_SOCKET, socket.SO_PEERCRED,
+                struct.calcsize(PEERCRED_STRUCT_DEF)
+            )
+        except socket.error as socket_err:
+            """Non-Linux kernels don't support SO_PEERCRED.
+
+            Refs:
+            http://welz.org.za/notes/on-peer-cred.html
+            https://github.com/daveti/tcpSockHack
+            msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025
+            """
+            six.raise_from(  # 3.6+: raise RuntimeError from socket_err
+                RuntimeError,
+                socket_err,
+            )
+        else:
+            pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds)
+            return pid, uid, gid
+
+    @property
+    def peer_pid(self):
+        """Return the id of the connected peer process."""
+        pid, _, _ = self.get_peer_creds()
+        return pid
+
+    @property
+    def peer_uid(self):
+        """Return the user id of the connected peer process."""
+        _, uid, _ = self.get_peer_creds()
+        return uid
+
+    @property
+    def peer_gid(self):
+        """Return the group id of the connected peer process."""
+        _, _, gid = self.get_peer_creds()
+        return gid
+
+    def resolve_peer_creds(self):  # LRU cached on per-instance basis
+        """Return the username and group tuple of the peercreds if available.
+
+        Raises:
+            NotImplementedError: in case of unsupported OS
+            RuntimeError: in case of UID/GID lookup unsupported or disabled
+
+        """
+        if (IS_WINDOWS or IS_ANDROID):
+            raise NotImplementedError(
+                'UID/GID lookup can only be done under UNIX-like OS'
+            )
+        elif not self.peercreds_resolve_enabled:
+            raise RuntimeError(
+                'UID/GID lookup is disabled within this server'
+            )
+
+        user = pwd.getpwuid(self.peer_uid).pw_name  # [0]
+        group = grp.getgrgid(self.peer_gid).gr_name  # [0]
+
+        return user, group
+
+    @property
+    def peer_user(self):
+        """Return the username of the connected peer process."""
+        user, _ = self.resolve_peer_creds()
+        return user
+
+    @property
+    def peer_group(self):
+        """Return the group of the connected peer process."""
+        _, group = self.resolve_peer_creds()
+        return group
+
+    def _close_kernel_socket(self):
+        """Close kernel socket in outdated Python versions.
+
+        On old Python versions,
+        Python's socket module does NOT call close on the kernel
+        socket when you call socket.close(). We do so manually here
+        because we want this server to send a FIN TCP segment
+        immediately. Note this must be called *before* calling
+        socket.close(), because the latter drops its reference to
+        the kernel socket.
+        """
+        if six.PY2 and hasattr(self.socket, '_sock'):
+            self.socket._sock.close()
+
+
+def prevent_socket_inheritance(sock):
+    """Stub inheritance prevention.
+
+    Dummy function, since neither fcntl nor ctypes are available.
+    """
+    pass
+
+
+class HTTPServer:
+    """An HTTP server."""
+
+    _bind_addr = '127.0.0.1'
+    _interrupt = None
+
+    gateway = None
+    """A Gateway instance."""
+
+    minthreads = None
+    """The minimum number of worker threads to create (default 10)."""
+
+    maxthreads = None
+    """The maximum number of worker threads to create.
+
+    (default -1 = no limit)"""
+
+    server_name = None
+    """The name of the server; defaults to ``self.version``."""
+
+    protocol = 'HTTP/1.1'
+    """The version string to write in the Status-Line of all HTTP responses.
+
+    For example, "HTTP/1.1" is the default. This also limits the supported
+    features used in the response."""
+
+    request_queue_size = 5
+    """The 'backlog' arg to socket.listen(); max queued connections.
+
+    (default 5)."""
+
+    shutdown_timeout = 5
+    """The total time to wait for worker threads to cleanly exit.
+
+    Specified in seconds."""
+
+    timeout = 10
+    """The timeout in seconds for accepted connections (default 10)."""
+
+    version = 'Cheroot/' + __version__
+    """A version string for the HTTPServer."""
+
+    software = None
+    """The value to set for the SERVER_SOFTWARE entry in the WSGI environ.
+
+    If None, this defaults to ``'%s Server' % self.version``.
+    """
+
+    ready = False
+    """Internal flag which indicating the socket is accepting connections."""
+
+    max_request_header_size = 0
+    """The maximum size, in bytes, for request headers, or 0 for no limit."""
+
+    max_request_body_size = 0
+    """The maximum size, in bytes, for request bodies, or 0 for no limit."""
+
+    nodelay = True
+    """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
+
+    ConnectionClass = HTTPConnection
+    """The class to use for handling HTTP connections."""
+
+    ssl_adapter = None
+    """An instance of ssl.Adapter (or a subclass).
+
+    You must have the corresponding SSL driver library installed.
+    """
+
+    peercreds_enabled = False
+    """If True, peer cred lookup can be performed via UNIX domain socket."""
+
+    peercreds_resolve_enabled = False
+    """If True, username/group will be looked up in the OS from peercreds."""
+
+    def __init__(
+        self, bind_addr, gateway,
+        minthreads=10, maxthreads=-1, server_name=None,
+        peercreds_enabled=False, peercreds_resolve_enabled=False,
+    ):
+        """Initialize HTTPServer instance.
+
+        Args:
+            bind_addr (tuple): network interface to listen to
+            gateway (Gateway): gateway for processing HTTP requests
+            minthreads (int): minimum number of threads for HTTP thread pool
+            maxthreads (int): maximum number of threads for HTTP thread pool
+            server_name (str): web server name to be advertised via Server
+                HTTP header
+        """
+        self.bind_addr = bind_addr
+        self.gateway = gateway
+
+        self.requests = threadpool.ThreadPool(
+            self, min=minthreads or 1, max=maxthreads)
+
+        if not server_name:
+            server_name = self.version
+        self.server_name = server_name
+        self.peercreds_enabled = peercreds_enabled
+        self.peercreds_resolve_enabled = (
+            peercreds_resolve_enabled and peercreds_enabled
+        )
+        self.clear_stats()
+
+    def clear_stats(self):
+        """Reset server stat counters.."""
+        self._start_time = None
+        self._run_time = 0
+        self.stats = {
+            'Enabled': False,
+            'Bind Address': lambda s: repr(self.bind_addr),
+            'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(),
+            'Accepts': 0,
+            'Accepts/sec': lambda s: s['Accepts'] / self.runtime(),
+            'Queue': lambda s: getattr(self.requests, 'qsize', None),
+            'Threads': lambda s: len(getattr(self.requests, '_threads', [])),
+            'Threads Idle': lambda s: getattr(self.requests, 'idle', None),
+            'Socket Errors': 0,
+            'Requests': lambda s: (not s['Enabled']) and -1 or sum(
+                [w['Requests'](w) for w in s['Worker Threads'].values()], 0),
+            'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum(
+                [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0),
+            'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum(
+                [w['Bytes Written'](w) for w in s['Worker Threads'].values()],
+                0),
+            'Work Time': lambda s: (not s['Enabled']) and -1 or sum(
+                [w['Work Time'](w) for w in s['Worker Threads'].values()], 0),
+            'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum(
+                [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
+                 for w in s['Worker Threads'].values()], 0),
+            'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum(
+                [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
+                 for w in s['Worker Threads'].values()], 0),
+            'Worker Threads': {},
+        }
+        logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats
+
+    def runtime(self):
+        """Return server uptime."""
+        if self._start_time is None:
+            return self._run_time
+        else:
+            return self._run_time + (time.time() - self._start_time)
+
+    def __str__(self):
+        """Render Server instance representing bind address."""
+        return '%s.%s(%r)' % (self.__module__, self.__class__.__name__,
+                              self.bind_addr)
+
+    @property
+    def bind_addr(self):
+        """Return the interface on which to listen for connections.
+
+        For TCP sockets, a (host, port) tuple. Host values may be any IPv4
+        or IPv6 address, or any valid hostname. The string 'localhost' is a
+        synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
+        The string '0.0.0.0' is a special IPv4 entry meaning "any active
+        interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
+        IPv6. The empty string or None are not allowed.
+
+        For UNIX sockets, supply the filename as a string.
+
+        Systemd socket activation is automatic and doesn't require tempering
+        with this variable.
+        """
+        return self._bind_addr
+
+    @bind_addr.setter
+    def bind_addr(self, value):
+        """Set the interface on which to listen for connections."""
+        if isinstance(value, tuple) and value[0] in ('', None):
+            # Despite the socket module docs, using '' does not
+            # allow AI_PASSIVE to work. Passing None instead
+            # returns '0.0.0.0' like we want. In other words:
+            #     host    AI_PASSIVE     result
+            #      ''         Y         192.168.x.y
+            #      ''         N         192.168.x.y
+            #     None        Y         0.0.0.0
+            #     None        N         127.0.0.1
+            # But since you can get the same effect with an explicit
+            # '0.0.0.0', we deny both the empty string and None as values.
+            raise ValueError("Host values of '' or None are not allowed. "
+                             "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
+                             'to listen on all active interfaces.')
+        self._bind_addr = value
+
+    def safe_start(self):
+        """Run the server forever, and stop it cleanly on exit."""
+        try:
+            self.start()
+        except (KeyboardInterrupt, IOError):
+            # The time.sleep call might raise
+            # "IOError: [Errno 4] Interrupted function call" on KBInt.
+            self.error_log('Keyboard Interrupt: shutting down')
+            self.stop()
+            raise
+        except SystemExit:
+            self.error_log('SystemExit raised: shutting down')
+            self.stop()
+            raise
+
+    def prepare(self):
+        """Prepare server to serving requests.
+
+        It binds a socket's port, setups the socket to ``listen()`` and does
+        other preparing things.
+        """
+        self._interrupt = None
+
+        if self.software is None:
+            self.software = '%s Server' % self.version
+
+        # Select the appropriate socket
+        self.socket = None
+        if os.getenv('LISTEN_PID', None):
+            # systemd socket activation
+            self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
+        elif isinstance(self.bind_addr, six.string_types):
+            # AF_UNIX socket
+
+            # So we can reuse the socket...
+            try:
+                os.unlink(self.bind_addr)
+            except Exception:
+                pass
+
+            # So everyone can access the socket...
+            try:
+                os.chmod(self.bind_addr, 0o777)
+            except Exception:
+                pass
+
+            info = [
+                (socket.AF_UNIX, socket.SOCK_STREAM, 0, '', self.bind_addr)]
+        else:
+            # AF_INET or AF_INET6 socket
+            # Get the correct address family for our host (allows IPv6
+            # addresses)
+            host, port = self.bind_addr
+            try:
+                info = socket.getaddrinfo(
+                    host, port, socket.AF_UNSPEC,
+                    socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
+            except socket.gaierror:
+                sock_type = socket.AF_INET
+                bind_addr = self.bind_addr
+
+                if ':' in host:
+                    sock_type = socket.AF_INET6
+                    bind_addr = bind_addr + (0, 0)
+
+                info = [(sock_type, socket.SOCK_STREAM, 0, '', bind_addr)]
+
+        if not self.socket:
+            msg = 'No socket could be created'
+            for res in info:
+                af, socktype, proto, canonname, sa = res
+                try:
+                    self.bind(af, socktype, proto)
+                    break
+                except socket.error as serr:
+                    msg = '%s -- (%s: %s)' % (msg, sa, serr)
+                    if self.socket:
+                        self.socket.close()
+                    self.socket = None
+
+            if not self.socket:
+                raise socket.error(msg)
+
+        # Timeout so KeyboardInterrupt can be caught on Win32
+        self.socket.settimeout(1)
+        self.socket.listen(self.request_queue_size)
+
+        # Create worker threads
+        self.requests.start()
+
+        self.ready = True
+        self._start_time = time.time()
+
+    def serve(self):
+        """Serve requests, after invoking :func:`prepare()`."""
+        while self.ready:
+            try:
+                self.tick()
+            except (KeyboardInterrupt, SystemExit):
+                raise
+            except Exception:
+                self.error_log('Error in HTTPServer.tick', level=logging.ERROR,
+                               traceback=True)
+
+            if self.interrupt:
+                while self.interrupt is True:
+                    # Wait for self.stop() to complete. See _set_interrupt.
+                    time.sleep(0.1)
+                if self.interrupt:
+                    raise self.interrupt
+
+    def start(self):
+        """Run the server forever.
+
+        It is shortcut for invoking :func:`prepare()` then :func:`serve()`.
+        """
+        # We don't have to trap KeyboardInterrupt or SystemExit here,
+        # because cherrypy.server already does so, calling self.stop() for us.
+        # If you're using this server with another framework, you should
+        # trap those exceptions in whatever code block calls start().
+        self.prepare()
+        self.serve()
+
+    def error_log(self, msg='', level=20, traceback=False):
+        """Write error message to log.
+
+        Args:
+            msg (str): error message
+            level (int): logging level
+            traceback (bool): add traceback to output or not
+        """
+        # Override this in subclasses as desired
+        sys.stderr.write(msg + '\n')
+        sys.stderr.flush()
+        if traceback:
+            tblines = traceback_.format_exc()
+            sys.stderr.write(tblines)
+            sys.stderr.flush()
+
+    def bind(self, family, type, proto=0):
+        """Create (or recreate) the actual socket object."""
+        self.socket = socket.socket(family, type, proto)
+        prevent_socket_inheritance(self.socket)
+        if not IS_WINDOWS:
+            # Windows has different semantics for SO_REUSEADDR,
+            # so don't set it.
+            # https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx
+            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        if self.nodelay and not isinstance(self.bind_addr, str):
+            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
+
+        if self.ssl_adapter is not None:
+            self.socket = self.ssl_adapter.bind(self.socket)
+
+        host, port = self.bind_addr[:2]
+
+        # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
+        # activate dual-stack. See
+        # https://github.com/cherrypy/cherrypy/issues/871.
+        listening_ipv6 = (
+            hasattr(socket, 'AF_INET6')
+            and family == socket.AF_INET6
+            and host in ('::', '::0', '::0.0.0.0')
+        )
+        if listening_ipv6:
+            try:
+                self.socket.setsockopt(
+                    socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
+            except (AttributeError, socket.error):
+                # Apparently, the socket option is not available in
+                # this machine's TCP stack
+                pass
+
+        self.socket.bind(self.bind_addr)
+        # TODO: keep requested bind_addr separate real bound_addr (port is
+        # different in case of ephemeral port 0)
+        self.bind_addr = self.socket.getsockname()
+        if family in (
+            # Windows doesn't have socket.AF_UNIX, so not using it in check
+            socket.AF_INET,
+            socket.AF_INET6,
+        ):
+            """UNIX domain sockets are strings or bytes.
+
+            In case of bytes with a leading null-byte it's an abstract socket.
+            """
+            self.bind_addr = self.bind_addr[:2]
+
+    def tick(self):
+        """Accept a new connection and put it on the Queue."""
+        try:
+            s, addr = self.socket.accept()
+            if self.stats['Enabled']:
+                self.stats['Accepts'] += 1
+            if not self.ready:
+                return
+
+            prevent_socket_inheritance(s)
+            if hasattr(s, 'settimeout'):
+                s.settimeout(self.timeout)
+
+            mf = MakeFile
+            ssl_env = {}
+            # if ssl cert and key are set, we try to be a secure HTTP server
+            if self.ssl_adapter is not None:
+                try:
+                    s, ssl_env = self.ssl_adapter.wrap(s)
+                except errors.NoSSLError:
+                    msg = ('The client sent a plain HTTP request, but '
+                           'this server only speaks HTTPS on this port.')
+                    buf = ['%s 400 Bad Request\r\n' % self.protocol,
+                           'Content-Length: %s\r\n' % len(msg),
+                           'Content-Type: text/plain\r\n\r\n',
+                           msg]
+
+                    sock_to_make = s if six.PY3 else s._sock
+                    wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE)
+                    try:
+                        wfile.write(''.join(buf).encode('ISO-8859-1'))
+                    except socket.error as ex:
+                        if ex.args[0] not in errors.socket_errors_to_ignore:
+                            raise
+                    return
+                if not s:
+                    return
+                mf = self.ssl_adapter.makefile
+                # Re-apply our timeout since we may have a new socket object
+                if hasattr(s, 'settimeout'):
+                    s.settimeout(self.timeout)
+
+            conn = self.ConnectionClass(self, s, mf)
+
+            if not isinstance(self.bind_addr, six.string_types):
+                # optional values
+                # Until we do DNS lookups, omit REMOTE_HOST
+                if addr is None:  # sometimes this can happen
+                    # figure out if AF_INET or AF_INET6.
+                    if len(s.getsockname()) == 2:
+                        # AF_INET
+                        addr = ('0.0.0.0', 0)
+                    else:
+                        # AF_INET6
+                        addr = ('::', 0)
+                conn.remote_addr = addr[0]
+                conn.remote_port = addr[1]
+
+            conn.ssl_env = ssl_env
+
+            try:
+                self.requests.put(conn)
+            except queue.Full:
+                # Just drop the conn. TODO: write 503 back?
+                conn.close()
+                return
+        except socket.timeout:
+            # The only reason for the timeout in start() is so we can
+            # notice keyboard interrupts on Win32, which don't interrupt
+            # accept() by default
+            return
+        except socket.error as ex:
+            if self.stats['Enabled']:
+                self.stats['Socket Errors'] += 1
+            if ex.args[0] in errors.socket_error_eintr:
+                # I *think* this is right. EINTR should occur when a signal
+                # is received during the accept() call; all docs say retry
+                # the call, and I *think* I'm reading it right that Python
+                # will then go ahead and poll for and handle the signal
+                # elsewhere. See
+                # https://github.com/cherrypy/cherrypy/issues/707.
+                return
+            if ex.args[0] in errors.socket_errors_nonblocking:
+                # Just try again. See
+                # https://github.com/cherrypy/cherrypy/issues/479.
+                return
+            if ex.args[0] in errors.socket_errors_to_ignore:
+                # Our socket was closed.
+                # See https://github.com/cherrypy/cherrypy/issues/686.
+                return
+            raise
+
+    @property
+    def interrupt(self):
+        """Flag interrupt of the server."""
+        return self._interrupt
+
+    @interrupt.setter
+    def interrupt(self, interrupt):
+        """Perform the shutdown of this server and save the exception."""
+        self._interrupt = True
+        self.stop()
+        self._interrupt = interrupt
+
+    def stop(self):
+        """Gracefully shutdown a server that is serving forever."""
+        self.ready = False
+        if self._start_time is not None:
+            self._run_time += (time.time() - self._start_time)
+        self._start_time = None
+
+        sock = getattr(self, 'socket', None)
+        if sock:
+            if not isinstance(self.bind_addr, six.string_types):
+                # Touch our own socket to make accept() return immediately.
+                try:
+                    host, port = sock.getsockname()[:2]
+                except socket.error as ex:
+                    if ex.args[0] not in errors.socket_errors_to_ignore:
+                        # Changed to use error code and not message
+                        # See
+                        # https://github.com/cherrypy/cherrypy/issues/860.
+                        raise
+                else:
+                    # Note that we're explicitly NOT using AI_PASSIVE,
+                    # here, because we want an actual IP to touch.
+                    # localhost won't work if we've bound to a public IP,
+                    # but it will if we bound to '0.0.0.0' (INADDR_ANY).
+                    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+                                                  socket.SOCK_STREAM):
+                        af, socktype, proto, canonname, sa = res
+                        s = None
+                        try:
+                            s = socket.socket(af, socktype, proto)
+                            # See
+                            # https://groups.google.com/group/cherrypy-users/
+                            #     browse_frm/thread/bbfe5eb39c904fe0
+                            s.settimeout(1.0)
+                            s.connect((host, port))
+                            s.close()
+                        except socket.error:
+                            if s:
+                                s.close()
+            if hasattr(sock, 'close'):
+                sock.close()
+            self.socket = None
+
+        self.requests.stop(self.shutdown_timeout)
+
+
+class Gateway:
+    """Base class to interface HTTPServer with other systems, such as WSGI."""
+
+    def __init__(self, req):
+        """Initialize Gateway instance with request.
+
+        Args:
+            req (HTTPRequest): current HTTP request
+        """
+        self.req = req
+
+    def respond(self):
+        """Process the current request. Must be overridden in a subclass."""
+        raise NotImplementedError
+
+
+# These may either be ssl.Adapter subclasses or the string names
+# of such classes (in which case they will be lazily loaded).
+ssl_adapters = {
+    'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter',
+    'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter',
+}
+
+
+def get_ssl_adapter_class(name='builtin'):
+    """Return an SSL adapter class for the given name."""
+    adapter = ssl_adapters[name.lower()]
+    if isinstance(adapter, six.string_types):
+        last_dot = adapter.rfind('.')
+        attr_name = adapter[last_dot + 1:]
+        mod_path = adapter[:last_dot]
+
+        try:
+            mod = sys.modules[mod_path]
+            if mod is None:
+                raise KeyError()
+        except KeyError:
+            # The last [''] is important.
+            mod = __import__(mod_path, globals(), locals(), [''])
+
+        # Let an AttributeError propagate outward.
+        try:
+            adapter = getattr(mod, attr_name)
+        except AttributeError:
+            raise AttributeError("'%s' object has no attribute '%s'"
+                                 % (mod_path, attr_name))
+
+    return adapter
diff --git a/libraries/cheroot/ssl/__init__.py b/libraries/cheroot/ssl/__init__.py
new file mode 100644
index 00000000..ec1a0d90
--- /dev/null
+++ b/libraries/cheroot/ssl/__init__.py
@@ -0,0 +1,51 @@
+"""Implementation of the SSL adapter base interface."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from abc import ABCMeta, abstractmethod
+
+from six import add_metaclass
+
+
+@add_metaclass(ABCMeta)
+class Adapter:
+    """Base class for SSL driver library adapters.
+
+    Required methods:
+
+        * ``wrap(sock) -> (wrapped socket, ssl environ dict)``
+        * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
+          socket file object``
+    """
+
+    @abstractmethod
+    def __init__(
+            self, certificate, private_key, certificate_chain=None,
+            ciphers=None):
+        """Set up certificates, private key ciphers and reset context."""
+        self.certificate = certificate
+        self.private_key = private_key
+        self.certificate_chain = certificate_chain
+        self.ciphers = ciphers
+        self.context = None
+
+    @abstractmethod
+    def bind(self, sock):
+        """Wrap and return the given socket."""
+        return sock
+
+    @abstractmethod
+    def wrap(self, sock):
+        """Wrap and return the given socket, plus WSGI environ entries."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def get_environ(self):
+        """Return WSGI environ entries to be merged into each request."""
+        raise NotImplementedError
+
+    @abstractmethod
+    def makefile(self, sock, mode='r', bufsize=-1):
+        """Return socket file object."""
+        raise NotImplementedError
diff --git a/libraries/cheroot/ssl/builtin.py b/libraries/cheroot/ssl/builtin.py
new file mode 100644
index 00000000..a19f7eef
--- /dev/null
+++ b/libraries/cheroot/ssl/builtin.py
@@ -0,0 +1,162 @@
+"""
+A library for integrating Python's builtin ``ssl`` library with Cheroot.
+
+The ssl module must be importable for SSL functionality.
+
+To use this module, set ``HTTPServer.ssl_adapter`` to an instance of
+``BuiltinSSLAdapter``.
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+try:
+    import ssl
+except ImportError:
+    ssl = None
+
+try:
+    from _pyio import DEFAULT_BUFFER_SIZE
+except ImportError:
+    try:
+        from io import DEFAULT_BUFFER_SIZE
+    except ImportError:
+        DEFAULT_BUFFER_SIZE = -1
+
+import six
+
+from . import Adapter
+from .. import errors
+from ..makefile import MakeFile
+
+
+if six.PY3:
+    generic_socket_error = OSError
+else:
+    import socket
+    generic_socket_error = socket.error
+    del socket
+
+
+def _assert_ssl_exc_contains(exc, *msgs):
+    """Check whether SSL exception contains either of messages provided."""
+    if len(msgs) < 1:
+        raise TypeError(
+            '_assert_ssl_exc_contains() requires '
+            'at least one message to be passed.'
+        )
+    err_msg_lower = exc.args[1].lower()
+    return any(m.lower() in err_msg_lower for m in msgs)
+
+
+class BuiltinSSLAdapter(Adapter):
+    """A wrapper for integrating Python's builtin ssl module with Cheroot."""
+
+    certificate = None
+    """The filename of the server SSL certificate."""
+
+    private_key = None
+    """The filename of the server's private key file."""
+
+    certificate_chain = None
+    """The filename of the certificate chain file."""
+
+    context = None
+    """The ssl.SSLContext that will be used to wrap sockets."""
+
+    ciphers = None
+    """The ciphers list of SSL."""
+
+    def __init__(
+            self, certificate, private_key, certificate_chain=None,
+            ciphers=None):
+        """Set up context in addition to base class properties if available."""
+        if ssl is None:
+            raise ImportError('You must install the ssl module to use HTTPS.')
+
+        super(BuiltinSSLAdapter, self).__init__(
+            certificate, private_key, certificate_chain, ciphers)
+
+        self.context = ssl.create_default_context(
+            purpose=ssl.Purpose.CLIENT_AUTH,
+            cafile=certificate_chain
+        )
+        self.context.load_cert_chain(certificate, private_key)
+        if self.ciphers is not None:
+            self.context.set_ciphers(ciphers)
+
+    def bind(self, sock):
+        """Wrap and return the given socket."""
+        return super(BuiltinSSLAdapter, self).bind(sock)
+
+    def wrap(self, sock):
+        """Wrap and return the given socket, plus WSGI environ entries."""
+        EMPTY_RESULT = None, {}
+        try:
+            s = self.context.wrap_socket(
+                sock, do_handshake_on_connect=True, server_side=True,
+            )
+        except ssl.SSLError as ex:
+            if ex.errno == ssl.SSL_ERROR_EOF:
+                # This is almost certainly due to the cherrypy engine
+                # 'pinging' the socket to assert it's connectable;
+                # the 'ping' isn't SSL.
+                return EMPTY_RESULT
+            elif ex.errno == ssl.SSL_ERROR_SSL:
+                if _assert_ssl_exc_contains(ex, 'http request'):
+                    # The client is speaking HTTP to an HTTPS server.
+                    raise errors.NoSSLError
+
+                # Check if it's one of the known errors
+                # Errors that are caught by PyOpenSSL, but thrown by
+                # built-in ssl
+                _block_errors = (
+                    'unknown protocol', 'unknown ca', 'unknown_ca',
+                    'unknown error',
+                    'https proxy request', 'inappropriate fallback',
+                    'wrong version number',
+                    'no shared cipher', 'certificate unknown',
+                    'ccs received early',
+                )
+                if _assert_ssl_exc_contains(ex, *_block_errors):
+                    # Accepted error, let's pass
+                    return EMPTY_RESULT
+            elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'):
+                # This error is thrown by builtin SSL after a timeout
+                # when client is speaking HTTP to an HTTPS server.
+                # The connection can safely be dropped.
+                return EMPTY_RESULT
+            raise
+        except generic_socket_error as exc:
+            """It is unclear why exactly this happens.
+
+            It's reproducible only under Python 2 with openssl>1.0 and stdlib
+            ``ssl`` wrapper, and only with CherryPy.
+            So it looks like some healthcheck tries to connect to this socket
+            during startup (from the same process).
+
+
+            Ref: https://github.com/cherrypy/cherrypy/issues/1618
+            """
+            if six.PY2 and exc.args == (0, 'Error'):
+                return EMPTY_RESULT
+            raise
+        return s, self.get_environ(s)
+
+    # TODO: fill this out more with mod ssl env
+    def get_environ(self, sock):
+        """Create WSGI environ entries to be merged into each request."""
+        cipher = sock.cipher()
+        ssl_environ = {
+            'wsgi.url_scheme': 'https',
+            'HTTPS': 'on',
+            'SSL_PROTOCOL': cipher[1],
+            'SSL_CIPHER': cipher[0]
+            # SSL_VERSION_INTERFACE     string  The mod_ssl program version
+            # SSL_VERSION_LIBRARY   string  The OpenSSL program version
+        }
+        return ssl_environ
+
+    def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
+        """Return socket file object."""
+        return MakeFile(sock, mode, bufsize)
diff --git a/libraries/cheroot/ssl/pyopenssl.py b/libraries/cheroot/ssl/pyopenssl.py
new file mode 100644
index 00000000..2185f851
--- /dev/null
+++ b/libraries/cheroot/ssl/pyopenssl.py
@@ -0,0 +1,267 @@
+"""
+A library for integrating pyOpenSSL with Cheroot.
+
+The OpenSSL module must be importable for SSL functionality.
+You can obtain it from `here <https://launchpad.net/pyopenssl>`_.
+
+To use this module, set HTTPServer.ssl_adapter to an instance of
+ssl.Adapter. There are two ways to use SSL:
+
+Method One
+----------
+
+ * ``ssl_adapter.context``: an instance of SSL.Context.
+
+If this is not None, it is assumed to be an SSL.Context instance,
+and will be passed to SSL.Connection on bind(). The developer is
+responsible for forming a valid Context object. This approach is
+to be preferred for more flexibility, e.g. if the cert and key are
+streams instead of files, or need decryption, or SSL.SSLv3_METHOD
+is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
+the pyOpenSSL documentation for complete options.
+
+Method Two (shortcut)
+---------------------
+
+ * ``ssl_adapter.certificate``: the filename of the server SSL certificate.
+ * ``ssl_adapter.private_key``: the filename of the server's private key file.
+
+Both are None by default. If ssl_adapter.context is None, but .private_key
+and .certificate are both given and valid, they will be read, and the
+context will be automatically created from them.
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import socket
+import threading
+import time
+
+try:
+    from OpenSSL import SSL
+    from OpenSSL import crypto
+except ImportError:
+    SSL = None
+
+from . import Adapter
+from .. import errors, server as cheroot_server
+from ..makefile import MakeFile
+
+
+class SSL_fileobject(MakeFile):
+    """SSL file object attached to a socket object."""
+
+    ssl_timeout = 3
+    ssl_retry = .01
+
+    def _safe_call(self, is_reader, call, *args, **kwargs):
+        """Wrap the given call with SSL error-trapping.
+
+        is_reader: if False EOF errors will be raised. If True, EOF errors
+        will return "" (to emulate normal sockets).
+        """
+        start = time.time()
+        while True:
+            try:
+                return call(*args, **kwargs)
+            except SSL.WantReadError:
+                # Sleep and try again. This is dangerous, because it means
+                # the rest of the stack has no way of differentiating
+                # between a "new handshake" error and "client dropped".
+                # Note this isn't an endless loop: there's a timeout below.
+                time.sleep(self.ssl_retry)
+            except SSL.WantWriteError:
+                time.sleep(self.ssl_retry)
+            except SSL.SysCallError as e:
+                if is_reader and e.args == (-1, 'Unexpected EOF'):
+                    return ''
+
+                errnum = e.args[0]
+                if is_reader and errnum in errors.socket_errors_to_ignore:
+                    return ''
+                raise socket.error(errnum)
+            except SSL.Error as e:
+                if is_reader and e.args == (-1, 'Unexpected EOF'):
+                    return ''
+
+                thirdarg = None
+                try:
+                    thirdarg = e.args[0][0][2]
+                except IndexError:
+                    pass
+
+                if thirdarg == 'http request':
+                    # The client is talking HTTP to an HTTPS server.
+                    raise errors.NoSSLError()
+
+                raise errors.FatalSSLAlert(*e.args)
+
+            if time.time() - start > self.ssl_timeout:
+                raise socket.timeout('timed out')
+
+    def recv(self, size):
+        """Receive message of a size from the socket."""
+        return self._safe_call(True, super(SSL_fileobject, self).recv, size)
+
+    def sendall(self, *args, **kwargs):
+        """Send whole message to the socket."""
+        return self._safe_call(False, super(SSL_fileobject, self).sendall,
+                               *args, **kwargs)
+
+    def send(self, *args, **kwargs):
+        """Send some part of message to the socket."""
+        return self._safe_call(False, super(SSL_fileobject, self).send,
+                               *args, **kwargs)
+
+
+class SSLConnection:
+    """A thread-safe wrapper for an SSL.Connection.
+
+    ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
+    """
+
+    def __init__(self, *args):
+        """Initialize SSLConnection instance."""
+        self._ssl_conn = SSL.Connection(*args)
+        self._lock = threading.RLock()
+
+    for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
+              'renegotiate', 'bind', 'listen', 'connect', 'accept',
+              'setblocking', 'fileno', 'close', 'get_cipher_list',
+              'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
+              'makefile', 'get_app_data', 'set_app_data', 'state_string',
+              'sock_shutdown', 'get_peer_certificate', 'want_read',
+              'want_write', 'set_connect_state', 'set_accept_state',
+              'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
+        exec("""def %s(self, *args):
+        self._lock.acquire()
+        try:
+            return self._ssl_conn.%s(*args)
+        finally:
+            self._lock.release()
+""" % (f, f))
+
+    def shutdown(self, *args):
+        """Shutdown the SSL connection.
+
+        Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args.
+        """
+        self._lock.acquire()
+        try:
+            return self._ssl_conn.shutdown()
+        finally:
+            self._lock.release()
+
+
+class pyOpenSSLAdapter(Adapter):
+    """A wrapper for integrating pyOpenSSL with Cheroot."""
+
+    certificate = None
+    """The filename of the server SSL certificate."""
+
+    private_key = None
+    """The filename of the server's private key file."""
+
+    certificate_chain = None
+    """Optional. The filename of CA's intermediate certificate bundle.
+
+    This is needed for cheaper "chained root" SSL certificates, and should be
+    left as None if not required."""
+
+    context = None
+    """An instance of SSL.Context."""
+
+    ciphers = None
+    """The ciphers list of SSL."""
+
+    def __init__(
+            self, certificate, private_key, certificate_chain=None,
+            ciphers=None):
+        """Initialize OpenSSL Adapter instance."""
+        if SSL is None:
+            raise ImportError('You must install pyOpenSSL to use HTTPS.')
+
+        super(pyOpenSSLAdapter, self).__init__(
+            certificate, private_key, certificate_chain, ciphers)
+
+        self._environ = None
+
+    def bind(self, sock):
+        """Wrap and return the given socket."""
+        if self.context is None:
+            self.context = self.get_context()
+        conn = SSLConnection(self.context, sock)
+        self._environ = self.get_environ()
+        return conn
+
+    def wrap(self, sock):
+        """Wrap and return the given socket, plus WSGI environ entries."""
+        return sock, self._environ.copy()
+
+    def get_context(self):
+        """Return an SSL.Context from self attributes."""
+        # See https://code.activestate.com/recipes/442473/
+        c = SSL.Context(SSL.SSLv23_METHOD)
+        c.use_privatekey_file(self.private_key)
+        if self.certificate_chain:
+            c.load_verify_locations(self.certificate_chain)
+        c.use_certificate_file(self.certificate)
+        return c
+
+    def get_environ(self):
+        """Return WSGI environ entries to be merged into each request."""
+        ssl_environ = {
+            'HTTPS': 'on',
+            # pyOpenSSL doesn't provide access to any of these AFAICT
+            # 'SSL_PROTOCOL': 'SSLv2',
+            # SSL_CIPHER    string  The cipher specification name
+            # SSL_VERSION_INTERFACE     string  The mod_ssl program version
+            # SSL_VERSION_LIBRARY   string  The OpenSSL program version
+        }
+
+        if self.certificate:
+            # Server certificate attributes
+            cert = open(self.certificate, 'rb').read()
+            cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
+            ssl_environ.update({
+                'SSL_SERVER_M_VERSION': cert.get_version(),
+                'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
+                # 'SSL_SERVER_V_START':
+                #   Validity of server's certificate (start time),
+                # 'SSL_SERVER_V_END':
+                #   Validity of server's certificate (end time),
+            })
+
+            for prefix, dn in [('I', cert.get_issuer()),
+                               ('S', cert.get_subject())]:
+                # X509Name objects don't seem to have a way to get the
+                # complete DN string. Use str() and slice it instead,
+                # because str(dn) == "<X509Name object '/C=US/ST=...'>"
+                dnstr = str(dn)[18:-2]
+
+                wsgikey = 'SSL_SERVER_%s_DN' % prefix
+                ssl_environ[wsgikey] = dnstr
+
+                # The DN should be of the form: /k1=v1/k2=v2, but we must allow
+                # for any value to contain slashes itself (in a URL).
+                while dnstr:
+                    pos = dnstr.rfind('=')
+                    dnstr, value = dnstr[:pos], dnstr[pos + 1:]
+                    pos = dnstr.rfind('/')
+                    dnstr, key = dnstr[:pos], dnstr[pos + 1:]
+                    if key and value:
+                        wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
+                        ssl_environ[wsgikey] = value
+
+        return ssl_environ
+
+    def makefile(self, sock, mode='r', bufsize=-1):
+        """Return socket file object."""
+        if SSL and isinstance(sock, SSL.ConnectionType):
+            timeout = sock.gettimeout()
+            f = SSL_fileobject(sock, mode, bufsize)
+            f.ssl_timeout = timeout
+            return f
+        else:
+            return cheroot_server.CP_fileobject(sock, mode, bufsize)
diff --git a/libraries/cheroot/test/__init__.py b/libraries/cheroot/test/__init__.py
new file mode 100644
index 00000000..e2a7b348
--- /dev/null
+++ b/libraries/cheroot/test/__init__.py
@@ -0,0 +1 @@
+"""Cheroot test suite."""
diff --git a/libraries/cheroot/test/conftest.py b/libraries/cheroot/test/conftest.py
new file mode 100644
index 00000000..9f5f9284
--- /dev/null
+++ b/libraries/cheroot/test/conftest.py
@@ -0,0 +1,27 @@
+"""Pytest configuration module.
+
+Contains fixtures, which are tightly bound to the Cheroot framework
+itself, useless for end-users' app testing.
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+
+from ..testing import (  # noqa: F401
+    native_server, wsgi_server,
+)
+from ..testing import get_server_client
+
+
+@pytest.fixture  # noqa: F811
+def wsgi_server_client(wsgi_server):
+    """Create a test client out of given WSGI server."""
+    return get_server_client(wsgi_server)
+
+
+@pytest.fixture  # noqa: F811
+def native_server_client(native_server):
+    """Create a test client out of given HTTP server."""
+    return get_server_client(native_server)
diff --git a/libraries/cheroot/test/helper.py b/libraries/cheroot/test/helper.py
new file mode 100644
index 00000000..38f40b26
--- /dev/null
+++ b/libraries/cheroot/test/helper.py
@@ -0,0 +1,169 @@
+"""A library of helper functions for the Cheroot test suite."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import datetime
+import logging
+import os
+import sys
+import time
+import threading
+import types
+
+from six.moves import http_client
+
+import six
+
+import cheroot.server
+import cheroot.wsgi
+
+from cheroot.test import webtest
+
+log = logging.getLogger(__name__)
+thisdir = os.path.abspath(os.path.dirname(__file__))
+serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
+
+
+config = {
+    'bind_addr': ('127.0.0.1', 54583),
+    'server': 'wsgi',
+    'wsgi_app': None,
+}
+
+
+class CherootWebCase(webtest.WebCase):
+    """Helper class for a web app test suite."""
+
+    script_name = ''
+    scheme = 'http'
+
+    available_servers = {
+        'wsgi': cheroot.wsgi.Server,
+        'native': cheroot.server.HTTPServer,
+    }
+
+    @classmethod
+    def setup_class(cls):
+        """Create and run one HTTP server per class."""
+        conf = config.copy()
+        conf.update(getattr(cls, 'config', {}))
+
+        s_class = conf.pop('server', 'wsgi')
+        server_factory = cls.available_servers.get(s_class)
+        if server_factory is None:
+            raise RuntimeError('Unknown server in config: %s' % conf['server'])
+        cls.httpserver = server_factory(**conf)
+
+        cls.HOST, cls.PORT = cls.httpserver.bind_addr
+        if cls.httpserver.ssl_adapter is None:
+            ssl = ''
+            cls.scheme = 'http'
+        else:
+            ssl = ' (ssl)'
+            cls.HTTP_CONN = http_client.HTTPSConnection
+            cls.scheme = 'https'
+
+        v = sys.version.split()[0]
+        log.info('Python version used to run this test script: %s' % v)
+        log.info('Cheroot version: %s' % cheroot.__version__)
+        log.info('HTTP server version: %s%s' % (cls.httpserver.protocol, ssl))
+        log.info('PID: %s' % os.getpid())
+
+        if hasattr(cls, 'setup_server'):
+            # Clear the wsgi server so that
+            # it can be updated with the new root
+            cls.setup_server()
+            cls.start()
+
+    @classmethod
+    def teardown_class(cls):
+        """Cleanup HTTP server."""
+        if hasattr(cls, 'setup_server'):
+            cls.stop()
+
+    @classmethod
+    def start(cls):
+        """Load and start the HTTP server."""
+        threading.Thread(target=cls.httpserver.safe_start).start()
+        while not cls.httpserver.ready:
+            time.sleep(0.1)
+
+    @classmethod
+    def stop(cls):
+        """Terminate HTTP server."""
+        cls.httpserver.stop()
+        td = getattr(cls, 'teardown', None)
+        if td:
+            td()
+
+    date_tolerance = 2
+
+    def assertEqualDates(self, dt1, dt2, seconds=None):
+        """Assert abs(dt1 - dt2) is within Y seconds."""
+        if seconds is None:
+            seconds = self.date_tolerance
+
+        if dt1 > dt2:
+            diff = dt1 - dt2
+        else:
+            diff = dt2 - dt1
+        if not diff < datetime.timedelta(seconds=seconds):
+            raise AssertionError('%r and %r are not within %r seconds.' %
+                                 (dt1, dt2, seconds))
+
+
+class Request:
+    """HTTP request container."""
+
+    def __init__(self, environ):
+        """Initialize HTTP request."""
+        self.environ = environ
+
+
+class Response:
+    """HTTP response container."""
+
+    def __init__(self):
+        """Initialize HTTP response."""
+        self.status = '200 OK'
+        self.headers = {'Content-Type': 'text/html'}
+        self.body = None
+
+    def output(self):
+        """Generate iterable response body object."""
+        if self.body is None:
+            return []
+        elif isinstance(self.body, six.text_type):
+            return [self.body.encode('iso-8859-1')]
+        elif isinstance(self.body, six.binary_type):
+            return [self.body]
+        else:
+            return [x.encode('iso-8859-1') for x in self.body]
+
+
+class Controller:
+    """WSGI app for tests."""
+
+    def __call__(self, environ, start_response):
+        """WSGI request handler."""
+        req, resp = Request(environ), Response()
+        try:
+            # Python 3 supports unicode attribute names
+            # Python 2 encodes them
+            handler = self.handlers[environ['PATH_INFO']]
+        except KeyError:
+            resp.status = '404 Not Found'
+        else:
+            output = handler(req, resp)
+            if (output is not None and
+                    not any(resp.status.startswith(status_code)
+                            for status_code in ('204', '304'))):
+                resp.body = output
+                try:
+                    resp.headers.setdefault('Content-Length', str(len(output)))
+                except TypeError:
+                    if not isinstance(output, types.GeneratorType):
+                        raise
+        start_response(resp.status, resp.headers.items())
+        return resp.output()
diff --git a/libraries/cheroot/test/test.pem b/libraries/cheroot/test/test.pem
new file mode 100644
index 00000000..47a47042
--- /dev/null
+++ b/libraries/cheroot/test/test.pem
@@ -0,0 +1,38 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
+R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
+da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
+AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
+9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
+enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
+8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
+tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
+0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
+MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
+yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
+8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
+yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
+MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
+MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
+cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
+bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
+FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
+cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
+ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
+C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
+KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
+2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
+/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
+MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
+CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
+BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
+8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
+D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
+NluCaWQys3MS
+-----END CERTIFICATE-----
diff --git a/libraries/cheroot/test/test__compat.py b/libraries/cheroot/test/test__compat.py
new file mode 100644
index 00000000..d34e5eb8
--- /dev/null
+++ b/libraries/cheroot/test/test__compat.py
@@ -0,0 +1,49 @@
+# -*- coding: utf-8 -*-
+"""Test suite for cross-python compatibility helpers."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pytest
+import six
+
+from cheroot._compat import ntob, ntou, bton
+
+
+@pytest.mark.parametrize(
+    'func,inp,out',
+    [
+        (ntob, 'bar', b'bar'),
+        (ntou, 'bar', u'bar'),
+        (bton, b'bar', 'bar'),
+    ],
+)
+def test_compat_functions_positive(func, inp, out):
+    """Check that compat functions work with correct input."""
+    assert func(inp, encoding='utf-8') == out
+
+
+@pytest.mark.parametrize(
+    'func',
+    [
+        ntob,
+        ntou,
+    ],
+)
+def test_compat_functions_negative_nonnative(func):
+    """Check that compat functions fail loudly for incorrect input."""
+    non_native_test_str = b'bar' if six.PY3 else u'bar'
+    with pytest.raises(TypeError):
+        func(non_native_test_str, encoding='utf-8')
+
+
+@pytest.mark.skip(reason='This test does not work now')
+@pytest.mark.skipif(
+    six.PY3,
+    reason='This code path only appears in Python 2 version.',
+)
+def test_ntou_escape():
+    """Check that ntou supports escape-encoding under Python 2."""
+    expected = u''
+    actual = ntou('hi'.encode('ISO-8859-1'), encoding='escape')
+    assert actual == expected
diff --git a/libraries/cheroot/test/test_conn.py b/libraries/cheroot/test/test_conn.py
new file mode 100644
index 00000000..f543dd9b
--- /dev/null
+++ b/libraries/cheroot/test/test_conn.py
@@ -0,0 +1,897 @@
+"""Tests for TCP connection handling, including proper and timely close."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import socket
+import time
+
+from six.moves import range, http_client, urllib
+
+import six
+import pytest
+
+from cheroot.test import helper, webtest
+
+
+timeout = 1
+pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
+
+
+class Controller(helper.Controller):
+    """Controller for serving WSGI apps."""
+
+    def hello(req, resp):
+        """Render Hello world."""
+        return 'Hello, world!'
+
+    def pov(req, resp):
+        """Render pov value."""
+        return pov
+
+    def stream(req, resp):
+        """Render streaming response."""
+        if 'set_cl' in req.environ['QUERY_STRING']:
+            resp.headers['Content-Length'] = str(10)
+
+        def content():
+            for x in range(10):
+                yield str(x)
+
+        return content()
+
+    def upload(req, resp):
+        """Process file upload and render thank."""
+        if not req.environ['REQUEST_METHOD'] == 'POST':
+            raise AssertionError("'POST' != request.method %r" %
+                                 req.environ['REQUEST_METHOD'])
+        return "thanks for '%s'" % req.environ['wsgi.input'].read()
+
+    def custom_204(req, resp):
+        """Render response with status 204."""
+        resp.status = '204'
+        return 'Code = 204'
+
+    def custom_304(req, resp):
+        """Render response with status 304."""
+        resp.status = '304'
+        return 'Code = 304'
+
+    def err_before_read(req, resp):
+        """Render response with status 500."""
+        resp.status = '500 Internal Server Error'
+        return 'ok'
+
+    def one_megabyte_of_a(req, resp):
+        """Render 1MB response."""
+        return ['a' * 1024] * 1024
+
+    def wrong_cl_buffered(req, resp):
+        """Render buffered response with invalid length value."""
+        resp.headers['Content-Length'] = '5'
+        return 'I have too many bytes'
+
+    def wrong_cl_unbuffered(req, resp):
+        """Render unbuffered response with invalid length value."""
+        resp.headers['Content-Length'] = '5'
+        return ['I too', ' have too many bytes']
+
+    def _munge(string):
+        """Encode PATH_INFO correctly depending on Python version.
+
+        WSGI 1.0 is a mess around unicode. Create endpoints
+        that match the PATH_INFO that it produces.
+        """
+        if six.PY3:
+            return string.encode('utf-8').decode('latin-1')
+        return string
+
+    handlers = {
+        '/hello': hello,
+        '/pov': pov,
+        '/page1': pov,
+        '/page2': pov,
+        '/page3': pov,
+        '/stream': stream,
+        '/upload': upload,
+        '/custom/204': custom_204,
+        '/custom/304': custom_304,
+        '/err_before_read': err_before_read,
+        '/one_megabyte_of_a': one_megabyte_of_a,
+        '/wrong_cl_buffered': wrong_cl_buffered,
+        '/wrong_cl_unbuffered': wrong_cl_unbuffered,
+    }
+
+
+@pytest.fixture
+def testing_server(wsgi_server_client):
+    """Attach a WSGI app to the given server and pre-configure it."""
+    app = Controller()
+
+    def _timeout(req, resp):
+        return str(wsgi_server.timeout)
+    app.handlers['/timeout'] = _timeout
+    wsgi_server = wsgi_server_client.server_instance
+    wsgi_server.wsgi_app = app
+    wsgi_server.max_request_body_size = 1001
+    wsgi_server.timeout = timeout
+    wsgi_server.server_client = wsgi_server_client
+    return wsgi_server
+
+
+@pytest.fixture
+def test_client(testing_server):
+    """Get and return a test client out of the given server."""
+    return testing_server.server_client
+
+
+def header_exists(header_name, headers):
+    """Check that a header is present."""
+    return header_name.lower() in (k.lower() for (k, _) in headers)
+
+
+def header_has_value(header_name, header_value, headers):
+    """Check that a header with a given value is present."""
+    return header_name.lower() in (k.lower() for (k, v) in headers
+                                   if v == header_value)
+
+
+def test_HTTP11_persistent_connections(test_client):
+    """Test persistent HTTP/1.1 connections."""
+    # Initialize a persistent HTTP connection
+    http_connection = test_client.get_connection()
+    http_connection.auto_open = False
+    http_connection.connect()
+
+    # Make the first request and assert there's no "Connection: close".
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/pov', http_conn=http_connection
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert not header_exists('Connection', actual_headers)
+
+    # Make another request on the same connection.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/page1', http_conn=http_connection
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert not header_exists('Connection', actual_headers)
+
+    # Test client-side close.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/page2', http_conn=http_connection,
+        headers=[('Connection', 'close')]
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert header_has_value('Connection', 'close', actual_headers)
+
+    # Make another request on the same connection, which should error.
+    with pytest.raises(http_client.NotConnected):
+        test_client.get('/pov', http_conn=http_connection)
+
+
+@pytest.mark.parametrize(
+    'set_cl',
+    (
+        False,  # Without Content-Length
+        True,  # With Content-Length
+    )
+)
+def test_streaming_11(test_client, set_cl):
+    """Test serving of streaming responses with HTTP/1.1 protocol."""
+    # Initialize a persistent HTTP connection
+    http_connection = test_client.get_connection()
+    http_connection.auto_open = False
+    http_connection.connect()
+
+    # Make the first request and assert there's no "Connection: close".
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/pov', http_conn=http_connection
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert not header_exists('Connection', actual_headers)
+
+    # Make another, streamed request on the same connection.
+    if set_cl:
+        # When a Content-Length is provided, the content should stream
+        # without closing the connection.
+        status_line, actual_headers, actual_resp_body = test_client.get(
+            '/stream?set_cl=Yes', http_conn=http_connection
+        )
+        assert header_exists('Content-Length', actual_headers)
+        assert not header_has_value('Connection', 'close', actual_headers)
+        assert not header_exists('Transfer-Encoding', actual_headers)
+
+        assert actual_status == 200
+        assert status_line[4:] == 'OK'
+        assert actual_resp_body == b'0123456789'
+    else:
+        # When no Content-Length response header is provided,
+        # streamed output will either close the connection, or use
+        # chunked encoding, to determine transfer-length.
+        status_line, actual_headers, actual_resp_body = test_client.get(
+            '/stream', http_conn=http_connection
+        )
+        assert not header_exists('Content-Length', actual_headers)
+        assert actual_status == 200
+        assert status_line[4:] == 'OK'
+        assert actual_resp_body == b'0123456789'
+
+        chunked_response = False
+        for k, v in actual_headers:
+            if k.lower() == 'transfer-encoding':
+                if str(v) == 'chunked':
+                    chunked_response = True
+
+        if chunked_response:
+            assert not header_has_value('Connection', 'close', actual_headers)
+        else:
+            assert header_has_value('Connection', 'close', actual_headers)
+
+            # Make another request on the same connection, which should
+            # error.
+            with pytest.raises(http_client.NotConnected):
+                test_client.get('/pov', http_conn=http_connection)
+
+        # Try HEAD.
+        # See https://www.bitbucket.org/cherrypy/cherrypy/issue/864.
+        # TODO: figure out how can this be possible on an closed connection
+        # (chunked_response case)
+        status_line, actual_headers, actual_resp_body = test_client.head(
+            '/stream', http_conn=http_connection
+        )
+        assert actual_status == 200
+        assert status_line[4:] == 'OK'
+        assert actual_resp_body == b''
+        assert not header_exists('Transfer-Encoding', actual_headers)
+
+
+@pytest.mark.parametrize(
+    'set_cl',
+    (
+        False,  # Without Content-Length
+        True,  # With Content-Length
+    )
+)
+def test_streaming_10(test_client, set_cl):
+    """Test serving of streaming responses with HTTP/1.0 protocol."""
+    original_server_protocol = test_client.server_instance.protocol
+    test_client.server_instance.protocol = 'HTTP/1.0'
+
+    # Initialize a persistent HTTP connection
+    http_connection = test_client.get_connection()
+    http_connection.auto_open = False
+    http_connection.connect()
+
+    # Make the first request and assert Keep-Alive.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/pov', http_conn=http_connection,
+        headers=[('Connection', 'Keep-Alive')],
+        protocol='HTTP/1.0',
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert header_has_value('Connection', 'Keep-Alive', actual_headers)
+
+    # Make another, streamed request on the same connection.
+    if set_cl:
+        # When a Content-Length is provided, the content should
+        # stream without closing the connection.
+        status_line, actual_headers, actual_resp_body = test_client.get(
+            '/stream?set_cl=Yes', http_conn=http_connection,
+            headers=[('Connection', 'Keep-Alive')],
+            protocol='HTTP/1.0',
+        )
+        actual_status = int(status_line[:3])
+        assert actual_status == 200
+        assert status_line[4:] == 'OK'
+        assert actual_resp_body == b'0123456789'
+
+        assert header_exists('Content-Length', actual_headers)
+        assert header_has_value('Connection', 'Keep-Alive', actual_headers)
+        assert not header_exists('Transfer-Encoding', actual_headers)
+    else:
+        # When a Content-Length is not provided,
+        # the server should close the connection.
+        status_line, actual_headers, actual_resp_body = test_client.get(
+            '/stream', http_conn=http_connection,
+            headers=[('Connection', 'Keep-Alive')],
+            protocol='HTTP/1.0',
+        )
+        actual_status = int(status_line[:3])
+        assert actual_status == 200
+        assert status_line[4:] == 'OK'
+        assert actual_resp_body == b'0123456789'
+
+        assert not header_exists('Content-Length', actual_headers)
+        assert not header_has_value('Connection', 'Keep-Alive', actual_headers)
+        assert not header_exists('Transfer-Encoding', actual_headers)
+
+        # Make another request on the same connection, which should error.
+        with pytest.raises(http_client.NotConnected):
+            test_client.get(
+                '/pov', http_conn=http_connection,
+                protocol='HTTP/1.0',
+            )
+
+    test_client.server_instance.protocol = original_server_protocol
+
+
+@pytest.mark.parametrize(
+    'http_server_protocol',
+    (
+        'HTTP/1.0',
+        'HTTP/1.1',
+    )
+)
+def test_keepalive(test_client, http_server_protocol):
+    """Test Keep-Alive enabled connections."""
+    original_server_protocol = test_client.server_instance.protocol
+    test_client.server_instance.protocol = http_server_protocol
+
+    http_client_protocol = 'HTTP/1.0'
+
+    # Initialize a persistent HTTP connection
+    http_connection = test_client.get_connection()
+    http_connection.auto_open = False
+    http_connection.connect()
+
+    # Test a normal HTTP/1.0 request.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/page2',
+        protocol=http_client_protocol,
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert not header_exists('Connection', actual_headers)
+
+    # Test a keep-alive HTTP/1.0 request.
+
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/page3', headers=[('Connection', 'Keep-Alive')],
+        http_conn=http_connection, protocol=http_client_protocol,
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert header_has_value('Connection', 'Keep-Alive', actual_headers)
+
+    # Remove the keep-alive header again.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/page3', http_conn=http_connection,
+        protocol=http_client_protocol,
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert not header_exists('Connection', actual_headers)
+
+    test_client.server_instance.protocol = original_server_protocol
+
+
+@pytest.mark.parametrize(
+    'timeout_before_headers',
+    (
+        True,
+        False,
+    )
+)
+def test_HTTP11_Timeout(test_client, timeout_before_headers):
+    """Check timeout without sending any data.
+
+    The server will close the conn with a 408.
+    """
+    conn = test_client.get_connection()
+    conn.auto_open = False
+    conn.connect()
+
+    if not timeout_before_headers:
+        # Connect but send half the headers only.
+        conn.send(b'GET /hello HTTP/1.1')
+        conn.send(('Host: %s' % conn.host).encode('ascii'))
+    # else: Connect but send nothing.
+
+    # Wait for our socket timeout
+    time.sleep(timeout * 2)
+
+    # The request should have returned 408 already.
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    assert response.status == 408
+    conn.close()
+
+
+def test_HTTP11_Timeout_after_request(test_client):
+    """Check timeout after at least one request has succeeded.
+
+    The server should close the connection without 408.
+    """
+    fail_msg = "Writing to timed out socket didn't fail as it should have: %s"
+
+    # Make an initial request
+    conn = test_client.get_connection()
+    conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.endheaders()
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    assert response.status == 200
+    actual_body = response.read()
+    expected_body = str(timeout).encode()
+    assert actual_body == expected_body
+
+    # Make a second request on the same socket
+    conn._output(b'GET /hello HTTP/1.1')
+    conn._output(('Host: %s' % conn.host).encode('ascii'))
+    conn._send_output()
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    assert response.status == 200
+    actual_body = response.read()
+    expected_body = b'Hello, world!'
+    assert actual_body == expected_body
+
+    # Wait for our socket timeout
+    time.sleep(timeout * 2)
+
+    # Make another request on the same socket, which should error
+    conn._output(b'GET /hello HTTP/1.1')
+    conn._output(('Host: %s' % conn.host).encode('ascii'))
+    conn._send_output()
+    response = conn.response_class(conn.sock, method='GET')
+    try:
+        response.begin()
+    except (socket.error, http_client.BadStatusLine):
+        pass
+    except Exception as ex:
+        pytest.fail(fail_msg % ex)
+    else:
+        if response.status != 408:
+            pytest.fail(fail_msg % response.read())
+
+    conn.close()
+
+    # Make another request on a new socket, which should work
+    conn = test_client.get_connection()
+    conn.putrequest('GET', '/pov', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.endheaders()
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    assert response.status == 200
+    actual_body = response.read()
+    expected_body = pov.encode()
+    assert actual_body == expected_body
+
+    # Make another request on the same socket,
+    # but timeout on the headers
+    conn.send(b'GET /hello HTTP/1.1')
+    # Wait for our socket timeout
+    time.sleep(timeout * 2)
+    response = conn.response_class(conn.sock, method='GET')
+    try:
+        response.begin()
+    except (socket.error, http_client.BadStatusLine):
+        pass
+    except Exception as ex:
+        pytest.fail(fail_msg % ex)
+    else:
+        if response.status != 408:
+            pytest.fail(fail_msg % response.read())
+
+    conn.close()
+
+    # Retry the request on a new connection, which should work
+    conn = test_client.get_connection()
+    conn.putrequest('GET', '/pov', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.endheaders()
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    assert response.status == 200
+    actual_body = response.read()
+    expected_body = pov.encode()
+    assert actual_body == expected_body
+    conn.close()
+
+
+def test_HTTP11_pipelining(test_client):
+    """Test HTTP/1.1 pipelining.
+
+    httplib doesn't support this directly.
+    """
+    conn = test_client.get_connection()
+
+    # Put request 1
+    conn.putrequest('GET', '/hello', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.endheaders()
+
+    for trial in range(5):
+        # Put next request
+        conn._output(
+            ('GET /hello?%s HTTP/1.1' % trial).encode('iso-8859-1')
+        )
+        conn._output(('Host: %s' % conn.host).encode('ascii'))
+        conn._send_output()
+
+        # Retrieve previous response
+        response = conn.response_class(conn.sock, method='GET')
+        # there is a bug in python3 regarding the buffering of
+        # ``conn.sock``. Until that bug get's fixed we will
+        # monkey patch the ``reponse`` instance.
+        # https://bugs.python.org/issue23377
+        if six.PY3:
+            response.fp = conn.sock.makefile('rb', 0)
+        response.begin()
+        body = response.read(13)
+        assert response.status == 200
+        assert body == b'Hello, world!'
+
+    # Retrieve final response
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    body = response.read()
+    assert response.status == 200
+    assert body == b'Hello, world!'
+
+    conn.close()
+
+
+def test_100_Continue(test_client):
+    """Test 100-continue header processing."""
+    conn = test_client.get_connection()
+
+    # Try a page without an Expect request header first.
+    # Note that httplib's response.begin automatically ignores
+    # 100 Continue responses, so we must manually check for it.
+    conn.putrequest('POST', '/upload', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.putheader('Content-Type', 'text/plain')
+    conn.putheader('Content-Length', '4')
+    conn.endheaders()
+    conn.send(b"d'oh")
+    response = conn.response_class(conn.sock, method='POST')
+    version, status, reason = response._read_status()
+    assert status != 100
+    conn.close()
+
+    # Now try a page with an Expect header...
+    conn.connect()
+    conn.putrequest('POST', '/upload', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.putheader('Content-Type', 'text/plain')
+    conn.putheader('Content-Length', '17')
+    conn.putheader('Expect', '100-continue')
+    conn.endheaders()
+    response = conn.response_class(conn.sock, method='POST')
+
+    # ...assert and then skip the 100 response
+    version, status, reason = response._read_status()
+    assert status == 100
+    while True:
+        line = response.fp.readline().strip()
+        if line:
+            pytest.fail(
+                '100 Continue should not output any headers. Got %r' %
+                line)
+        else:
+            break
+
+    # ...send the body
+    body = b'I am a small file'
+    conn.send(body)
+
+    # ...get the final response
+    response.begin()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    expected_resp_body = ("thanks for '%s'" % body).encode()
+    assert actual_resp_body == expected_resp_body
+    conn.close()
+
+
+@pytest.mark.parametrize(
+    'max_request_body_size',
+    (
+        0,
+        1001,
+    )
+)
+def test_readall_or_close(test_client, max_request_body_size):
+    """Test a max_request_body_size of 0 (the default) and 1001."""
+    old_max = test_client.server_instance.max_request_body_size
+
+    test_client.server_instance.max_request_body_size = max_request_body_size
+
+    conn = test_client.get_connection()
+
+    # Get a POST page with an error
+    conn.putrequest('POST', '/err_before_read', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.putheader('Content-Type', 'text/plain')
+    conn.putheader('Content-Length', '1000')
+    conn.putheader('Expect', '100-continue')
+    conn.endheaders()
+    response = conn.response_class(conn.sock, method='POST')
+
+    # ...assert and then skip the 100 response
+    version, status, reason = response._read_status()
+    assert status == 100
+    skip = True
+    while skip:
+        skip = response.fp.readline().strip()
+
+    # ...send the body
+    conn.send(b'x' * 1000)
+
+    # ...get the final response
+    response.begin()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+    assert actual_status == 500
+
+    # Now try a working page with an Expect header...
+    conn._output(b'POST /upload HTTP/1.1')
+    conn._output(('Host: %s' % conn.host).encode('ascii'))
+    conn._output(b'Content-Type: text/plain')
+    conn._output(b'Content-Length: 17')
+    conn._output(b'Expect: 100-continue')
+    conn._send_output()
+    response = conn.response_class(conn.sock, method='POST')
+
+    # ...assert and then skip the 100 response
+    version, status, reason = response._read_status()
+    assert status == 100
+    skip = True
+    while skip:
+        skip = response.fp.readline().strip()
+
+    # ...send the body
+    body = b'I am a small file'
+    conn.send(body)
+
+    # ...get the final response
+    response.begin()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    expected_resp_body = ("thanks for '%s'" % body).encode()
+    assert actual_resp_body == expected_resp_body
+    conn.close()
+
+    test_client.server_instance.max_request_body_size = old_max
+
+
+def test_No_Message_Body(test_client):
+    """Test HTTP queries with an empty response body."""
+    # Initialize a persistent HTTP connection
+    http_connection = test_client.get_connection()
+    http_connection.auto_open = False
+    http_connection.connect()
+
+    # Make the first request and assert there's no "Connection: close".
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/pov', http_conn=http_connection
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    assert actual_resp_body == pov.encode()
+    assert not header_exists('Connection', actual_headers)
+
+    # Make a 204 request on the same connection.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/custom/204', http_conn=http_connection
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 204
+    assert not header_exists('Content-Length', actual_headers)
+    assert actual_resp_body == b''
+    assert not header_exists('Connection', actual_headers)
+
+    # Make a 304 request on the same connection.
+    status_line, actual_headers, actual_resp_body = test_client.get(
+        '/custom/304', http_conn=http_connection
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == 304
+    assert not header_exists('Content-Length', actual_headers)
+    assert actual_resp_body == b''
+    assert not header_exists('Connection', actual_headers)
+
+
+@pytest.mark.xfail(
+    reason='Server does not correctly read trailers/ending of the previous '
+           'HTTP request, thus the second request fails as the server tries '
+           r"to parse b'Content-Type: application/json\r\n' as a "
+           'Request-Line. This results in HTTP status code 400, instead of 413'
+           'Ref: https://github.com/cherrypy/cheroot/issues/69'
+)
+def test_Chunked_Encoding(test_client):
+    """Test HTTP uploads with chunked transfer-encoding."""
+    # Initialize a persistent HTTP connection
+    conn = test_client.get_connection()
+
+    # Try a normal chunked request (with extensions)
+    body = (
+        b'8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
+        b'Content-Type: application/json\r\n'
+        b'\r\n'
+    )
+    conn.putrequest('POST', '/upload', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.putheader('Transfer-Encoding', 'chunked')
+    conn.putheader('Trailer', 'Content-Type')
+    # Note that this is somewhat malformed:
+    # we shouldn't be sending Content-Length.
+    # RFC 2616 says the server should ignore it.
+    conn.putheader('Content-Length', '3')
+    conn.endheaders()
+    conn.send(body)
+    response = conn.getresponse()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+    assert actual_status == 200
+    assert status_line[4:] == 'OK'
+    expected_resp_body = ("thanks for '%s'" % b'xx\r\nxxxxyyyyy').encode()
+    assert actual_resp_body == expected_resp_body
+
+    # Try a chunked request that exceeds server.max_request_body_size.
+    # Note that the delimiters and trailer are included.
+    body = b'3e3\r\n' + (b'x' * 995) + b'\r\n0\r\n\r\n'
+    conn.putrequest('POST', '/upload', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.putheader('Transfer-Encoding', 'chunked')
+    conn.putheader('Content-Type', 'text/plain')
+    # Chunked requests don't need a content-length
+    # conn.putheader("Content-Length", len(body))
+    conn.endheaders()
+    conn.send(body)
+    response = conn.getresponse()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+    assert actual_status == 413
+    conn.close()
+
+
+def test_Content_Length_in(test_client):
+    """Try a non-chunked request where Content-Length exceeds limit.
+
+    (server.max_request_body_size).
+    Assert error before body send.
+    """
+    # Initialize a persistent HTTP connection
+    conn = test_client.get_connection()
+
+    conn.putrequest('POST', '/upload', skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.putheader('Content-Type', 'text/plain')
+    conn.putheader('Content-Length', '9999')
+    conn.endheaders()
+    response = conn.getresponse()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+    assert actual_status == 413
+    expected_resp_body = (
+        b'The entity sent with the request exceeds '
+        b'the maximum allowed bytes.'
+    )
+    assert actual_resp_body == expected_resp_body
+    conn.close()
+
+
+def test_Content_Length_not_int(test_client):
+    """Test that malicious Content-Length header returns 400."""
+    status_line, actual_headers, actual_resp_body = test_client.post(
+        '/upload',
+        headers=[
+            ('Content-Type', 'text/plain'),
+            ('Content-Length', 'not-an-integer'),
+        ],
+    )
+    actual_status = int(status_line[:3])
+
+    assert actual_status == 400
+    assert actual_resp_body == b'Malformed Content-Length Header.'
+
+
+@pytest.mark.parametrize(
+    'uri,expected_resp_status,expected_resp_body',
+    (
+        ('/wrong_cl_buffered', 500,
+         (b'The requested resource returned more bytes than the '
+          b'declared Content-Length.')),
+        ('/wrong_cl_unbuffered', 200, b'I too'),
+    )
+)
+def test_Content_Length_out(
+    test_client,
+    uri, expected_resp_status, expected_resp_body
+):
+    """Test response with Content-Length less than the response body.
+
+    (non-chunked response)
+    """
+    conn = test_client.get_connection()
+    conn.putrequest('GET', uri, skip_host=True)
+    conn.putheader('Host', conn.host)
+    conn.endheaders()
+
+    response = conn.getresponse()
+    status_line, actual_headers, actual_resp_body = webtest.shb(response)
+    actual_status = int(status_line[:3])
+
+    assert actual_status == expected_resp_status
+    assert actual_resp_body == expected_resp_body
+
+    conn.close()
+
+
+@pytest.mark.xfail(
+    reason='Sometimes this test fails due to low timeout. '
+           'Ref: https://github.com/cherrypy/cherrypy/issues/598'
+)
+def test_598(test_client):
+    """Test serving large file with a read timeout in place."""
+    # Initialize a persistent HTTP connection
+    conn = test_client.get_connection()
+    remote_data_conn = urllib.request.urlopen(
+        '%s://%s:%s/one_megabyte_of_a'
+        % ('http', conn.host, conn.port)
+    )
+    buf = remote_data_conn.read(512)
+    time.sleep(timeout * 0.6)
+    remaining = (1024 * 1024) - 512
+    while remaining:
+        data = remote_data_conn.read(remaining)
+        if not data:
+            break
+        buf += data
+        remaining -= len(data)
+
+    assert len(buf) == 1024 * 1024
+    assert buf == b'a' * 1024 * 1024
+    assert remaining == 0
+    remote_data_conn.close()
+
+
+@pytest.mark.parametrize(
+    'invalid_terminator',
+    (
+        b'\n\n',
+        b'\r\n\n',
+    )
+)
+def test_No_CRLF(test_client, invalid_terminator):
+    """Test HTTP queries with no valid CRLF terminators."""
+    # Initialize a persistent HTTP connection
+    conn = test_client.get_connection()
+
+    # (b'%s' % b'') is not supported in Python 3.4, so just use +
+    conn.send(b'GET /hello HTTP/1.1' + invalid_terminator)
+    response = conn.response_class(conn.sock, method='GET')
+    response.begin()
+    actual_resp_body = response.read()
+    expected_resp_body = b'HTTP requires CRLF terminators'
+    assert actual_resp_body == expected_resp_body
+    conn.close()
diff --git a/libraries/cheroot/test/test_core.py b/libraries/cheroot/test/test_core.py
new file mode 100644
index 00000000..7c91b13e
--- /dev/null
+++ b/libraries/cheroot/test/test_core.py
@@ -0,0 +1,405 @@
+"""Tests for managing HTTP issues (malformed requests, etc)."""
+# -*- coding: utf-8 -*-
+# vim: set fileencoding=utf-8 :
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import errno
+import socket
+
+import pytest
+import six
+from six.moves import urllib
+
+from cheroot.test import helper
+
+
+HTTP_BAD_REQUEST = 400
+HTTP_LENGTH_REQUIRED = 411
+HTTP_NOT_FOUND = 404
+HTTP_OK = 200
+HTTP_VERSION_NOT_SUPPORTED = 505
+
+
+class HelloController(helper.Controller):
+    """Controller for serving WSGI apps."""
+
+    def hello(req, resp):
+        """Render Hello world."""
+        return 'Hello world!'
+
+    def body_required(req, resp):
+        """Render Hello world or set 411."""
+        if req.environ.get('Content-Length', None) is None:
+            resp.status = '411 Length Required'
+            return
+        return 'Hello world!'
+
+    def query_string(req, resp):
+        """Render QUERY_STRING value."""
+        return req.environ.get('QUERY_STRING', '')
+
+    def asterisk(req, resp):
+        """Render request method value."""
+        method = req.environ.get('REQUEST_METHOD', 'NO METHOD FOUND')
+        tmpl = 'Got asterisk URI path with {method} method'
+        return tmpl.format(**locals())
+
+    def _munge(string):
+        """Encode PATH_INFO correctly depending on Python version.
+
+        WSGI 1.0 is a mess around unicode. Create endpoints
+        that match the PATH_INFO that it produces.
+        """
+        if six.PY3:
+            return string.encode('utf-8').decode('latin-1')
+        return string
+
+    handlers = {
+        '/hello': hello,
+        '/no_body': hello,
+        '/body_required': body_required,
+        '/query_string': query_string,
+        _munge('/привіт'): hello,
+        _munge('/Юххууу'): hello,
+        '/\xa0Ðblah key 0 900 4 data': hello,
+        '/*': asterisk,
+    }
+
+
+def _get_http_response(connection, method='GET'):
+    c = connection
+    kwargs = {'strict': c.strict} if hasattr(c, 'strict') else {}
+    # Python 3.2 removed the 'strict' feature, saying:
+    # "http.client now always assumes HTTP/1.x compliant servers."
+    return c.response_class(c.sock, method=method, **kwargs)
+
+
+@pytest.fixture
+def testing_server(wsgi_server_client):
+    """Attach a WSGI app to the given server and pre-configure it."""
+    wsgi_server = wsgi_server_client.server_instance
+    wsgi_server.wsgi_app = HelloController()
+    wsgi_server.max_request_body_size = 30000000
+    wsgi_server.server_client = wsgi_server_client
+    return wsgi_server
+
+
+@pytest.fixture
+def test_client(testing_server):
+    """Get and return a test client out of the given server."""
+    return testing_server.server_client
+
+
+def test_http_connect_request(test_client):
+    """Check that CONNECT query results in Method Not Allowed status."""
+    status_line = test_client.connect('/anything')[0]
+    actual_status = int(status_line[:3])
+    assert actual_status == 405
+
+
+def test_normal_request(test_client):
+    """Check that normal GET query succeeds."""
+    status_line, _, actual_resp_body = test_client.get('/hello')
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_OK
+    assert actual_resp_body == b'Hello world!'
+
+
+def test_query_string_request(test_client):
+    """Check that GET param is parsed well."""
+    status_line, _, actual_resp_body = test_client.get(
+        '/query_string?test=True'
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_OK
+    assert actual_resp_body == b'test=True'
+
+
+@pytest.mark.parametrize(
+    'uri',
+    (
+        '/hello',  # plain
+        '/query_string?test=True',  # query
+        '/{0}?{1}={2}'.format(  # quoted unicode
+            *map(urllib.parse.quote, ('Юххууу', 'ї', 'йо'))
+        ),
+    )
+)
+def test_parse_acceptable_uri(test_client, uri):
+    """Check that server responds with OK to valid GET queries."""
+    status_line = test_client.get(uri)[0]
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_OK
+
+
+@pytest.mark.xfail(six.PY2, reason='Fails on Python 2')
+def test_parse_uri_unsafe_uri(test_client):
+    """Test that malicious URI does not allow HTTP injection.
+
+    This effectively checks that sending GET request with URL
+
+    /%A0%D0blah%20key%200%20900%204%20data
+
+    is not converted into
+
+    GET /
+    blah key 0 900 4 data
+    HTTP/1.1
+
+    which would be a security issue otherwise.
+    """
+    c = test_client.get_connection()
+    resource = '/\xa0Ðblah key 0 900 4 data'.encode('latin-1')
+    quoted = urllib.parse.quote(resource)
+    assert quoted == '/%A0%D0blah%20key%200%20900%204%20data'
+    request = 'GET {quoted} HTTP/1.1'.format(**locals())
+    c._output(request.encode('utf-8'))
+    c._send_output()
+    response = _get_http_response(c, method='GET')
+    response.begin()
+    assert response.status == HTTP_OK
+    assert response.fp.read(12) == b'Hello world!'
+    c.close()
+
+
+def test_parse_uri_invalid_uri(test_client):
+    """Check that server responds with Bad Request to invalid GET queries.
+
+    Invalid request line test case: it should only contain US-ASCII.
+    """
+    c = test_client.get_connection()
+    c._output(u'GET /йопта! HTTP/1.1'.encode('utf-8'))
+    c._send_output()
+    response = _get_http_response(c, method='GET')
+    response.begin()
+    assert response.status == HTTP_BAD_REQUEST
+    assert response.fp.read(21) == b'Malformed Request-URI'
+    c.close()
+
+
+@pytest.mark.parametrize(
+    'uri',
+    (
+        'hello',  # ascii
+        'привіт',  # non-ascii
+    )
+)
+def test_parse_no_leading_slash_invalid(test_client, uri):
+    """Check that server responds with Bad Request to invalid GET queries.
+
+    Invalid request line test case: it should have leading slash (be absolute).
+    """
+    status_line, _, actual_resp_body = test_client.get(
+        urllib.parse.quote(uri)
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_BAD_REQUEST
+    assert b'starting with a slash' in actual_resp_body
+
+
+def test_parse_uri_absolute_uri(test_client):
+    """Check that server responds with Bad Request to Absolute URI.
+
+    Only proxy servers should allow this.
+    """
+    status_line, _, actual_resp_body = test_client.get('http://google.com/')
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_BAD_REQUEST
+    expected_body = b'Absolute URI not allowed if server is not a proxy.'
+    assert actual_resp_body == expected_body
+
+
+def test_parse_uri_asterisk_uri(test_client):
+    """Check that server responds with OK to OPTIONS with "*" Absolute URI."""
+    status_line, _, actual_resp_body = test_client.options('*')
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_OK
+    expected_body = b'Got asterisk URI path with OPTIONS method'
+    assert actual_resp_body == expected_body
+
+
+def test_parse_uri_fragment_uri(test_client):
+    """Check that server responds with Bad Request to URI with fragment."""
+    status_line, _, actual_resp_body = test_client.get(
+        '/hello?test=something#fake',
+    )
+    actual_status = int(status_line[:3])
+    assert actual_status == HTTP_BAD_REQUEST
+    expected_body = b'Illegal #fragment in Request-URI.'
+    assert actual_resp_body == expected_body
+
+
+def test_no_content_length(test_client):
+    """Test POST query with an empty body being successful."""
+    # "The presence of a message-body in a request is signaled by the
+    # inclusion of a Content-Length or Transfer-Encoding header field in
+    # the request's message-headers."
+    #
+    # Send a message with neither header and no body.
+    c = test_client.get_connection()
+    c.request('POST', '/no_body')
+    response = c.getresponse()
+    actual_resp_body = response.fp.read()
+    actual_status = response.status
+    assert actual_status == HTTP_OK
+    assert actual_resp_body == b'Hello world!'
+
+
+def test_content_length_required(test_client):
+    """Test POST query with body failing because of missing Content-Length."""
+    # Now send a message that has no Content-Length, but does send a body.
+    # Verify that CP times out the socket and responds
+    # with 411 Length Required.
+
+    c = test_client.get_connection()
+    c.request('POST', '/body_required')
+    response = c.getresponse()
+    response.fp.read()
+
+    actual_status = response.status
+    assert actual_status == HTTP_LENGTH_REQUIRED
+
+
+@pytest.mark.parametrize(
+    'request_line,status_code,expected_body',
+    (
+        (b'GET /',  # missing proto
+         HTTP_BAD_REQUEST, b'Malformed Request-Line'),
+        (b'GET / HTTPS/1.1',  # invalid proto
+         HTTP_BAD_REQUEST, b'Malformed Request-Line: bad protocol'),
+        (b'GET / HTTP/2.15',  # invalid ver
+         HTTP_VERSION_NOT_SUPPORTED, b'Cannot fulfill request'),
+    )
+)
+def test_malformed_request_line(
+    test_client, request_line,
+    status_code, expected_body
+):
+    """Test missing or invalid HTTP version in Request-Line."""
+    c = test_client.get_connection()
+    c._output(request_line)
+    c._send_output()
+    response = _get_http_response(c, method='GET')
+    response.begin()
+    assert response.status == status_code
+    assert response.fp.read(len(expected_body)) == expected_body
+    c.close()
+
+
+def test_malformed_http_method(test_client):
+    """Test non-uppercase HTTP method."""
+    c = test_client.get_connection()
+    c.putrequest('GeT', '/malformed_method_case')
+    c.putheader('Content-Type', 'text/plain')
+    c.endheaders()
+
+    response = c.getresponse()
+    actual_status = response.status
+    assert actual_status == HTTP_BAD_REQUEST
+    actual_resp_body = response.fp.read(21)
+    assert actual_resp_body == b'Malformed method name'
+
+
+def test_malformed_header(test_client):
+    """Check that broken HTTP header results in Bad Request."""
+    c = test_client.get_connection()
+    c.putrequest('GET', '/')
+    c.putheader('Content-Type', 'text/plain')
+    # See https://www.bitbucket.org/cherrypy/cherrypy/issue/941
+    c._output(b'Re, 1.2.3.4#015#012')
+    c.endheaders()
+
+    response = c.getresponse()
+    actual_status = response.status
+    assert actual_status == HTTP_BAD_REQUEST
+    actual_resp_body = response.fp.read(20)
+    assert actual_resp_body == b'Illegal header line.'
+
+
+def test_request_line_split_issue_1220(test_client):
+    """Check that HTTP request line of exactly 256 chars length is OK."""
+    Request_URI = (
+        '/hello?'
+        'intervenant-entreprise-evenement_classaction='
+        'evenement-mailremerciements'
+        '&_path=intervenant-entreprise-evenement'
+        '&intervenant-entreprise-evenement_action-id=19404'
+        '&intervenant-entreprise-evenement_id=19404'
+        '&intervenant-entreprise_id=28092'
+    )
+    assert len('GET %s HTTP/1.1\r\n' % Request_URI) == 256
+
+    actual_resp_body = test_client.get(Request_URI)[2]
+    assert actual_resp_body == b'Hello world!'
+
+
+def test_garbage_in(test_client):
+    """Test that server sends an error for garbage received over TCP."""
+    # Connect without SSL regardless of server.scheme
+
+    c = test_client.get_connection()
+    c._output(b'gjkgjklsgjklsgjkljklsg')
+    c._send_output()
+    response = c.response_class(c.sock, method='GET')
+    try:
+        response.begin()
+        actual_status = response.status
+        assert actual_status == HTTP_BAD_REQUEST
+        actual_resp_body = response.fp.read(22)
+        assert actual_resp_body == b'Malformed Request-Line'
+        c.close()
+    except socket.error as ex:
+        # "Connection reset by peer" is also acceptable.
+        if ex.errno != errno.ECONNRESET:
+            raise
+
+
+class CloseController:
+    """Controller for testing the close callback."""
+
+    def __call__(self, environ, start_response):
+        """Get the req to know header sent status."""
+        self.req = start_response.__self__.req
+        resp = CloseResponse(self.close)
+        start_response(resp.status, resp.headers.items())
+        return resp
+
+    def close(self):
+        """Close, writing hello."""
+        self.req.write(b'hello')
+
+
+class CloseResponse:
+    """Dummy empty response to trigger the no body status."""
+
+    def __init__(self, close):
+        """Use some defaults to ensure we have a header."""
+        self.status = '200 OK'
+        self.headers = {'Content-Type': 'text/html'}
+        self.close = close
+
+    def __getitem__(self, index):
+        """Ensure we don't have a body."""
+        raise IndexError()
+
+    def output(self):
+        """Return self to hook the close method."""
+        return self
+
+
+@pytest.fixture
+def testing_server_close(wsgi_server_client):
+    """Attach a WSGI app to the given server and pre-configure it."""
+    wsgi_server = wsgi_server_client.server_instance
+    wsgi_server.wsgi_app = CloseController()
+    wsgi_server.max_request_body_size = 30000000
+    wsgi_server.server_client = wsgi_server_client
+    return wsgi_server
+
+
+def test_send_header_before_closing(testing_server_close):
+    """Test we are actually sending the headers before calling 'close'."""
+    _, _, resp_body = testing_server_close.server_client.get('/')
+    assert resp_body == b'hello'
diff --git a/libraries/cheroot/test/test_server.py b/libraries/cheroot/test/test_server.py
new file mode 100644
index 00000000..c53f7a81
--- /dev/null
+++ b/libraries/cheroot/test/test_server.py
@@ -0,0 +1,193 @@
+"""Tests for the HTTP server."""
+# -*- coding: utf-8 -*-
+# vim: set fileencoding=utf-8 :
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import os
+import socket
+import tempfile
+import threading
+import time
+
+import pytest
+
+from .._compat import bton
+from ..server import Gateway, HTTPServer
+from ..testing import (
+    ANY_INTERFACE_IPV4,
+    ANY_INTERFACE_IPV6,
+    EPHEMERAL_PORT,
+    get_server_client,
+)
+
+
+def make_http_server(bind_addr):
+    """Create and start an HTTP server bound to bind_addr."""
+    httpserver = HTTPServer(
+        bind_addr=bind_addr,
+        gateway=Gateway,
+    )
+
+    threading.Thread(target=httpserver.safe_start).start()
+
+    while not httpserver.ready:
+        time.sleep(0.1)
+
+    return httpserver
+
+
+non_windows_sock_test = pytest.mark.skipif(
+    not hasattr(socket, 'AF_UNIX'),
+    reason='UNIX domain sockets are only available under UNIX-based OS',
+)
+
+
+@pytest.fixture
+def http_server():
+    """Provision a server creator as a fixture."""
+    def start_srv():
+        bind_addr = yield
+        httpserver = make_http_server(bind_addr)
+        yield httpserver
+        yield httpserver
+
+    srv_creator = iter(start_srv())
+    next(srv_creator)
+    yield srv_creator
+    try:
+        while True:
+            httpserver = next(srv_creator)
+            if httpserver is not None:
+                httpserver.stop()
+    except StopIteration:
+        pass
+
+
+@pytest.fixture
+def unix_sock_file():
+    """Check that bound UNIX socket address is stored in server."""
+    tmp_sock_fh, tmp_sock_fname = tempfile.mkstemp()
+
+    yield tmp_sock_fname
+
+    os.close(tmp_sock_fh)
+    os.unlink(tmp_sock_fname)
+
+
+def test_prepare_makes_server_ready():
+    """Check that prepare() makes the server ready, and stop() clears it."""
+    httpserver = HTTPServer(
+        bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
+        gateway=Gateway,
+    )
+
+    assert not httpserver.ready
+    assert not httpserver.requests._threads
+
+    httpserver.prepare()
+
+    assert httpserver.ready
+    assert httpserver.requests._threads
+    for thr in httpserver.requests._threads:
+        assert thr.ready
+
+    httpserver.stop()
+
+    assert not httpserver.requests._threads
+    assert not httpserver.ready
+
+
+def test_stop_interrupts_serve():
+    """Check that stop() interrupts running of serve()."""
+    httpserver = HTTPServer(
+        bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
+        gateway=Gateway,
+    )
+
+    httpserver.prepare()
+    serve_thread = threading.Thread(target=httpserver.serve)
+    serve_thread.start()
+
+    serve_thread.join(0.5)
+    assert serve_thread.is_alive()
+
+    httpserver.stop()
+
+    serve_thread.join(0.5)
+    assert not serve_thread.is_alive()
+
+
+@pytest.mark.parametrize(
+    'ip_addr',
+    (
+        ANY_INTERFACE_IPV4,
+        ANY_INTERFACE_IPV6,
+    )
+)
+def test_bind_addr_inet(http_server, ip_addr):
+    """Check that bound IP address is stored in server."""
+    httpserver = http_server.send((ip_addr, EPHEMERAL_PORT))
+
+    assert httpserver.bind_addr[0] == ip_addr
+    assert httpserver.bind_addr[1] != EPHEMERAL_PORT
+
+
+@non_windows_sock_test
+def test_bind_addr_unix(http_server, unix_sock_file):
+    """Check that bound UNIX socket address is stored in server."""
+    httpserver = http_server.send(unix_sock_file)
+
+    assert httpserver.bind_addr == unix_sock_file
+
+
+@pytest.mark.skip(reason="Abstract sockets don't work currently")
+@non_windows_sock_test
+def test_bind_addr_unix_abstract(http_server):
+    """Check that bound UNIX socket address is stored in server."""
+    unix_abstract_sock = b'\x00cheroot/test/socket/here.sock'
+    httpserver = http_server.send(unix_abstract_sock)
+
+    assert httpserver.bind_addr == unix_abstract_sock
+
+
+PEERCRED_IDS_URI = '/peer_creds/ids'
+PEERCRED_TEXTS_URI = '/peer_creds/texts'
+
+
+class _TestGateway(Gateway):
+    def respond(self):
+        req = self.req
+        conn = req.conn
+        req_uri = bton(req.uri)
+        if req_uri == PEERCRED_IDS_URI:
+            peer_creds = conn.peer_pid, conn.peer_uid, conn.peer_gid
+            return ['|'.join(map(str, peer_creds))]
+        elif req_uri == PEERCRED_TEXTS_URI:
+            return ['!'.join((conn.peer_user, conn.peer_group))]
+        return super(_TestGateway, self).respond()
+
+
+@pytest.mark.skip(
+    reason='Test HTTP client is not able to work through UNIX socket currently'
+)
+@non_windows_sock_test
+def test_peercreds_unix_sock(http_server, unix_sock_file):
+    """Check that peercred lookup and resolution work when enabled."""
+    httpserver = http_server.send(unix_sock_file)
+    httpserver.gateway = _TestGateway
+    httpserver.peercreds_enabled = True
+
+    testclient = get_server_client(httpserver)
+
+    expected_peercreds = os.getpid(), os.getuid(), os.getgid()
+    expected_peercreds = '|'.join(map(str, expected_peercreds))
+    assert testclient.get(PEERCRED_IDS_URI) == expected_peercreds
+    assert 'RuntimeError' in testclient.get(PEERCRED_TEXTS_URI)
+
+    httpserver.peercreds_resolve_enabled = True
+    import grp
+    expected_textcreds = os.getlogin(), grp.getgrgid(os.getgid()).gr_name
+    expected_textcreds = '!'.join(map(str, expected_textcreds))
+    assert testclient.get(PEERCRED_TEXTS_URI) == expected_textcreds
diff --git a/libraries/cheroot/test/webtest.py b/libraries/cheroot/test/webtest.py
new file mode 100644
index 00000000..43448f5b
--- /dev/null
+++ b/libraries/cheroot/test/webtest.py
@@ -0,0 +1,581 @@
+"""Extensions to unittest for web frameworks.
+
+Use the WebCase.getPage method to request a page from your HTTP server.
+Framework Integration
+=====================
+If you have control over your server process, you can handle errors
+in the server-side of the HTTP conversation a bit better. You must run
+both the client (your WebCase tests) and the server in the same process
+(but in separate threads, obviously).
+When an error occurs in the framework, call server_error. It will print
+the traceback to stdout, and keep any assertions you have from running
+(the assumption is that, if the server errors, the page output will not
+be of further significance to your tests).
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import pprint
+import re
+import socket
+import sys
+import time
+import traceback
+import os
+import json
+import unittest
+import warnings
+
+from six.moves import range, http_client, map, urllib_parse
+import six
+
+from more_itertools.more import always_iterable
+
+
+def interface(host):
+    """Return an IP address for a client connection given the server host.
+
+    If the server is listening on '0.0.0.0' (INADDR_ANY)
+    or '::' (IN6ADDR_ANY), this will return the proper localhost.
+    """
+    if host == '0.0.0.0':
+        # INADDR_ANY, which should respond on localhost.
+        return '127.0.0.1'
+    if host == '::':
+        # IN6ADDR_ANY, which should respond on localhost.
+        return '::1'
+    return host
+
+
+try:
+    # Jython support
+    if sys.platform[:4] == 'java':
+        def getchar():
+            """Get a key press."""
+            # Hopefully this is enough
+            return sys.stdin.read(1)
+    else:
+        # On Windows, msvcrt.getch reads a single char without output.
+        import msvcrt
+
+        def getchar():
+            """Get a key press."""
+            return msvcrt.getch()
+except ImportError:
+    # Unix getchr
+    import tty
+    import termios
+
+    def getchar():
+        """Get a key press."""
+        fd = sys.stdin.fileno()
+        old_settings = termios.tcgetattr(fd)
+        try:
+            tty.setraw(sys.stdin.fileno())
+            ch = sys.stdin.read(1)
+        finally:
+            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+        return ch
+
+
+# from jaraco.properties
+class NonDataProperty:
+    """Non-data property decorator."""
+
+    def __init__(self, fget):
+        """Initialize a non-data property."""
+        assert fget is not None, 'fget cannot be none'
+        assert callable(fget), 'fget must be callable'
+        self.fget = fget
+
+    def __get__(self, obj, objtype=None):
+        """Return a class property."""
+        if obj is None:
+            return self
+        return self.fget(obj)
+
+
+class WebCase(unittest.TestCase):
+    """Helper web test suite base."""
+
+    HOST = '127.0.0.1'
+    PORT = 8000
+    HTTP_CONN = http_client.HTTPConnection
+    PROTOCOL = 'HTTP/1.1'
+
+    scheme = 'http'
+    url = None
+
+    status = None
+    headers = None
+    body = None
+
+    encoding = 'utf-8'
+
+    time = None
+
+    @property
+    def _Conn(self):
+        """Return HTTPConnection or HTTPSConnection based on self.scheme.
+
+        * from http.client.
+        """
+        cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper())
+        return getattr(http_client, cls_name)
+
+    def get_conn(self, auto_open=False):
+        """Return a connection to our HTTP server."""
+        conn = self._Conn(self.interface(), self.PORT)
+        # Automatically re-connect?
+        conn.auto_open = auto_open
+        conn.connect()
+        return conn
+
+    def set_persistent(self, on=True, auto_open=False):
+        """Make our HTTP_CONN persistent (or not).
+
+        If the 'on' argument is True (the default), then self.HTTP_CONN
+        will be set to an instance of HTTP(S)?Connection
+        to persist across requests.
+        As this class only allows for a single open connection, if
+        self already has an open connection, it will be closed.
+        """
+        try:
+            self.HTTP_CONN.close()
+        except (TypeError, AttributeError):
+            pass
+
+        self.HTTP_CONN = (
+            self.get_conn(auto_open=auto_open)
+            if on
+            else self._Conn
+        )
+
+    @property
+    def persistent(self):  # noqa: D401; irrelevant for properties
+        """Presense of the persistent HTTP connection."""
+        return hasattr(self.HTTP_CONN, '__class__')
+
+    @persistent.setter
+    def persistent(self, on):
+        self.set_persistent(on)
+
+    def interface(self):
+        """Return an IP address for a client connection.
+
+        If the server is listening on '0.0.0.0' (INADDR_ANY)
+        or '::' (IN6ADDR_ANY), this will return the proper localhost.
+        """
+        return interface(self.HOST)
+
+    def getPage(self, url, headers=None, method='GET', body=None,
+                protocol=None, raise_subcls=None):
+        """Open the url with debugging support. Return status, headers, body.
+
+        url should be the identifier passed to the server, typically a
+        server-absolute path and query string (sent between method and
+        protocol), and should only be an absolute URI if proxy support is
+        enabled in the server.
+
+        If the application under test generates absolute URIs, be sure
+        to wrap them first with strip_netloc::
+
+            class MyAppWebCase(WebCase):
+                def getPage(url, *args, **kwargs):
+                    super(MyAppWebCase, self).getPage(
+                        cheroot.test.webtest.strip_netloc(url),
+                        *args, **kwargs
+                    )
+
+        `raise_subcls` must be a tuple with the exceptions classes
+        or a single exception class that are not going to be considered
+        a socket.error regardless that they were are subclass of a
+        socket.error and therefore not considered for a connection retry.
+        """
+        ServerError.on = False
+
+        if isinstance(url, six.text_type):
+            url = url.encode('utf-8')
+        if isinstance(body, six.text_type):
+            body = body.encode('utf-8')
+
+        self.url = url
+        self.time = None
+        start = time.time()
+        result = openURL(url, headers, method, body, self.HOST, self.PORT,
+                         self.HTTP_CONN, protocol or self.PROTOCOL,
+                         raise_subcls)
+        self.time = time.time() - start
+        self.status, self.headers, self.body = result
+
+        # Build a list of request cookies from the previous response cookies.
+        self.cookies = [('Cookie', v) for k, v in self.headers
+                        if k.lower() == 'set-cookie']
+
+        if ServerError.on:
+            raise ServerError()
+        return result
+
+    @NonDataProperty
+    def interactive(self):
+        """Determine whether tests are run in interactive mode.
+
+        Load interactivity setting from environment, where
+        the value can be numeric or a string like true or
+        False or 1 or 0.
+        """
+        env_str = os.environ.get('WEBTEST_INTERACTIVE', 'True')
+        is_interactive = bool(json.loads(env_str.lower()))
+        if is_interactive:
+            warnings.warn(
+                'Interactive test failure interceptor support via '
+                'WEBTEST_INTERACTIVE environment variable is deprecated.',
+                DeprecationWarning
+            )
+        return is_interactive
+
+    console_height = 30
+
+    def _handlewebError(self, msg):
+        print('')
+        print('    ERROR: %s' % msg)
+
+        if not self.interactive:
+            raise self.failureException(msg)
+
+        p = ('    Show: '
+             '[B]ody [H]eaders [S]tatus [U]RL; '
+             '[I]gnore, [R]aise, or sys.e[X]it >> ')
+        sys.stdout.write(p)
+        sys.stdout.flush()
+        while True:
+            i = getchar().upper()
+            if not isinstance(i, type('')):
+                i = i.decode('ascii')
+            if i not in 'BHSUIRX':
+                continue
+            print(i.upper())  # Also prints new line
+            if i == 'B':
+                for x, line in enumerate(self.body.splitlines()):
+                    if (x + 1) % self.console_height == 0:
+                        # The \r and comma should make the next line overwrite
+                        sys.stdout.write('<-- More -->\r')
+                        m = getchar().lower()
+                        # Erase our "More" prompt
+                        sys.stdout.write('            \r')
+                        if m == 'q':
+                            break
+                    print(line)
+            elif i == 'H':
+                pprint.pprint(self.headers)
+            elif i == 'S':
+                print(self.status)
+            elif i == 'U':
+                print(self.url)
+            elif i == 'I':
+                # return without raising the normal exception
+                return
+            elif i == 'R':
+                raise self.failureException(msg)
+            elif i == 'X':
+                sys.exit()
+            sys.stdout.write(p)
+            sys.stdout.flush()
+
+    @property
+    def status_code(self):  # noqa: D401; irrelevant for properties
+        """Integer HTTP status code."""
+        return int(self.status[:3])
+
+    def status_matches(self, expected):
+        """Check whether actual status matches expected."""
+        actual = (
+            self.status_code
+            if isinstance(expected, int) else
+            self.status
+        )
+        return expected == actual
+
+    def assertStatus(self, status, msg=None):
+        """Fail if self.status != status.
+
+        status may be integer code, exact string status, or
+        iterable of allowed possibilities.
+        """
+        if any(map(self.status_matches, always_iterable(status))):
+            return
+
+        tmpl = 'Status {self.status} does not match {status}'
+        msg = msg or tmpl.format(**locals())
+        self._handlewebError(msg)
+
+    def assertHeader(self, key, value=None, msg=None):
+        """Fail if (key, [value]) not in self.headers."""
+        lowkey = key.lower()
+        for k, v in self.headers:
+            if k.lower() == lowkey:
+                if value is None or str(value) == v:
+                    return v
+
+        if msg is None:
+            if value is None:
+                msg = '%r not in headers' % key
+            else:
+                msg = '%r:%r not in headers' % (key, value)
+        self._handlewebError(msg)
+
+    def assertHeaderIn(self, key, values, msg=None):
+        """Fail if header indicated by key doesn't have one of the values."""
+        lowkey = key.lower()
+        for k, v in self.headers:
+            if k.lower() == lowkey:
+                matches = [value for value in values if str(value) == v]
+                if matches:
+                    return matches
+
+        if msg is None:
+            msg = '%(key)r not in %(values)r' % vars()
+        self._handlewebError(msg)
+
+    def assertHeaderItemValue(self, key, value, msg=None):
+        """Fail if the header does not contain the specified value."""
+        actual_value = self.assertHeader(key, msg=msg)
+        header_values = map(str.strip, actual_value.split(','))
+        if value in header_values:
+            return value
+
+        if msg is None:
+            msg = '%r not in %r' % (value, header_values)
+        self._handlewebError(msg)
+
+    def assertNoHeader(self, key, msg=None):
+        """Fail if key in self.headers."""
+        lowkey = key.lower()
+        matches = [k for k, v in self.headers if k.lower() == lowkey]
+        if matches:
+            if msg is None:
+                msg = '%r in headers' % key
+            self._handlewebError(msg)
+
+    def assertNoHeaderItemValue(self, key, value, msg=None):
+        """Fail if the header contains the specified value."""
+        lowkey = key.lower()
+        hdrs = self.headers
+        matches = [k for k, v in hdrs if k.lower() == lowkey and v == value]
+        if matches:
+            if msg is None:
+                msg = '%r:%r in %r' % (key, value, hdrs)
+            self._handlewebError(msg)
+
+    def assertBody(self, value, msg=None):
+        """Fail if value != self.body."""
+        if isinstance(value, six.text_type):
+            value = value.encode(self.encoding)
+        if value != self.body:
+            if msg is None:
+                msg = 'expected body:\n%r\n\nactual body:\n%r' % (
+                    value, self.body)
+            self._handlewebError(msg)
+
+    def assertInBody(self, value, msg=None):
+        """Fail if value not in self.body."""
+        if isinstance(value, six.text_type):
+            value = value.encode(self.encoding)
+        if value not in self.body:
+            if msg is None:
+                msg = '%r not in body: %s' % (value, self.body)
+            self._handlewebError(msg)
+
+    def assertNotInBody(self, value, msg=None):
+        """Fail if value in self.body."""
+        if isinstance(value, six.text_type):
+            value = value.encode(self.encoding)
+        if value in self.body:
+            if msg is None:
+                msg = '%r found in body' % value
+            self._handlewebError(msg)
+
+    def assertMatchesBody(self, pattern, msg=None, flags=0):
+        """Fail if value (a regex pattern) is not in self.body."""
+        if isinstance(pattern, six.text_type):
+            pattern = pattern.encode(self.encoding)
+        if re.search(pattern, self.body, flags) is None:
+            if msg is None:
+                msg = 'No match for %r in body' % pattern
+            self._handlewebError(msg)
+
+
+methods_with_bodies = ('POST', 'PUT', 'PATCH')
+
+
+def cleanHeaders(headers, method, body, host, port):
+    """Return request headers, with required headers added (if missing)."""
+    if headers is None:
+        headers = []
+
+    # Add the required Host request header if not present.
+    # [This specifies the host:port of the server, not the client.]
+    found = False
+    for k, v in headers:
+        if k.lower() == 'host':
+            found = True
+            break
+    if not found:
+        if port == 80:
+            headers.append(('Host', host))
+        else:
+            headers.append(('Host', '%s:%s' % (host, port)))
+
+    if method in methods_with_bodies:
+        # Stick in default type and length headers if not present
+        found = False
+        for k, v in headers:
+            if k.lower() == 'content-type':
+                found = True
+                break
+        if not found:
+            headers.append(
+                ('Content-Type', 'application/x-www-form-urlencoded'))
+            headers.append(('Content-Length', str(len(body or ''))))
+
+    return headers
+
+
+def shb(response):
+    """Return status, headers, body the way we like from a response."""
+    if six.PY3:
+        h = response.getheaders()
+    else:
+        h = []
+        key, value = None, None
+        for line in response.msg.headers:
+            if line:
+                if line[0] in ' \t':
+                    value += line.strip()
+                else:
+                    if key and value:
+                        h.append((key, value))
+                    key, value = line.split(':', 1)
+                    key = key.strip()
+                    value = value.strip()
+        if key and value:
+            h.append((key, value))
+
+    return '%s %s' % (response.status, response.reason), h, response.read()
+
+
+def openURL(url, headers=None, method='GET', body=None,
+            host='127.0.0.1', port=8000, http_conn=http_client.HTTPConnection,
+            protocol='HTTP/1.1', raise_subcls=None):
+    """
+    Open the given HTTP resource and return status, headers, and body.
+
+    `raise_subcls` must be a tuple with the exceptions classes
+    or a single exception class that are not going to be considered
+    a socket.error regardless that they were are subclass of a
+    socket.error and therefore not considered for a connection retry.
+    """
+    headers = cleanHeaders(headers, method, body, host, port)
+
+    # Trying 10 times is simply in case of socket errors.
+    # Normal case--it should run once.
+    for trial in range(10):
+        try:
+            # Allow http_conn to be a class or an instance
+            if hasattr(http_conn, 'host'):
+                conn = http_conn
+            else:
+                conn = http_conn(interface(host), port)
+
+            conn._http_vsn_str = protocol
+            conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()]))
+
+            if six.PY3 and isinstance(url, bytes):
+                url = url.decode()
+            conn.putrequest(method.upper(), url, skip_host=True,
+                            skip_accept_encoding=True)
+
+            for key, value in headers:
+                conn.putheader(key, value.encode('Latin-1'))
+            conn.endheaders()
+
+            if body is not None:
+                conn.send(body)
+
+            # Handle response
+            response = conn.getresponse()
+
+            s, h, b = shb(response)
+
+            if not hasattr(http_conn, 'host'):
+                # We made our own conn instance. Close it.
+                conn.close()
+
+            return s, h, b
+        except socket.error as e:
+            if raise_subcls is not None and isinstance(e, raise_subcls):
+                raise
+            else:
+                time.sleep(0.5)
+                if trial == 9:
+                    raise
+
+
+def strip_netloc(url):
+    """Return absolute-URI path from URL.
+
+    Strip the scheme and host from the URL, returning the
+    server-absolute portion.
+
+    Useful for wrapping an absolute-URI for which only the
+    path is expected (such as in calls to getPage).
+
+    >>> strip_netloc('https://google.com/foo/bar?bing#baz')
+    '/foo/bar?bing'
+
+    >>> strip_netloc('//google.com/foo/bar?bing#baz')
+    '/foo/bar?bing'
+
+    >>> strip_netloc('/foo/bar?bing#baz')
+    '/foo/bar?bing'
+    """
+    parsed = urllib_parse.urlparse(url)
+    scheme, netloc, path, params, query, fragment = parsed
+    stripped = '', '', path, params, query, ''
+    return urllib_parse.urlunparse(stripped)
+
+
+# Add any exceptions which your web framework handles
+# normally (that you don't want server_error to trap).
+ignored_exceptions = []
+
+# You'll want set this to True when you can't guarantee
+# that each response will immediately follow each request;
+# for example, when handling requests via multiple threads.
+ignore_all = False
+
+
+class ServerError(Exception):
+    """Exception for signalling server error."""
+
+    on = False
+
+
+def server_error(exc=None):
+    """Server debug hook.
+
+    Return True if exception handled, False if ignored.
+    You probably want to wrap this, so you can still handle an error using
+    your framework when it's ignored.
+    """
+    if exc is None:
+        exc = sys.exc_info()
+
+    if ignore_all or exc[0] in ignored_exceptions:
+        return False
+    else:
+        ServerError.on = True
+        print('')
+        print(''.join(traceback.format_exception(*exc)))
+        return True
diff --git a/libraries/cheroot/testing.py b/libraries/cheroot/testing.py
new file mode 100644
index 00000000..f01d0aa1
--- /dev/null
+++ b/libraries/cheroot/testing.py
@@ -0,0 +1,144 @@
+"""Pytest fixtures and other helpers for doing testing by end-users."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+from contextlib import closing
+import errno
+import socket
+import threading
+import time
+
+import pytest
+from six.moves import http_client
+
+import cheroot.server
+from cheroot.test import webtest
+import cheroot.wsgi
+
+EPHEMERAL_PORT = 0
+NO_INTERFACE = None  # Using this or '' will cause an exception
+ANY_INTERFACE_IPV4 = '0.0.0.0'
+ANY_INTERFACE_IPV6 = '::'
+
+config = {
+    cheroot.wsgi.Server: {
+        'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT),
+        'wsgi_app': None,
+    },
+    cheroot.server.HTTPServer: {
+        'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT),
+        'gateway': cheroot.server.Gateway,
+    },
+}
+
+
+def cheroot_server(server_factory):
+    """Set up and tear down a Cheroot server instance."""
+    conf = config[server_factory].copy()
+    bind_port = conf.pop('bind_addr')[-1]
+
+    for interface in ANY_INTERFACE_IPV6, ANY_INTERFACE_IPV4:
+        try:
+            actual_bind_addr = (interface, bind_port)
+            httpserver = server_factory(  # create it
+                bind_addr=actual_bind_addr,
+                **conf
+            )
+        except OSError:
+            pass
+        else:
+            break
+
+    threading.Thread(target=httpserver.safe_start).start()  # spawn it
+    while not httpserver.ready:  # wait until fully initialized and bound
+        time.sleep(0.1)
+
+    yield httpserver
+
+    httpserver.stop()  # destroy it
+
+
+@pytest.fixture(scope='module')
+def wsgi_server():
+    """Set up and tear down a Cheroot WSGI server instance."""
+    for srv in cheroot_server(cheroot.wsgi.Server):
+        yield srv
+
+
+@pytest.fixture(scope='module')
+def native_server():
+    """Set up and tear down a Cheroot HTTP server instance."""
+    for srv in cheroot_server(cheroot.server.HTTPServer):
+        yield srv
+
+
+class _TestClient:
+    def __init__(self, server):
+        self._interface, self._host, self._port = _get_conn_data(server)
+        self._http_connection = self.get_connection()
+        self.server_instance = server
+
+    def get_connection(self):
+        name = '{interface}:{port}'.format(
+            interface=self._interface,
+            port=self._port,
+        )
+        return http_client.HTTPConnection(name)
+
+    def request(
+        self, uri, method='GET', headers=None, http_conn=None,
+        protocol='HTTP/1.1',
+    ):
+        return webtest.openURL(
+            uri, method=method,
+            headers=headers,
+            host=self._host, port=self._port,
+            http_conn=http_conn or self._http_connection,
+            protocol=protocol,
+        )
+
+    def __getattr__(self, attr_name):
+        def _wrapper(uri, **kwargs):
+            http_method = attr_name.upper()
+            return self.request(uri, method=http_method, **kwargs)
+
+        return _wrapper
+
+
+def _probe_ipv6_sock(interface):
+    # Alternate way is to check IPs on interfaces using glibc, like:
+    # github.com/Gautier/minifail/blob/master/minifail/getifaddrs.py
+    try:
+        with closing(socket.socket(family=socket.AF_INET6)) as sock:
+            sock.bind((interface, 0))
+    except (OSError, socket.error) as sock_err:
+        # In Python 3 socket.error is an alias for OSError
+        # In Python 2 socket.error is a subclass of IOError
+        if sock_err.errno != errno.EADDRNOTAVAIL:
+            raise
+    else:
+        return True
+
+    return False
+
+
+def _get_conn_data(server):
+    if isinstance(server.bind_addr, tuple):
+        host, port = server.bind_addr
+    else:
+        host, port = server.bind_addr, 0
+
+    interface = webtest.interface(host)
+
+    if ':' in interface and not _probe_ipv6_sock(interface):
+        interface = '127.0.0.1'
+        if ':' in host:
+            host = interface
+
+    return interface, host, port
+
+
+def get_server_client(server):
+    """Create and return a test client for the given server."""
+    return _TestClient(server)
diff --git a/libraries/cheroot/workers/__init__.py b/libraries/cheroot/workers/__init__.py
new file mode 100644
index 00000000..098b8f25
--- /dev/null
+++ b/libraries/cheroot/workers/__init__.py
@@ -0,0 +1 @@
+"""HTTP workers pool."""
diff --git a/libraries/cheroot/workers/threadpool.py b/libraries/cheroot/workers/threadpool.py
new file mode 100644
index 00000000..ff8fbcee
--- /dev/null
+++ b/libraries/cheroot/workers/threadpool.py
@@ -0,0 +1,271 @@
+"""A thread-based worker pool."""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+
+import threading
+import time
+import socket
+
+from six.moves import queue
+
+
+__all__ = ('WorkerThread', 'ThreadPool')
+
+
+class TrueyZero:
+    """Object which equals and does math like the integer 0 but evals True."""
+
+    def __add__(self, other):
+        return other
+
+    def __radd__(self, other):
+        return other
+
+
+trueyzero = TrueyZero()
+
+_SHUTDOWNREQUEST = None
+
+
+class WorkerThread(threading.Thread):
+    """Thread which continuously polls a Queue for Connection objects.
+
+    Due to the timing issues of polling a Queue, a WorkerThread does not
+    check its own 'ready' flag after it has started. To stop the thread,
+    it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
+    (one for each running WorkerThread).
+    """
+
+    conn = None
+    """The current connection pulled off the Queue, or None."""
+
+    server = None
+    """The HTTP Server which spawned this thread, and which owns the
+    Queue and is placing active connections into it."""
+
+    ready = False
+    """A simple flag for the calling server to know when this thread
+    has begun polling the Queue."""
+
+    def __init__(self, server):
+        """Initialize WorkerThread instance.
+
+        Args:
+            server (cheroot.server.HTTPServer): web server object
+                receiving this request
+        """
+        self.ready = False
+        self.server = server
+
+        self.requests_seen = 0
+        self.bytes_read = 0
+        self.bytes_written = 0
+        self.start_time = None
+        self.work_time = 0
+        self.stats = {
+            'Requests': lambda s: self.requests_seen + (
+                (self.start_time is None) and
+                trueyzero or
+                self.conn.requests_seen
+            ),
+            'Bytes Read': lambda s: self.bytes_read + (
+                (self.start_time is None) and
+                trueyzero or
+                self.conn.rfile.bytes_read
+            ),
+            'Bytes Written': lambda s: self.bytes_written + (
+                (self.start_time is None) and
+                trueyzero or
+                self.conn.wfile.bytes_written
+            ),
+            'Work Time': lambda s: self.work_time + (
+                (self.start_time is None) and
+                trueyzero or
+                time.time() - self.start_time
+            ),
+            'Read Throughput': lambda s: s['Bytes Read'](s) / (
+                s['Work Time'](s) or 1e-6),
+            'Write Throughput': lambda s: s['Bytes Written'](s) / (
+                s['Work Time'](s) or 1e-6),
+        }
+        threading.Thread.__init__(self)
+
+    def run(self):
+        """Process incoming HTTP connections.
+
+        Retrieves incoming connections from thread pool.
+        """
+        self.server.stats['Worker Threads'][self.getName()] = self.stats
+        try:
+            self.ready = True
+            while True:
+                conn = self.server.requests.get()
+                if conn is _SHUTDOWNREQUEST:
+                    return
+
+                self.conn = conn
+                if self.server.stats['Enabled']:
+                    self.start_time = time.time()
+                try:
+                    conn.communicate()
+                finally:
+                    conn.close()
+                    if self.server.stats['Enabled']:
+                        self.requests_seen += self.conn.requests_seen
+                        self.bytes_read += self.conn.rfile.bytes_read
+                        self.bytes_written += self.conn.wfile.bytes_written
+                        self.work_time += time.time() - self.start_time
+                        self.start_time = None
+                    self.conn = None
+        except (KeyboardInterrupt, SystemExit) as ex:
+            self.server.interrupt = ex
+
+
+class ThreadPool:
+    """A Request Queue for an HTTPServer which pools threads.
+
+    ThreadPool objects must provide min, get(), put(obj), start()
+    and stop(timeout) attributes.
+    """
+
+    def __init__(
+            self, server, min=10, max=-1, accepted_queue_size=-1,
+            accepted_queue_timeout=10):
+        """Initialize HTTP requests queue instance.
+
+        Args:
+            server (cheroot.server.HTTPServer): web server object
+                receiving this request
+            min (int): minimum number of worker threads
+            max (int): maximum number of worker threads
+            accepted_queue_size (int): maximum number of active
+                requests in queue
+            accepted_queue_timeout (int): timeout for putting request
+                into queue
+        """
+        self.server = server
+        self.min = min
+        self.max = max
+        self._threads = []
+        self._queue = queue.Queue(maxsize=accepted_queue_size)
+        self._queue_put_timeout = accepted_queue_timeout
+        self.get = self._queue.get
+
+    def start(self):
+        """Start the pool of threads."""
+        for i in range(self.min):
+            self._threads.append(WorkerThread(self.server))
+        for worker in self._threads:
+            worker.setName('CP Server ' + worker.getName())
+            worker.start()
+        for worker in self._threads:
+            while not worker.ready:
+                time.sleep(.1)
+
+    @property
+    def idle(self):  # noqa: D401; irrelevant for properties
+        """Number of worker threads which are idle. Read-only."""
+        return len([t for t in self._threads if t.conn is None])
+
+    def put(self, obj):
+        """Put request into queue.
+
+        Args:
+            obj (cheroot.server.HTTPConnection): HTTP connection
+                waiting to be processed
+        """
+        self._queue.put(obj, block=True, timeout=self._queue_put_timeout)
+        if obj is _SHUTDOWNREQUEST:
+            return
+
+    def grow(self, amount):
+        """Spawn new worker threads (not above self.max)."""
+        if self.max > 0:
+            budget = max(self.max - len(self._threads), 0)
+        else:
+            # self.max <= 0 indicates no maximum
+            budget = float('inf')
+
+        n_new = min(amount, budget)
+
+        workers = [self._spawn_worker() for i in range(n_new)]
+        while not all(worker.ready for worker in workers):
+            time.sleep(.1)
+        self._threads.extend(workers)
+
+    def _spawn_worker(self):
+        worker = WorkerThread(self.server)
+        worker.setName('CP Server ' + worker.getName())
+        worker.start()
+        return worker
+
+    def shrink(self, amount):
+        """Kill off worker threads (not below self.min)."""
+        # Grow/shrink the pool if necessary.
+        # Remove any dead threads from our list
+        for t in self._threads:
+            if not t.isAlive():
+                self._threads.remove(t)
+                amount -= 1
+
+        # calculate the number of threads above the minimum
+        n_extra = max(len(self._threads) - self.min, 0)
+
+        # don't remove more than amount
+        n_to_remove = min(amount, n_extra)
+
+        # put shutdown requests on the queue equal to the number of threads
+        # to remove. As each request is processed by a worker, that worker
+        # will terminate and be culled from the list.
+        for n in range(n_to_remove):
+            self._queue.put(_SHUTDOWNREQUEST)
+
+    def stop(self, timeout=5):
+        """Terminate all worker threads.
+
+        Args:
+            timeout (int): time to wait for threads to stop gracefully
+        """
+        # Must shut down threads here so the code that calls
+        # this method can know when all threads are stopped.
+        for worker in self._threads:
+            self._queue.put(_SHUTDOWNREQUEST)
+
+        # Don't join currentThread (when stop is called inside a request).
+        current = threading.currentThread()
+        if timeout is not None and timeout >= 0:
+            endtime = time.time() + timeout
+        while self._threads:
+            worker = self._threads.pop()
+            if worker is not current and worker.isAlive():
+                try:
+                    if timeout is None or timeout < 0:
+                        worker.join()
+                    else:
+                        remaining_time = endtime - time.time()
+                        if remaining_time > 0:
+                            worker.join(remaining_time)
+                        if worker.isAlive():
+                            # We exhausted the timeout.
+                            # Forcibly shut down the socket.
+                            c = worker.conn
+                            if c and not c.rfile.closed:
+                                try:
+                                    c.socket.shutdown(socket.SHUT_RD)
+                                except TypeError:
+                                    # pyOpenSSL sockets don't take an arg
+                                    c.socket.shutdown()
+                            worker.join()
+                except (AssertionError,
+                        # Ignore repeated Ctrl-C.
+                        # See
+                        # https://github.com/cherrypy/cherrypy/issues/691.
+                        KeyboardInterrupt):
+                    pass
+
+    @property
+    def qsize(self):
+        """Return the queue size."""
+        return self._queue.qsize()
diff --git a/libraries/cheroot/wsgi.py b/libraries/cheroot/wsgi.py
new file mode 100644
index 00000000..a04c9438
--- /dev/null
+++ b/libraries/cheroot/wsgi.py
@@ -0,0 +1,423 @@
+"""This class holds Cheroot WSGI server implementation.
+
+Simplest example on how to use this server::
+
+    from cheroot import wsgi
+
+    def my_crazy_app(environ, start_response):
+        status = '200 OK'
+        response_headers = [('Content-type','text/plain')]
+        start_response(status, response_headers)
+        return [b'Hello world!']
+
+    addr = '0.0.0.0', 8070
+    server = wsgi.Server(addr, my_crazy_app)
+    server.start()
+
+The Cheroot WSGI server can serve as many WSGI applications
+as you want in one instance by using a PathInfoDispatcher::
+
+    path_map = {
+        '/': my_crazy_app,
+        '/blog': my_blog_app,
+    }
+    d = wsgi.PathInfoDispatcher(path_map)
+    server = wsgi.Server(addr, d)
+"""
+
+from __future__ import absolute_import, division, print_function
+__metaclass__ = type
+
+import sys
+
+import six
+from six.moves import filter
+
+from . import server
+from .workers import threadpool
+from ._compat import ntob, bton
+
+
+class Server(server.HTTPServer):
+    """A subclass of HTTPServer which calls a WSGI application."""
+
+    wsgi_version = (1, 0)
+    """The version of WSGI to produce."""
+
+    def __init__(
+        self, bind_addr, wsgi_app, numthreads=10, server_name=None,
+        max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5,
+        accepted_queue_size=-1, accepted_queue_timeout=10,
+        peercreds_enabled=False, peercreds_resolve_enabled=False,
+    ):
+        """Initialize WSGI Server instance.
+
+        Args:
+            bind_addr (tuple): network interface to listen to
+            wsgi_app (callable): WSGI application callable
+            numthreads (int): number of threads for WSGI thread pool
+            server_name (str): web server name to be advertised via
+                Server HTTP header
+            max (int): maximum number of worker threads
+            request_queue_size (int): the 'backlog' arg to
+                socket.listen(); max queued connections
+            timeout (int): the timeout in seconds for accepted connections
+            shutdown_timeout (int): the total time, in seconds, to
+                wait for worker threads to cleanly exit
+            accepted_queue_size (int): maximum number of active
+                requests in queue
+            accepted_queue_timeout (int): timeout for putting request
+                into queue
+        """
+        super(Server, self).__init__(
+            bind_addr,
+            gateway=wsgi_gateways[self.wsgi_version],
+            server_name=server_name,
+            peercreds_enabled=peercreds_enabled,
+            peercreds_resolve_enabled=peercreds_resolve_enabled,
+        )
+        self.wsgi_app = wsgi_app
+        self.request_queue_size = request_queue_size
+        self.timeout = timeout
+        self.shutdown_timeout = shutdown_timeout
+        self.requests = threadpool.ThreadPool(
+            self, min=numthreads or 1, max=max,
+            accepted_queue_size=accepted_queue_size,
+            accepted_queue_timeout=accepted_queue_timeout)
+
+    @property
+    def numthreads(self):
+        """Set minimum number of threads."""
+        return self.requests.min
+
+    @numthreads.setter
+    def numthreads(self, value):
+        self.requests.min = value
+
+
+class Gateway(server.Gateway):
+    """A base class to interface HTTPServer with WSGI."""
+
+    def __init__(self, req):
+        """Initialize WSGI Gateway instance with request.
+
+        Args:
+            req (HTTPRequest): current HTTP request
+        """
+        super(Gateway, self).__init__(req)
+        self.started_response = False
+        self.env = self.get_environ()
+        self.remaining_bytes_out = None
+
+    @classmethod
+    def gateway_map(cls):
+        """Create a mapping of gateways and their versions.
+
+        Returns:
+            dict[tuple[int,int],class]: map of gateway version and
+                corresponding class
+
+        """
+        return dict(
+            (gw.version, gw)
+            for gw in cls.__subclasses__()
+        )
+
+    def get_environ(self):
+        """Return a new environ dict targeting the given wsgi.version."""
+        raise NotImplementedError
+
+    def respond(self):
+        """Process the current request.
+
+        From :pep:`333`:
+
+            The start_response callable must not actually transmit
+            the response headers. Instead, it must store them for the
+            server or gateway to transmit only after the first
+            iteration of the application return value that yields
+            a NON-EMPTY string, or upon the application's first
+            invocation of the write() callable.
+        """
+        response = self.req.server.wsgi_app(self.env, self.start_response)
+        try:
+            for chunk in filter(None, response):
+                if not isinstance(chunk, six.binary_type):
+                    raise ValueError('WSGI Applications must yield bytes')
+                self.write(chunk)
+        finally:
+            # Send headers if not already sent
+            self.req.ensure_headers_sent()
+            if hasattr(response, 'close'):
+                response.close()
+
+    def start_response(self, status, headers, exc_info=None):
+        """WSGI callable to begin the HTTP response."""
+        # "The application may call start_response more than once,
+        # if and only if the exc_info argument is provided."
+        if self.started_response and not exc_info:
+            raise AssertionError('WSGI start_response called a second '
+                                 'time with no exc_info.')
+        self.started_response = True
+
+        # "if exc_info is provided, and the HTTP headers have already been
+        # sent, start_response must raise an error, and should raise the
+        # exc_info tuple."
+        if self.req.sent_headers:
+            try:
+                six.reraise(*exc_info)
+            finally:
+                exc_info = None
+
+        self.req.status = self._encode_status(status)
+
+        for k, v in headers:
+            if not isinstance(k, str):
+                raise TypeError(
+                    'WSGI response header key %r is not of type str.' % k)
+            if not isinstance(v, str):
+                raise TypeError(
+                    'WSGI response header value %r is not of type str.' % v)
+            if k.lower() == 'content-length':
+                self.remaining_bytes_out = int(v)
+            out_header = ntob(k), ntob(v)
+            self.req.outheaders.append(out_header)
+
+        return self.write
+
+    @staticmethod
+    def _encode_status(status):
+        """Cast status to bytes representation of current Python version.
+
+        According to :pep:`3333`, when using Python 3, the response status
+        and headers must be bytes masquerading as unicode; that is, they
+        must be of type "str" but are restricted to code points in the
+        "latin-1" set.
+        """
+        if six.PY2:
+            return status
+        if not isinstance(status, str):
+            raise TypeError('WSGI response status is not of type str.')
+        return status.encode('ISO-8859-1')
+
+    def write(self, chunk):
+        """WSGI callable to write unbuffered data to the client.
+
+        This method is also used internally by start_response (to write
+        data from the iterable returned by the WSGI application).
+        """
+        if not self.started_response:
+            raise AssertionError('WSGI write called before start_response.')
+
+        chunklen = len(chunk)
+        rbo = self.remaining_bytes_out
+        if rbo is not None and chunklen > rbo:
+            if not self.req.sent_headers:
+                # Whew. We can send a 500 to the client.
+                self.req.simple_response(
+                    '500 Internal Server Error',
+                    'The requested resource returned more bytes than the '
+                    'declared Content-Length.')
+            else:
+                # Dang. We have probably already sent data. Truncate the chunk
+                # to fit (so the client doesn't hang) and raise an error later.
+                chunk = chunk[:rbo]
+
+        self.req.ensure_headers_sent()
+
+        self.req.write(chunk)
+
+        if rbo is not None:
+            rbo -= chunklen
+            if rbo < 0:
+                raise ValueError(
+                    'Response body exceeds the declared Content-Length.')
+
+
+class Gateway_10(Gateway):
+    """A Gateway class to interface HTTPServer with WSGI 1.0.x."""
+
+    version = 1, 0
+
+    def get_environ(self):
+        """Return a new environ dict targeting the given wsgi.version."""
+        req = self.req
+        req_conn = req.conn
+        env = {
+            # set a non-standard environ entry so the WSGI app can know what
+            # the *real* server protocol is (and what features to support).
+            # See http://www.faqs.org/rfcs/rfc2145.html.
+            'ACTUAL_SERVER_PROTOCOL': req.server.protocol,
+            'PATH_INFO': bton(req.path),
+            'QUERY_STRING': bton(req.qs),
+            'REMOTE_ADDR': req_conn.remote_addr or '',
+            'REMOTE_PORT': str(req_conn.remote_port or ''),
+            'REQUEST_METHOD': bton(req.method),
+            'REQUEST_URI': bton(req.uri),
+            'SCRIPT_NAME': '',
+            'SERVER_NAME': req.server.server_name,
+            # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
+            'SERVER_PROTOCOL': bton(req.request_protocol),
+            'SERVER_SOFTWARE': req.server.software,
+            'wsgi.errors': sys.stderr,
+            'wsgi.input': req.rfile,
+            'wsgi.input_terminated': bool(req.chunked_read),
+            'wsgi.multiprocess': False,
+            'wsgi.multithread': True,
+            'wsgi.run_once': False,
+            'wsgi.url_scheme': bton(req.scheme),
+            'wsgi.version': self.version,
+        }
+
+        if isinstance(req.server.bind_addr, six.string_types):
+            # AF_UNIX. This isn't really allowed by WSGI, which doesn't
+            # address unix domain sockets. But it's better than nothing.
+            env['SERVER_PORT'] = ''
+            try:
+                env['X_REMOTE_PID'] = str(req_conn.peer_pid)
+                env['X_REMOTE_UID'] = str(req_conn.peer_uid)
+                env['X_REMOTE_GID'] = str(req_conn.peer_gid)
+
+                env['X_REMOTE_USER'] = str(req_conn.peer_user)
+                env['X_REMOTE_GROUP'] = str(req_conn.peer_group)
+
+                env['REMOTE_USER'] = env['X_REMOTE_USER']
+            except RuntimeError:
+                """Unable to retrieve peer creds data.
+
+                Unsupported by current kernel or socket error happened, or
+                unsupported socket type, or disabled.
+                """
+        else:
+            env['SERVER_PORT'] = str(req.server.bind_addr[1])
+
+        # Request headers
+        env.update(
+            ('HTTP_' + bton(k).upper().replace('-', '_'), bton(v))
+            for k, v in req.inheaders.items()
+        )
+
+        # CONTENT_TYPE/CONTENT_LENGTH
+        ct = env.pop('HTTP_CONTENT_TYPE', None)
+        if ct is not None:
+            env['CONTENT_TYPE'] = ct
+        cl = env.pop('HTTP_CONTENT_LENGTH', None)
+        if cl is not None:
+            env['CONTENT_LENGTH'] = cl
+
+        if req.conn.ssl_env:
+            env.update(req.conn.ssl_env)
+
+        return env
+
+
+class Gateway_u0(Gateway_10):
+    """A Gateway class to interface HTTPServer with WSGI u.0.
+
+    WSGI u.0 is an experimental protocol, which uses unicode for keys
+    and values in both Python 2 and Python 3.
+    """
+
+    version = 'u', 0
+
+    def get_environ(self):
+        """Return a new environ dict targeting the given wsgi.version."""
+        req = self.req
+        env_10 = super(Gateway_u0, self).get_environ()
+        env = dict(map(self._decode_key, env_10.items()))
+
+        # Request-URI
+        enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8'))
+        try:
+            env['PATH_INFO'] = req.path.decode(enc)
+            env['QUERY_STRING'] = req.qs.decode(enc)
+        except UnicodeDecodeError:
+            # Fall back to latin 1 so apps can transcode if needed.
+            env['wsgi.url_encoding'] = 'ISO-8859-1'
+            env['PATH_INFO'] = env_10['PATH_INFO']
+            env['QUERY_STRING'] = env_10['QUERY_STRING']
+
+        env.update(map(self._decode_value, env.items()))
+
+        return env
+
+    @staticmethod
+    def _decode_key(item):
+        k, v = item
+        if six.PY2:
+            k = k.decode('ISO-8859-1')
+        return k, v
+
+    @staticmethod
+    def _decode_value(item):
+        k, v = item
+        skip_keys = 'REQUEST_URI', 'wsgi.input'
+        if six.PY3 or not isinstance(v, bytes) or k in skip_keys:
+            return k, v
+        return k, v.decode('ISO-8859-1')
+
+
+wsgi_gateways = Gateway.gateway_map()
+
+
+class PathInfoDispatcher:
+    """A WSGI dispatcher for dispatch based on the PATH_INFO."""
+
+    def __init__(self, apps):
+        """Initialize path info WSGI app dispatcher.
+
+        Args:
+            apps (dict[str,object]|list[tuple[str,object]]): URI prefix
+                and WSGI app pairs
+        """
+        try:
+            apps = list(apps.items())
+        except AttributeError:
+            pass
+
+        # Sort the apps by len(path), descending
+        def by_path_len(app):
+            return len(app[0])
+        apps.sort(key=by_path_len, reverse=True)
+
+        # The path_prefix strings must start, but not end, with a slash.
+        # Use "" instead of "/".
+        self.apps = [(p.rstrip('/'), a) for p, a in apps]
+
+    def __call__(self, environ, start_response):
+        """Process incoming WSGI request.
+
+        Ref: :pep:`3333`
+
+        Args:
+            environ (Mapping): a dict containing WSGI environment variables
+            start_response (callable): function, which sets response
+                status and headers
+
+        Returns:
+            list[bytes]: iterable containing bytes to be returned in
+                HTTP response body
+
+        """
+        path = environ['PATH_INFO'] or '/'
+        for p, app in self.apps:
+            # The apps list should be sorted by length, descending.
+            if path.startswith(p + '/') or path == p:
+                environ = environ.copy()
+                environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p
+                environ['PATH_INFO'] = path[len(p):]
+                return app(environ, start_response)
+
+        start_response('404 Not Found', [('Content-Type', 'text/plain'),
+                                         ('Content-Length', '0')])
+        return ['']
+
+
+# compatibility aliases
+globals().update(
+    WSGIServer=Server,
+    WSGIGateway=Gateway,
+    WSGIGateway_u0=Gateway_u0,
+    WSGIGateway_10=Gateway_10,
+    WSGIPathInfoDispatcher=PathInfoDispatcher,
+)
diff --git a/libraries/cherrypy/__init__.py b/libraries/cherrypy/__init__.py
new file mode 100644
index 00000000..c5925980
--- /dev/null
+++ b/libraries/cherrypy/__init__.py
@@ -0,0 +1,362 @@
+"""CherryPy is a pythonic, object-oriented HTTP framework.
+
+CherryPy consists of not one, but four separate API layers.
+
+The APPLICATION LAYER is the simplest. CherryPy applications are written as
+a tree of classes and methods, where each branch in the tree corresponds to
+a branch in the URL path. Each method is a 'page handler', which receives
+GET and POST params as keyword arguments, and returns or yields the (HTML)
+body of the response. The special method name 'index' is used for paths
+that end in a slash, and the special method name 'default' is used to
+handle multiple paths via a single handler. This layer also includes:
+
+ * the 'exposed' attribute (and cherrypy.expose)
+ * cherrypy.quickstart()
+ * _cp_config attributes
+ * cherrypy.tools (including cherrypy.session)
+ * cherrypy.url()
+
+The ENVIRONMENT LAYER is used by developers at all levels. It provides
+information about the current request and response, plus the application
+and server environment, via a (default) set of top-level objects:
+
+ * cherrypy.request
+ * cherrypy.response
+ * cherrypy.engine
+ * cherrypy.server
+ * cherrypy.tree
+ * cherrypy.config
+ * cherrypy.thread_data
+ * cherrypy.log
+ * cherrypy.HTTPError, NotFound, and HTTPRedirect
+ * cherrypy.lib
+
+The EXTENSION LAYER allows advanced users to construct and share their own
+plugins. It consists of:
+
+ * Hook API
+ * Tool API
+ * Toolbox API
+ * Dispatch API
+ * Config Namespace API
+
+Finally, there is the CORE LAYER, which uses the core API's to construct
+the default components which are available at higher layers. You can think
+of the default components as the 'reference implementation' for CherryPy.
+Megaframeworks (and advanced users) may replace the default components
+with customized or extended components. The core API's are:
+
+ * Application API
+ * Engine API
+ * Request API
+ * Server API
+ * WSGI API
+
+These API's are described in the `CherryPy specification
+<https://github.com/cherrypy/cherrypy/wiki/CherryPySpec>`_.
+"""
+
+from threading import local as _local
+
+from ._cperror import (
+    HTTPError, HTTPRedirect, InternalRedirect,
+    NotFound, CherryPyException,
+)
+
+from . import _cpdispatch as dispatch
+
+from ._cptools import default_toolbox as tools, Tool
+from ._helper import expose, popargs, url
+
+from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig
+
+import cherrypy.lib.httputil as _httputil
+
+from ._cptree import Application
+from . import _cpwsgi as wsgi
+
+from . import process
+try:
+    from .process import win32
+    engine = win32.Win32Bus()
+    engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
+    del win32
+except ImportError:
+    engine = process.bus
+
+from . import _cpchecker
+
+__all__ = (
+    'HTTPError', 'HTTPRedirect', 'InternalRedirect',
+    'NotFound', 'CherryPyException',
+    'dispatch', 'tools', 'Tool', 'Application',
+    'wsgi', 'process', 'tree', 'engine',
+    'quickstart', 'serving', 'request', 'response', 'thread_data',
+    'log', 'expose', 'popargs', 'url', 'config',
+)
+
+
+__import__('cherrypy._cptools')
+__import__('cherrypy._cprequest')
+
+
+tree = _cptree.Tree()
+
+
+__version__ = '17.4.0'
+
+
+engine.listeners['before_request'] = set()
+engine.listeners['after_request'] = set()
+
+
+engine.autoreload = process.plugins.Autoreloader(engine)
+engine.autoreload.subscribe()
+
+engine.thread_manager = process.plugins.ThreadManager(engine)
+engine.thread_manager.subscribe()
+
+engine.signal_handler = process.plugins.SignalHandler(engine)
+
+
+class _HandleSignalsPlugin(object):
+    """Handle signals from other processes.
+
+    Based on the configured platform handlers above.
+    """
+
+    def __init__(self, bus):
+        self.bus = bus
+
+    def subscribe(self):
+        """Add the handlers based on the platform."""
+        if hasattr(self.bus, 'signal_handler'):
+            self.bus.signal_handler.subscribe()
+        if hasattr(self.bus, 'console_control_handler'):
+            self.bus.console_control_handler.subscribe()
+
+
+engine.signals = _HandleSignalsPlugin(engine)
+
+
+server = _cpserver.Server()
+server.subscribe()
+
+
+def quickstart(root=None, script_name='', config=None):
+    """Mount the given root, start the builtin server (and engine), then block.
+
+    root: an instance of a "controller class" (a collection of page handler
+        methods) which represents the root of the application.
+    script_name: a string containing the "mount point" of the application.
+        This should start with a slash, and be the path portion of the URL
+        at which to mount the given root. For example, if root.index() will
+        handle requests to "http://www.example.com:8080/dept/app1/", then
+        the script_name argument would be "/dept/app1".
+
+        It MUST NOT end in a slash. If the script_name refers to the root
+        of the URI, it MUST be an empty string (not "/").
+    config: a file or dict containing application config. If this contains
+        a [global] section, those entries will be used in the global
+        (site-wide) config.
+    """
+    if config:
+        _global_conf_alias.update(config)
+
+    tree.mount(root, script_name, config)
+
+    engine.signals.subscribe()
+    engine.start()
+    engine.block()
+
+
+class _Serving(_local):
+    """An interface for registering request and response objects.
+
+    Rather than have a separate "thread local" object for the request and
+    the response, this class works as a single threadlocal container for
+    both objects (and any others which developers wish to define). In this
+    way, we can easily dump those objects when we stop/start a new HTTP
+    conversation, yet still refer to them as module-level globals in a
+    thread-safe way.
+    """
+
+    request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
+                                 _httputil.Host('127.0.0.1', 1111))
+    """
+    The request object for the current thread. In the main thread,
+    and any threads which are not receiving HTTP requests, this is None."""
+
+    response = _cprequest.Response()
+    """
+    The response object for the current thread. In the main thread,
+    and any threads which are not receiving HTTP requests, this is None."""
+
+    def load(self, request, response):
+        self.request = request
+        self.response = response
+
+    def clear(self):
+        """Remove all attributes of self."""
+        self.__dict__.clear()
+
+
+serving = _Serving()
+
+
+class _ThreadLocalProxy(object):
+
+    __slots__ = ['__attrname__', '__dict__']
+
+    def __init__(self, attrname):
+        self.__attrname__ = attrname
+
+    def __getattr__(self, name):
+        child = getattr(serving, self.__attrname__)
+        return getattr(child, name)
+
+    def __setattr__(self, name, value):
+        if name in ('__attrname__', ):
+            object.__setattr__(self, name, value)
+        else:
+            child = getattr(serving, self.__attrname__)
+            setattr(child, name, value)
+
+    def __delattr__(self, name):
+        child = getattr(serving, self.__attrname__)
+        delattr(child, name)
+
+    @property
+    def __dict__(self):
+        child = getattr(serving, self.__attrname__)
+        d = child.__class__.__dict__.copy()
+        d.update(child.__dict__)
+        return d
+
+    def __getitem__(self, key):
+        child = getattr(serving, self.__attrname__)
+        return child[key]
+
+    def __setitem__(self, key, value):
+        child = getattr(serving, self.__attrname__)
+        child[key] = value
+
+    def __delitem__(self, key):
+        child = getattr(serving, self.__attrname__)
+        del child[key]
+
+    def __contains__(self, key):
+        child = getattr(serving, self.__attrname__)
+        return key in child
+
+    def __len__(self):
+        child = getattr(serving, self.__attrname__)
+        return len(child)
+
+    def __nonzero__(self):
+        child = getattr(serving, self.__attrname__)
+        return bool(child)
+    # Python 3
+    __bool__ = __nonzero__
+
+
+# Create request and response object (the same objects will be used
+#   throughout the entire life of the webserver, but will redirect
+#   to the "serving" object)
+request = _ThreadLocalProxy('request')
+response = _ThreadLocalProxy('response')
+
+# Create thread_data object as a thread-specific all-purpose storage
+
+
+class _ThreadData(_local):
+    """A container for thread-specific data."""
+
+
+thread_data = _ThreadData()
+
+
+# Monkeypatch pydoc to allow help() to go through the threadlocal proxy.
+# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve.
+# The only other way would be to change what is returned from type(request)
+# and that's not possible in pure Python (you'd have to fake ob_type).
+def _cherrypy_pydoc_resolve(thing, forceload=0):
+    """Given an object or a path to an object, get the object and its name."""
+    if isinstance(thing, _ThreadLocalProxy):
+        thing = getattr(serving, thing.__attrname__)
+    return _pydoc._builtin_resolve(thing, forceload)
+
+
+try:
+    import pydoc as _pydoc
+    _pydoc._builtin_resolve = _pydoc.resolve
+    _pydoc.resolve = _cherrypy_pydoc_resolve
+except ImportError:
+    pass
+
+
+class _GlobalLogManager(_cplogging.LogManager):
+    """A site-wide LogManager; routes to app.log or global log as appropriate.
+
+    This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
+    cherrypy.log() and cherrypy.log.access(). If either
+    function is called during a request, the message will be sent to the
+    logger for the current Application. If they are called outside of a
+    request, the message will be sent to the site-wide logger.
+    """
+
+    def __call__(self, *args, **kwargs):
+        """Log the given message to the app.log or global log.
+
+        Log the given message to the app.log or global
+        log as appropriate.
+        """
+        # Do NOT use try/except here. See
+        # https://github.com/cherrypy/cherrypy/issues/945
+        if hasattr(request, 'app') and hasattr(request.app, 'log'):
+            log = request.app.log
+        else:
+            log = self
+        return log.error(*args, **kwargs)
+
+    def access(self):
+        """Log an access message to the app.log or global log.
+
+        Log the given message to the app.log or global
+        log as appropriate.
+        """
+        try:
+            return request.app.log.access()
+        except AttributeError:
+            return _cplogging.LogManager.access(self)
+
+
+log = _GlobalLogManager()
+# Set a default screen handler on the global log.
+log.screen = True
+log.error_file = ''
+# Using an access file makes CP about 10% slower. Leave off by default.
+log.access_file = ''
+
+
+@engine.subscribe('log')
+def _buslog(msg, level):
+    log.error(msg, 'ENGINE', severity=level)
+
+
+# Use _global_conf_alias so quickstart can use 'config' as an arg
+# without shadowing cherrypy.config.
+config = _global_conf_alias = _cpconfig.Config()
+config.defaults = {
+    'tools.log_tracebacks.on': True,
+    'tools.log_headers.on': True,
+    'tools.trailing_slash.on': True,
+    'tools.encode.on': True
+}
+config.namespaces['log'] = lambda k, v: setattr(log, k, v)
+config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
+# Must reset to get our defaults applied.
+config.reset()
+
+checker = _cpchecker.Checker()
+engine.subscribe('start', checker)
diff --git a/libraries/cherrypy/__main__.py b/libraries/cherrypy/__main__.py
new file mode 100644
index 00000000..6674f7cb
--- /dev/null
+++ b/libraries/cherrypy/__main__.py
@@ -0,0 +1,5 @@
+"""CherryPy'd cherryd daemon runner."""
+from cherrypy.daemon import run
+
+
+__name__ == '__main__' and run()
diff --git a/libraries/cherrypy/_cpchecker.py b/libraries/cherrypy/_cpchecker.py
new file mode 100644
index 00000000..39b7c972
--- /dev/null
+++ b/libraries/cherrypy/_cpchecker.py
@@ -0,0 +1,325 @@
+"""Checker for CherryPy sites and mounted apps."""
+import os
+import warnings
+
+import six
+from six.moves import builtins
+
+import cherrypy
+
+
+class Checker(object):
+    """A checker for CherryPy sites and their mounted applications.
+
+    When this object is called at engine startup, it executes each
+    of its own methods whose names start with ``check_``. If you wish
+    to disable selected checks, simply add a line in your global
+    config which sets the appropriate method to False::
+
+        [global]
+        checker.check_skipped_app_config = False
+
+    You may also dynamically add or replace ``check_*`` methods in this way.
+    """
+
+    on = True
+    """If True (the default), run all checks; if False, turn off all checks."""
+
+    def __init__(self):
+        """Initialize Checker instance."""
+        self._populate_known_types()
+
+    def __call__(self):
+        """Run all check_* methods."""
+        if self.on:
+            oldformatwarning = warnings.formatwarning
+            warnings.formatwarning = self.formatwarning
+            try:
+                for name in dir(self):
+                    if name.startswith('check_'):
+                        method = getattr(self, name)
+                        if method and hasattr(method, '__call__'):
+                            method()
+            finally:
+                warnings.formatwarning = oldformatwarning
+
+    def formatwarning(self, message, category, filename, lineno, line=None):
+        """Format a warning."""
+        return 'CherryPy Checker:\n%s\n\n' % message
+
+    # This value should be set inside _cpconfig.
+    global_config_contained_paths = False
+
+    def check_app_config_entries_dont_start_with_script_name(self):
+        """Check for App config with sections that repeat script_name."""
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            if not app.config:
+                continue
+            if sn == '':
+                continue
+            sn_atoms = sn.strip('/').split('/')
+            for key in app.config.keys():
+                key_atoms = key.strip('/').split('/')
+                if key_atoms[:len(sn_atoms)] == sn_atoms:
+                    warnings.warn(
+                        'The application mounted at %r has config '
+                        'entries that start with its script name: %r' % (sn,
+                                                                         key))
+
+    def check_site_config_entries_in_app_config(self):
+        """Check for mounted Applications that have site-scoped config."""
+        for sn, app in six.iteritems(cherrypy.tree.apps):
+            if not isinstance(app, cherrypy.Application):
+                continue
+
+            msg = []
+            for section, entries in six.iteritems(app.config):
+                if section.startswith('/'):
+                    for key, value in six.iteritems(entries):
+                        for n in ('engine.', 'server.', 'tree.', 'checker.'):
+                            if key.startswith(n):
+                                msg.append('[%s] %s = %s' %
+                                           (section, key, value))
+            if msg:
+                msg.insert(0,
+                           'The application mounted at %r contains the '
+                           'following config entries, which are only allowed '
+                           'in site-wide config. Move them to a [global] '
+                           'section and pass them to cherrypy.config.update() '
+                           'instead of tree.mount().' % sn)
+                warnings.warn(os.linesep.join(msg))
+
+    def check_skipped_app_config(self):
+        """Check for mounted Applications that have no config."""
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            if not app.config:
+                msg = 'The Application mounted at %r has an empty config.' % sn
+                if self.global_config_contained_paths:
+                    msg += (' It looks like the config you passed to '
+                            'cherrypy.config.update() contains application-'
+                            'specific sections. You must explicitly pass '
+                            'application config via '
+                            'cherrypy.tree.mount(..., config=app_config)')
+                warnings.warn(msg)
+                return
+
+    def check_app_config_brackets(self):
+        """Check for App config with extraneous brackets in section names."""
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            if not app.config:
+                continue
+            for key in app.config.keys():
+                if key.startswith('[') or key.endswith(']'):
+                    warnings.warn(
+                        'The application mounted at %r has config '
+                        'section names with extraneous brackets: %r. '
+                        'Config *files* need brackets; config *dicts* '
+                        '(e.g. passed to tree.mount) do not.' % (sn, key))
+
+    def check_static_paths(self):
+        """Check Application config for incorrect static paths."""
+        # Use the dummy Request object in the main thread.
+        request = cherrypy.request
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            request.app = app
+            for section in app.config:
+                # get_resource will populate request.config
+                request.get_resource(section + '/dummy.html')
+                conf = request.config.get
+
+                if conf('tools.staticdir.on', False):
+                    msg = ''
+                    root = conf('tools.staticdir.root')
+                    dir = conf('tools.staticdir.dir')
+                    if dir is None:
+                        msg = 'tools.staticdir.dir is not set.'
+                    else:
+                        fulldir = ''
+                        if os.path.isabs(dir):
+                            fulldir = dir
+                            if root:
+                                msg = ('dir is an absolute path, even '
+                                       'though a root is provided.')
+                                testdir = os.path.join(root, dir[1:])
+                                if os.path.exists(testdir):
+                                    msg += (
+                                        '\nIf you meant to serve the '
+                                        'filesystem folder at %r, remove the '
+                                        'leading slash from dir.' % (testdir,))
+                        else:
+                            if not root:
+                                msg = (
+                                    'dir is a relative path and '
+                                    'no root provided.')
+                            else:
+                                fulldir = os.path.join(root, dir)
+                                if not os.path.isabs(fulldir):
+                                    msg = ('%r is not an absolute path.' % (
+                                        fulldir,))
+
+                        if fulldir and not os.path.exists(fulldir):
+                            if msg:
+                                msg += '\n'
+                            msg += ('%r (root + dir) is not an existing '
+                                    'filesystem path.' % fulldir)
+
+                    if msg:
+                        warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
+                                      % (msg, section, root, dir))
+
+    # -------------------------- Compatibility -------------------------- #
+    obsolete = {
+        'server.default_content_type': 'tools.response_headers.headers',
+        'log_access_file': 'log.access_file',
+        'log_config_options': None,
+        'log_file': 'log.error_file',
+        'log_file_not_found': None,
+        'log_request_headers': 'tools.log_headers.on',
+        'log_to_screen': 'log.screen',
+        'show_tracebacks': 'request.show_tracebacks',
+        'throw_errors': 'request.throw_errors',
+        'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
+                        'cherrypy.Application(Root())))'),
+    }
+
+    deprecated = {}
+
+    def _compat(self, config):
+        """Process config and warn on each obsolete or deprecated entry."""
+        for section, conf in config.items():
+            if isinstance(conf, dict):
+                for k in conf:
+                    if k in self.obsolete:
+                        warnings.warn('%r is obsolete. Use %r instead.\n'
+                                      'section: [%s]' %
+                                      (k, self.obsolete[k], section))
+                    elif k in self.deprecated:
+                        warnings.warn('%r is deprecated. Use %r instead.\n'
+                                      'section: [%s]' %
+                                      (k, self.deprecated[k], section))
+            else:
+                if section in self.obsolete:
+                    warnings.warn('%r is obsolete. Use %r instead.'
+                                  % (section, self.obsolete[section]))
+                elif section in self.deprecated:
+                    warnings.warn('%r is deprecated. Use %r instead.'
+                                  % (section, self.deprecated[section]))
+
+    def check_compatibility(self):
+        """Process config and warn on each obsolete or deprecated entry."""
+        self._compat(cherrypy.config)
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            self._compat(app.config)
+
+    # ------------------------ Known Namespaces ------------------------ #
+    extra_config_namespaces = []
+
+    def _known_ns(self, app):
+        ns = ['wsgi']
+        ns.extend(app.toolboxes)
+        ns.extend(app.namespaces)
+        ns.extend(app.request_class.namespaces)
+        ns.extend(cherrypy.config.namespaces)
+        ns += self.extra_config_namespaces
+
+        for section, conf in app.config.items():
+            is_path_section = section.startswith('/')
+            if is_path_section and isinstance(conf, dict):
+                for k in conf:
+                    atoms = k.split('.')
+                    if len(atoms) > 1:
+                        if atoms[0] not in ns:
+                            # Spit out a special warning if a known
+                            # namespace is preceded by "cherrypy."
+                            if atoms[0] == 'cherrypy' and atoms[1] in ns:
+                                msg = (
+                                    'The config entry %r is invalid; '
+                                    'try %r instead.\nsection: [%s]'
+                                    % (k, '.'.join(atoms[1:]), section))
+                            else:
+                                msg = (
+                                    'The config entry %r is invalid, '
+                                    'because the %r config namespace '
+                                    'is unknown.\n'
+                                    'section: [%s]' % (k, atoms[0], section))
+                            warnings.warn(msg)
+                        elif atoms[0] == 'tools':
+                            if atoms[1] not in dir(cherrypy.tools):
+                                msg = (
+                                    'The config entry %r may be invalid, '
+                                    'because the %r tool was not found.\n'
+                                    'section: [%s]' % (k, atoms[1], section))
+                                warnings.warn(msg)
+
+    def check_config_namespaces(self):
+        """Process config and warn on each unknown config namespace."""
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            self._known_ns(app)
+
+    # -------------------------- Config Types -------------------------- #
+    known_config_types = {}
+
+    def _populate_known_types(self):
+        b = [x for x in vars(builtins).values()
+             if type(x) is type(str)]
+
+        def traverse(obj, namespace):
+            for name in dir(obj):
+                # Hack for 3.2's warning about body_params
+                if name == 'body_params':
+                    continue
+                vtype = type(getattr(obj, name, None))
+                if vtype in b:
+                    self.known_config_types[namespace + '.' + name] = vtype
+
+        traverse(cherrypy.request, 'request')
+        traverse(cherrypy.response, 'response')
+        traverse(cherrypy.server, 'server')
+        traverse(cherrypy.engine, 'engine')
+        traverse(cherrypy.log, 'log')
+
+    def _known_types(self, config):
+        msg = ('The config entry %r in section %r is of type %r, '
+               'which does not match the expected type %r.')
+
+        for section, conf in config.items():
+            if not isinstance(conf, dict):
+                conf = {section: conf}
+            for k, v in conf.items():
+                if v is not None:
+                    expected_type = self.known_config_types.get(k, None)
+                    vtype = type(v)
+                    if expected_type and vtype != expected_type:
+                        warnings.warn(msg % (k, section, vtype.__name__,
+                                             expected_type.__name__))
+
+    def check_config_types(self):
+        """Assert that config values are of the same type as default values."""
+        self._known_types(cherrypy.config)
+        for sn, app in cherrypy.tree.apps.items():
+            if not isinstance(app, cherrypy.Application):
+                continue
+            self._known_types(app.config)
+
+    # -------------------- Specific config warnings -------------------- #
+    def check_localhost(self):
+        """Warn if any socket_host is 'localhost'. See #711."""
+        for k, v in cherrypy.config.items():
+            if k == 'server.socket_host' and v == 'localhost':
+                warnings.warn("The use of 'localhost' as a socket host can "
+                              'cause problems on newer systems, since '
+                              "'localhost' can map to either an IPv4 or an "
+                              "IPv6 address. You should use '127.0.0.1' "
+                              "or '[::1]' instead.")
diff --git a/libraries/cherrypy/_cpcompat.py b/libraries/cherrypy/_cpcompat.py
new file mode 100644
index 00000000..f454505c
--- /dev/null
+++ b/libraries/cherrypy/_cpcompat.py
@@ -0,0 +1,162 @@
+"""Compatibility code for using CherryPy with various versions of Python.
+
+To retain compatibility with older Python versions, this module provides a
+useful abstraction over the differences between Python versions, sometimes by
+preferring a newer idiom, sometimes an older one, and sometimes a custom one.
+
+In particular, Python 2 uses str and '' for byte strings, while Python 3
+uses str and '' for unicode strings. We will call each of these the 'native
+string' type for each version. Because of this major difference, this module
+provides
+two functions: 'ntob', which translates native strings (of type 'str') into
+byte strings regardless of Python version, and 'ntou', which translates native
+strings to unicode strings.
+
+Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'.
+They were created with Python 2.3-2.5 compatibility in mind.
+Instead, use unicode literals (from __future__) and bytes literals
+and their .encode/.decode methods as needed.
+"""
+
+import re
+import sys
+import threading
+
+import six
+from six.moves import urllib
+
+
+if six.PY3:
+    def ntob(n, encoding='ISO-8859-1'):
+        """Return the given native string as a byte string in the given
+        encoding.
+        """
+        assert_native(n)
+        # In Python 3, the native string type is unicode
+        return n.encode(encoding)
+
+    def ntou(n, encoding='ISO-8859-1'):
+        """Return the given native string as a unicode string with the given
+        encoding.
+        """
+        assert_native(n)
+        # In Python 3, the native string type is unicode
+        return n
+
+    def tonative(n, encoding='ISO-8859-1'):
+        """Return the given string as a native string in the given encoding."""
+        # In Python 3, the native string type is unicode
+        if isinstance(n, bytes):
+            return n.decode(encoding)
+        return n
+else:
+    # Python 2
+    def ntob(n, encoding='ISO-8859-1'):
+        """Return the given native string as a byte string in the given
+        encoding.
+        """
+        assert_native(n)
+        # In Python 2, the native string type is bytes. Assume it's already
+        # in the given encoding, which for ISO-8859-1 is almost always what
+        # was intended.
+        return n
+
+    def ntou(n, encoding='ISO-8859-1'):
+        """Return the given native string as a unicode string with the given
+        encoding.
+        """
+        assert_native(n)
+        # In Python 2, the native string type is bytes.
+        # First, check for the special encoding 'escape'. The test suite uses
+        # this to signal that it wants to pass a string with embedded \uXXXX
+        # escapes, but without having to prefix it with u'' for Python 2,
+        # but no prefix for Python 3.
+        if encoding == 'escape':
+            return six.text_type(  # unicode for Python 2
+                re.sub(r'\\u([0-9a-zA-Z]{4})',
+                       lambda m: six.unichr(int(m.group(1), 16)),
+                       n.decode('ISO-8859-1')))
+        # Assume it's already in the given encoding, which for ISO-8859-1
+        # is almost always what was intended.
+        return n.decode(encoding)
+
+    def tonative(n, encoding='ISO-8859-1'):
+        """Return the given string as a native string in the given encoding."""
+        # In Python 2, the native string type is bytes.
+        if isinstance(n, six.text_type):  # unicode for Python 2
+            return n.encode(encoding)
+        return n
+
+
+def assert_native(n):
+    if not isinstance(n, str):
+        raise TypeError('n must be a native str (got %s)' % type(n).__name__)
+
+
+# Some platforms don't expose HTTPSConnection, so handle it separately
+HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None)
+
+
+def _unquote_plus_compat(string, encoding='utf-8', errors='replace'):
+    return urllib.parse.unquote_plus(string).decode(encoding, errors)
+
+
+def _unquote_compat(string, encoding='utf-8', errors='replace'):
+    return urllib.parse.unquote(string).decode(encoding, errors)
+
+
+def _quote_compat(string, encoding='utf-8', errors='replace'):
+    return urllib.parse.quote(string.encode(encoding, errors))
+
+
+unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat
+unquote = urllib.parse.unquote if six.PY3 else _unquote_compat
+quote = urllib.parse.quote if six.PY3 else _quote_compat
+
+try:
+    # Prefer simplejson
+    import simplejson as json
+except ImportError:
+    import json
+
+
+json_decode = json.JSONDecoder().decode
+_json_encode = json.JSONEncoder().iterencode
+
+
+if six.PY3:
+    # Encode to bytes on Python 3
+    def json_encode(value):
+        for chunk in _json_encode(value):
+            yield chunk.encode('utf-8')
+else:
+    json_encode = _json_encode
+
+
+text_or_bytes = six.text_type, bytes
+
+
+if sys.version_info >= (3, 3):
+    Timer = threading.Timer
+    Event = threading.Event
+else:
+    # Python 3.2 and earlier
+    Timer = threading._Timer
+    Event = threading._Event
+
+# html module come in 3.2 version
+try:
+    from html import escape
+except ImportError:
+    from cgi import escape
+
+
+# html module needed the argument quote=False because in cgi the default
+# is False. With quote=True the results differ.
+
+def escape_html(s, escape_quote=False):
+    """Replace special characters "&", "<" and ">" to HTML-safe sequences.
+
+    When escape_quote=True, escape (') and (") chars.
+    """
+    return escape(s, quote=escape_quote)
diff --git a/libraries/cherrypy/_cpconfig.py b/libraries/cherrypy/_cpconfig.py
new file mode 100644
index 00000000..79d9d911
--- /dev/null
+++ b/libraries/cherrypy/_cpconfig.py
@@ -0,0 +1,300 @@
+"""
+Configuration system for CherryPy.
+
+Configuration in CherryPy is implemented via dictionaries. Keys are strings
+which name the mapped value, which may be of any type.
+
+
+Architecture
+------------
+
+CherryPy Requests are part of an Application, which runs in a global context,
+and configuration data may apply to any of those three scopes:
+
+Global
+    Configuration entries which apply everywhere are stored in
+    cherrypy.config.
+
+Application
+    Entries which apply to each mounted application are stored
+    on the Application object itself, as 'app.config'. This is a two-level
+    dict where each key is a path, or "relative URL" (for example, "/" or
+    "/path/to/my/page"), and each value is a config dict. Usually, this
+    data is provided in the call to tree.mount(root(), config=conf),
+    although you may also use app.merge(conf).
+
+Request
+    Each Request object possesses a single 'Request.config' dict.
+    Early in the request process, this dict is populated by merging global
+    config entries, Application entries (whose path equals or is a parent
+    of Request.path_info), and any config acquired while looking up the
+    page handler (see next).
+
+
+Declaration
+-----------
+
+Configuration data may be supplied as a Python dictionary, as a filename,
+or as an open file object. When you supply a filename or file, CherryPy
+uses Python's builtin ConfigParser; you declare Application config by
+writing each path as a section header::
+
+    [/path/to/my/page]
+    request.stream = True
+
+To declare global configuration entries, place them in a [global] section.
+
+You may also declare config entries directly on the classes and methods
+(page handlers) that make up your CherryPy application via the ``_cp_config``
+attribute, set with the ``cherrypy.config`` decorator. For example::
+
+    @cherrypy.config(**{'tools.gzip.on': True})
+    class Demo:
+
+        @cherrypy.expose
+        @cherrypy.config(**{'request.show_tracebacks': False})
+        def index(self):
+            return "Hello world"
+
+.. note::
+
+    This behavior is only guaranteed for the default dispatcher.
+    Other dispatchers may have different restrictions on where
+    you can attach config attributes.
+
+
+Namespaces
+----------
+
+Configuration keys are separated into namespaces by the first "." in the key.
+Current namespaces:
+
+engine
+    Controls the 'application engine', including autoreload.
+    These can only be declared in the global config.
+
+tree
+    Grafts cherrypy.Application objects onto cherrypy.tree.
+    These can only be declared in the global config.
+
+hooks
+    Declares additional request-processing functions.
+
+log
+    Configures the logging for each application.
+    These can only be declared in the global or / config.
+
+request
+    Adds attributes to each Request.
+
+response
+    Adds attributes to each Response.
+
+server
+    Controls the default HTTP server via cherrypy.server.
+    These can only be declared in the global config.
+
+tools
+    Runs and configures additional request-processing packages.
+
+wsgi
+    Adds WSGI middleware to an Application's "pipeline".
+    These can only be declared in the app's root config ("/").
+
+checker
+    Controls the 'checker', which looks for common errors in
+    app state (including config) when the engine starts.
+    Global config only.
+
+The only key that does not exist in a namespace is the "environment" entry.
+This special entry 'imports' other config entries from a template stored in
+cherrypy._cpconfig.environments[environment]. It only applies to the global
+config, and only when you use cherrypy.config.update.
+
+You can define your own namespaces to be called at the Global, Application,
+or Request level, by adding a named handler to cherrypy.config.namespaces,
+app.namespaces, or app.request_class.namespaces. The name can
+be any string, and the handler must be either a callable or a (Python 2.5
+style) context manager.
+"""
+
+import cherrypy
+from cherrypy._cpcompat import text_or_bytes
+from cherrypy.lib import reprconf
+
+
+def _if_filename_register_autoreload(ob):
+    """Register for autoreload if ob is a string (presumed filename)."""
+    is_filename = isinstance(ob, text_or_bytes)
+    is_filename and cherrypy.engine.autoreload.files.add(ob)
+
+
+def merge(base, other):
+    """Merge one app config (from a dict, file, or filename) into another.
+
+    If the given config is a filename, it will be appended to
+    the list of files to monitor for "autoreload" changes.
+    """
+    _if_filename_register_autoreload(other)
+
+    # Load other into base
+    for section, value_map in reprconf.Parser.load(other).items():
+        if not isinstance(value_map, dict):
+            raise ValueError(
+                'Application config must include section headers, but the '
+                "config you tried to merge doesn't have any sections. "
+                'Wrap your config in another dict with paths as section '
+                "headers, for example: {'/': config}.")
+        base.setdefault(section, {}).update(value_map)
+
+
+class Config(reprconf.Config):
+    """The 'global' configuration data for the entire CherryPy process."""
+
+    def update(self, config):
+        """Update self from a dict, file or filename."""
+        _if_filename_register_autoreload(config)
+        super(Config, self).update(config)
+
+    def _apply(self, config):
+        """Update self from a dict."""
+        if isinstance(config.get('global'), dict):
+            if len(config) > 1:
+                cherrypy.checker.global_config_contained_paths = True
+            config = config['global']
+        if 'tools.staticdir.dir' in config:
+            config['tools.staticdir.section'] = 'global'
+        super(Config, self)._apply(config)
+
+    @staticmethod
+    def __call__(**kwargs):
+        """Decorate for page handlers to set _cp_config."""
+        def tool_decorator(f):
+            _Vars(f).setdefault('_cp_config', {}).update(kwargs)
+            return f
+        return tool_decorator
+
+
+class _Vars(object):
+    """Adapter allowing setting a default attribute on a function or class."""
+
+    def __init__(self, target):
+        self.target = target
+
+    def setdefault(self, key, default):
+        if not hasattr(self.target, key):
+            setattr(self.target, key, default)
+        return getattr(self.target, key)
+
+
+# Sphinx begin config.environments
+Config.environments = environments = {
+    'staging': {
+        'engine.autoreload.on': False,
+        'checker.on': False,
+        'tools.log_headers.on': False,
+        'request.show_tracebacks': False,
+        'request.show_mismatched_params': False,
+    },
+    'production': {
+        'engine.autoreload.on': False,
+        'checker.on': False,
+        'tools.log_headers.on': False,
+        'request.show_tracebacks': False,
+        'request.show_mismatched_params': False,
+        'log.screen': False,
+    },
+    'embedded': {
+        # For use with CherryPy embedded in another deployment stack.
+        'engine.autoreload.on': False,
+        'checker.on': False,
+        'tools.log_headers.on': False,
+        'request.show_tracebacks': False,
+        'request.show_mismatched_params': False,
+        'log.screen': False,
+        'engine.SIGHUP': None,
+        'engine.SIGTERM': None,
+    },
+    'test_suite': {
+        'engine.autoreload.on': False,
+        'checker.on': False,
+        'tools.log_headers.on': False,
+        'request.show_tracebacks': True,
+        'request.show_mismatched_params': True,
+        'log.screen': False,
+    },
+}
+# Sphinx end config.environments
+
+
+def _server_namespace_handler(k, v):
+    """Config handler for the "server" namespace."""
+    atoms = k.split('.', 1)
+    if len(atoms) > 1:
+        # Special-case config keys of the form 'server.servername.socket_port'
+        # to configure additional HTTP servers.
+        if not hasattr(cherrypy, 'servers'):
+            cherrypy.servers = {}
+
+        servername, k = atoms
+        if servername not in cherrypy.servers:
+            from cherrypy import _cpserver
+            cherrypy.servers[servername] = _cpserver.Server()
+            # On by default, but 'on = False' can unsubscribe it (see below).
+            cherrypy.servers[servername].subscribe()
+
+        if k == 'on':
+            if v:
+                cherrypy.servers[servername].subscribe()
+            else:
+                cherrypy.servers[servername].unsubscribe()
+        else:
+            setattr(cherrypy.servers[servername], k, v)
+    else:
+        setattr(cherrypy.server, k, v)
+
+
+Config.namespaces['server'] = _server_namespace_handler
+
+
+def _engine_namespace_handler(k, v):
+    """Config handler for the "engine" namespace."""
+    engine = cherrypy.engine
+
+    if k in {'SIGHUP', 'SIGTERM'}:
+        engine.subscribe(k, v)
+        return
+
+    if '.' in k:
+        plugin, attrname = k.split('.', 1)
+        try:
+            plugin = getattr(engine, plugin)
+        except Exception as error:
+            setattr(engine, k, v)
+        else:
+            op = 'subscribe' if v else 'unsubscribe'
+            sub_unsub = getattr(plugin, op, None)
+            if attrname == 'on' and callable(sub_unsub):
+                sub_unsub()
+                return
+            setattr(plugin, attrname, v)
+    else:
+        setattr(engine, k, v)
+
+
+Config.namespaces['engine'] = _engine_namespace_handler
+
+
+def _tree_namespace_handler(k, v):
+    """Namespace handler for the 'tree' config namespace."""
+    if isinstance(v, dict):
+        for script_name, app in v.items():
+            cherrypy.tree.graft(app, script_name)
+            msg = 'Mounted: %s on %s' % (app, script_name or '/')
+            cherrypy.engine.log(msg)
+    else:
+        cherrypy.tree.graft(v, v.script_name)
+        cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
+
+
+Config.namespaces['tree'] = _tree_namespace_handler
diff --git a/libraries/cherrypy/_cpdispatch.py b/libraries/cherrypy/_cpdispatch.py
new file mode 100644
index 00000000..83eb79cb
--- /dev/null
+++ b/libraries/cherrypy/_cpdispatch.py
@@ -0,0 +1,686 @@
+"""CherryPy dispatchers.
+
+A 'dispatcher' is the object which looks up the 'page handler' callable
+and collects config for the current request based on the path_info, other
+request attributes, and the application architecture. The core calls the
+dispatcher as early as possible, passing it a 'path_info' argument.
+
+The default dispatcher discovers the page handler by matching path_info
+to a hierarchical arrangement of objects, starting at request.app.root.
+"""
+
+import string
+import sys
+import types
+try:
+    classtype = (type, types.ClassType)
+except AttributeError:
+    classtype = type
+
+import cherrypy
+
+
+class PageHandler(object):
+
+    """Callable which sets response.body."""
+
+    def __init__(self, callable, *args, **kwargs):
+        self.callable = callable
+        self.args = args
+        self.kwargs = kwargs
+
+    @property
+    def args(self):
+        """The ordered args should be accessible from post dispatch hooks."""
+        return cherrypy.serving.request.args
+
+    @args.setter
+    def args(self, args):
+        cherrypy.serving.request.args = args
+        return cherrypy.serving.request.args
+
+    @property
+    def kwargs(self):
+        """The named kwargs should be accessible from post dispatch hooks."""
+        return cherrypy.serving.request.kwargs
+
+    @kwargs.setter
+    def kwargs(self, kwargs):
+        cherrypy.serving.request.kwargs = kwargs
+        return cherrypy.serving.request.kwargs
+
+    def __call__(self):
+        try:
+            return self.callable(*self.args, **self.kwargs)
+        except TypeError:
+            x = sys.exc_info()[1]
+            try:
+                test_callable_spec(self.callable, self.args, self.kwargs)
+            except cherrypy.HTTPError:
+                raise sys.exc_info()[1]
+            except Exception:
+                raise x
+            raise
+
+
+def test_callable_spec(callable, callable_args, callable_kwargs):
+    """
+    Inspect callable and test to see if the given args are suitable for it.
+
+    When an error occurs during the handler's invoking stage there are 2
+    erroneous cases:
+    1.  Too many parameters passed to a function which doesn't define
+        one of *args or **kwargs.
+    2.  Too little parameters are passed to the function.
+
+    There are 3 sources of parameters to a cherrypy handler.
+    1.  query string parameters are passed as keyword parameters to the
+        handler.
+    2.  body parameters are also passed as keyword parameters.
+    3.  when partial matching occurs, the final path atoms are passed as
+        positional args.
+    Both the query string and path atoms are part of the URI.  If they are
+    incorrect, then a 404 Not Found should be raised. Conversely the body
+    parameters are part of the request; if they are invalid a 400 Bad Request.
+    """
+    show_mismatched_params = getattr(
+        cherrypy.serving.request, 'show_mismatched_params', False)
+    try:
+        (args, varargs, varkw, defaults) = getargspec(callable)
+    except TypeError:
+        if isinstance(callable, object) and hasattr(callable, '__call__'):
+            (args, varargs, varkw,
+             defaults) = getargspec(callable.__call__)
+        else:
+            # If it wasn't one of our own types, re-raise
+            # the original error
+            raise
+
+    if args and (
+            # For callable objects, which have a __call__(self) method
+            hasattr(callable, '__call__') or
+            # For normal methods
+            inspect.ismethod(callable)
+    ):
+        # Strip 'self'
+        args = args[1:]
+
+    arg_usage = dict([(arg, 0,) for arg in args])
+    vararg_usage = 0
+    varkw_usage = 0
+    extra_kwargs = set()
+
+    for i, value in enumerate(callable_args):
+        try:
+            arg_usage[args[i]] += 1
+        except IndexError:
+            vararg_usage += 1
+
+    for key in callable_kwargs.keys():
+        try:
+            arg_usage[key] += 1
+        except KeyError:
+            varkw_usage += 1
+            extra_kwargs.add(key)
+
+    # figure out which args have defaults.
+    args_with_defaults = args[-len(defaults or []):]
+    for i, val in enumerate(defaults or []):
+        # Defaults take effect only when the arg hasn't been used yet.
+        if arg_usage[args_with_defaults[i]] == 0:
+            arg_usage[args_with_defaults[i]] += 1
+
+    missing_args = []
+    multiple_args = []
+    for key, usage in arg_usage.items():
+        if usage == 0:
+            missing_args.append(key)
+        elif usage > 1:
+            multiple_args.append(key)
+
+    if missing_args:
+        # In the case where the method allows body arguments
+        # there are 3 potential errors:
+        # 1. not enough query string parameters -> 404
+        # 2. not enough body parameters -> 400
+        # 3. not enough path parts (partial matches) -> 404
+        #
+        # We can't actually tell which case it is,
+        # so I'm raising a 404 because that covers 2/3 of the
+        # possibilities
+        #
+        # In the case where the method does not allow body
+        # arguments it's definitely a 404.
+        message = None
+        if show_mismatched_params:
+            message = 'Missing parameters: %s' % ','.join(missing_args)
+        raise cherrypy.HTTPError(404, message=message)
+
+    # the extra positional arguments come from the path - 404 Not Found
+    if not varargs and vararg_usage > 0:
+        raise cherrypy.HTTPError(404)
+
+    body_params = cherrypy.serving.request.body.params or {}
+    body_params = set(body_params.keys())
+    qs_params = set(callable_kwargs.keys()) - body_params
+
+    if multiple_args:
+        if qs_params.intersection(set(multiple_args)):
+            # If any of the multiple parameters came from the query string then
+            # it's a 404 Not Found
+            error = 404
+        else:
+            # Otherwise it's a 400 Bad Request
+            error = 400
+
+        message = None
+        if show_mismatched_params:
+            message = 'Multiple values for parameters: '\
+                '%s' % ','.join(multiple_args)
+        raise cherrypy.HTTPError(error, message=message)
+
+    if not varkw and varkw_usage > 0:
+
+        # If there were extra query string parameters, it's a 404 Not Found
+        extra_qs_params = set(qs_params).intersection(extra_kwargs)
+        if extra_qs_params:
+            message = None
+            if show_mismatched_params:
+                message = 'Unexpected query string '\
+                    'parameters: %s' % ', '.join(extra_qs_params)
+            raise cherrypy.HTTPError(404, message=message)
+
+        # If there were any extra body parameters, it's a 400 Not Found
+        extra_body_params = set(body_params).intersection(extra_kwargs)
+        if extra_body_params:
+            message = None
+            if show_mismatched_params:
+                message = 'Unexpected body parameters: '\
+                    '%s' % ', '.join(extra_body_params)
+            raise cherrypy.HTTPError(400, message=message)
+
+
+try:
+    import inspect
+except ImportError:
+    def test_callable_spec(callable, args, kwargs):  # noqa: F811
+        return None
+else:
+    getargspec = inspect.getargspec
+    # Python 3 requires using getfullargspec if
+    # keyword-only arguments are present
+    if hasattr(inspect, 'getfullargspec'):
+        def getargspec(callable):
+            return inspect.getfullargspec(callable)[:4]
+
+
+class LateParamPageHandler(PageHandler):
+
+    """When passing cherrypy.request.params to the page handler, we do not
+    want to capture that dict too early; we want to give tools like the
+    decoding tool a chance to modify the params dict in-between the lookup
+    of the handler and the actual calling of the handler. This subclass
+    takes that into account, and allows request.params to be 'bound late'
+    (it's more complicated than that, but that's the effect).
+    """
+
+    @property
+    def kwargs(self):
+        """Page handler kwargs (with cherrypy.request.params copied in)."""
+        kwargs = cherrypy.serving.request.params.copy()
+        if self._kwargs:
+            kwargs.update(self._kwargs)
+        return kwargs
+
+    @kwargs.setter
+    def kwargs(self, kwargs):
+        cherrypy.serving.request.kwargs = kwargs
+        self._kwargs = kwargs
+
+
+if sys.version_info < (3, 0):
+    punctuation_to_underscores = string.maketrans(
+        string.punctuation, '_' * len(string.punctuation))
+
+    def validate_translator(t):
+        if not isinstance(t, str) or len(t) != 256:
+            raise ValueError(
+                'The translate argument must be a str of len 256.')
+else:
+    punctuation_to_underscores = str.maketrans(
+        string.punctuation, '_' * len(string.punctuation))
+
+    def validate_translator(t):
+        if not isinstance(t, dict):
+            raise ValueError('The translate argument must be a dict.')
+
+
+class Dispatcher(object):
+
+    """CherryPy Dispatcher which walks a tree of objects to find a handler.
+
+    The tree is rooted at cherrypy.request.app.root, and each hierarchical
+    component in the path_info argument is matched to a corresponding nested
+    attribute of the root object. Matching handlers must have an 'exposed'
+    attribute which evaluates to True. The special method name "index"
+    matches a URI which ends in a slash ("/"). The special method name
+    "default" may match a portion of the path_info (but only when no longer
+    substring of the path_info matches some other object).
+
+    This is the default, built-in dispatcher for CherryPy.
+    """
+
+    dispatch_method_name = '_cp_dispatch'
+    """
+    The name of the dispatch method that nodes may optionally implement
+    to provide their own dynamic dispatch algorithm.
+    """
+
+    def __init__(self, dispatch_method_name=None,
+                 translate=punctuation_to_underscores):
+        validate_translator(translate)
+        self.translate = translate
+        if dispatch_method_name:
+            self.dispatch_method_name = dispatch_method_name
+
+    def __call__(self, path_info):
+        """Set handler and config for the current request."""
+        request = cherrypy.serving.request
+        func, vpath = self.find_handler(path_info)
+
+        if func:
+            # Decode any leftover %2F in the virtual_path atoms.
+            vpath = [x.replace('%2F', '/') for x in vpath]
+            request.handler = LateParamPageHandler(func, *vpath)
+        else:
+            request.handler = cherrypy.NotFound()
+
+    def find_handler(self, path):
+        """Return the appropriate page handler, plus any virtual path.
+
+        This will return two objects. The first will be a callable,
+        which can be used to generate page output. Any parameters from
+        the query string or request body will be sent to that callable
+        as keyword arguments.
+
+        The callable is found by traversing the application's tree,
+        starting from cherrypy.request.app.root, and matching path
+        components to successive objects in the tree. For example, the
+        URL "/path/to/handler" might return root.path.to.handler.
+
+        The second object returned will be a list of names which are
+        'virtual path' components: parts of the URL which are dynamic,
+        and were not used when looking up the handler.
+        These virtual path components are passed to the handler as
+        positional arguments.
+        """
+        request = cherrypy.serving.request
+        app = request.app
+        root = app.root
+        dispatch_name = self.dispatch_method_name
+
+        # Get config for the root object/path.
+        fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
+        fullpath_len = len(fullpath)
+        segleft = fullpath_len
+        nodeconf = {}
+        if hasattr(root, '_cp_config'):
+            nodeconf.update(root._cp_config)
+        if '/' in app.config:
+            nodeconf.update(app.config['/'])
+        object_trail = [['root', root, nodeconf, segleft]]
+
+        node = root
+        iternames = fullpath[:]
+        while iternames:
+            name = iternames[0]
+            # map to legal Python identifiers (e.g. replace '.' with '_')
+            objname = name.translate(self.translate)
+
+            nodeconf = {}
+            subnode = getattr(node, objname, None)
+            pre_len = len(iternames)
+            if subnode is None:
+                dispatch = getattr(node, dispatch_name, None)
+                if dispatch and hasattr(dispatch, '__call__') and not \
+                        getattr(dispatch, 'exposed', False) and \
+                        pre_len > 1:
+                    # Don't expose the hidden 'index' token to _cp_dispatch
+                    # We skip this if pre_len == 1 since it makes no sense
+                    # to call a dispatcher when we have no tokens left.
+                    index_name = iternames.pop()
+                    subnode = dispatch(vpath=iternames)
+                    iternames.append(index_name)
+                else:
+                    # We didn't find a path, but keep processing in case there
+                    # is a default() handler.
+                    iternames.pop(0)
+            else:
+                # We found the path, remove the vpath entry
+                iternames.pop(0)
+            segleft = len(iternames)
+            if segleft > pre_len:
+                # No path segment was removed.  Raise an error.
+                raise cherrypy.CherryPyException(
+                    'A vpath segment was added.  Custom dispatchers may only '
+                    'remove elements.  While trying to process '
+                    '{0} in {1}'.format(name, fullpath)
+                )
+            elif segleft == pre_len:
+                # Assume that the handler used the current path segment, but
+                # did not pop it.  This allows things like
+                # return getattr(self, vpath[0], None)
+                iternames.pop(0)
+                segleft -= 1
+            node = subnode
+
+            if node is not None:
+                # Get _cp_config attached to this node.
+                if hasattr(node, '_cp_config'):
+                    nodeconf.update(node._cp_config)
+
+            # Mix in values from app.config for this path.
+            existing_len = fullpath_len - pre_len
+            if existing_len != 0:
+                curpath = '/' + '/'.join(fullpath[0:existing_len])
+            else:
+                curpath = ''
+            new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
+            for seg in new_segs:
+                curpath += '/' + seg
+                if curpath in app.config:
+                    nodeconf.update(app.config[curpath])
+
+            object_trail.append([name, node, nodeconf, segleft])
+
+        def set_conf():
+            """Collapse all object_trail config into cherrypy.request.config.
+            """
+            base = cherrypy.config.copy()
+            # Note that we merge the config from each node
+            # even if that node was None.
+            for name, obj, conf, segleft in object_trail:
+                base.update(conf)
+                if 'tools.staticdir.dir' in conf:
+                    base['tools.staticdir.section'] = '/' + \
+                        '/'.join(fullpath[0:fullpath_len - segleft])
+            return base
+
+        # Try successive objects (reverse order)
+        num_candidates = len(object_trail) - 1
+        for i in range(num_candidates, -1, -1):
+
+            name, candidate, nodeconf, segleft = object_trail[i]
+            if candidate is None:
+                continue
+
+            # Try a "default" method on the current leaf.
+            if hasattr(candidate, 'default'):
+                defhandler = candidate.default
+                if getattr(defhandler, 'exposed', False):
+                    # Insert any extra _cp_config from the default handler.
+                    conf = getattr(defhandler, '_cp_config', {})
+                    object_trail.insert(
+                        i + 1, ['default', defhandler, conf, segleft])
+                    request.config = set_conf()
+                    # See https://github.com/cherrypy/cherrypy/issues/613
+                    request.is_index = path.endswith('/')
+                    return defhandler, fullpath[fullpath_len - segleft:-1]
+
+            # Uncomment the next line to restrict positional params to
+            # "default".
+            # if i < num_candidates - 2: continue
+
+            # Try the current leaf.
+            if getattr(candidate, 'exposed', False):
+                request.config = set_conf()
+                if i == num_candidates:
+                    # We found the extra ".index". Mark request so tools
+                    # can redirect if path_info has no trailing slash.
+                    request.is_index = True
+                else:
+                    # We're not at an 'index' handler. Mark request so tools
+                    # can redirect if path_info has NO trailing slash.
+                    # Note that this also includes handlers which take
+                    # positional parameters (virtual paths).
+                    request.is_index = False
+                return candidate, fullpath[fullpath_len - segleft:-1]
+
+        # We didn't find anything
+        request.config = set_conf()
+        return None, []
+
+
+class MethodDispatcher(Dispatcher):
+
+    """Additional dispatch based on cherrypy.request.method.upper().
+
+    Methods named GET, POST, etc will be called on an exposed class.
+    The method names must be all caps; the appropriate Allow header
+    will be output showing all capitalized method names as allowable
+    HTTP verbs.
+
+    Note that the containing class must be exposed, not the methods.
+    """
+
+    def __call__(self, path_info):
+        """Set handler and config for the current request."""
+        request = cherrypy.serving.request
+        resource, vpath = self.find_handler(path_info)
+
+        if resource:
+            # Set Allow header
+            avail = [m for m in dir(resource) if m.isupper()]
+            if 'GET' in avail and 'HEAD' not in avail:
+                avail.append('HEAD')
+            avail.sort()
+            cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
+
+            # Find the subhandler
+            meth = request.method.upper()
+            func = getattr(resource, meth, None)
+            if func is None and meth == 'HEAD':
+                func = getattr(resource, 'GET', None)
+            if func:
+                # Grab any _cp_config on the subhandler.
+                if hasattr(func, '_cp_config'):
+                    request.config.update(func._cp_config)
+
+                # Decode any leftover %2F in the virtual_path atoms.
+                vpath = [x.replace('%2F', '/') for x in vpath]
+                request.handler = LateParamPageHandler(func, *vpath)
+            else:
+                request.handler = cherrypy.HTTPError(405)
+        else:
+            request.handler = cherrypy.NotFound()
+
+
+class RoutesDispatcher(object):
+
+    """A Routes based dispatcher for CherryPy."""
+
+    def __init__(self, full_result=False, **mapper_options):
+        """
+        Routes dispatcher
+
+        Set full_result to True if you wish the controller
+        and the action to be passed on to the page handler
+        parameters. By default they won't be.
+        """
+        import routes
+        self.full_result = full_result
+        self.controllers = {}
+        self.mapper = routes.Mapper(**mapper_options)
+        self.mapper.controller_scan = self.controllers.keys
+
+    def connect(self, name, route, controller, **kwargs):
+        self.controllers[name] = controller
+        self.mapper.connect(name, route, controller=name, **kwargs)
+
+    def redirect(self, url):
+        raise cherrypy.HTTPRedirect(url)
+
+    def __call__(self, path_info):
+        """Set handler and config for the current request."""
+        func = self.find_handler(path_info)
+        if func:
+            cherrypy.serving.request.handler = LateParamPageHandler(func)
+        else:
+            cherrypy.serving.request.handler = cherrypy.NotFound()
+
+    def find_handler(self, path_info):
+        """Find the right page handler, and set request.config."""
+        import routes
+
+        request = cherrypy.serving.request
+
+        config = routes.request_config()
+        config.mapper = self.mapper
+        if hasattr(request, 'wsgi_environ'):
+            config.environ = request.wsgi_environ
+        config.host = request.headers.get('Host', None)
+        config.protocol = request.scheme
+        config.redirect = self.redirect
+
+        result = self.mapper.match(path_info)
+
+        config.mapper_dict = result
+        params = {}
+        if result:
+            params = result.copy()
+        if not self.full_result:
+            params.pop('controller', None)
+            params.pop('action', None)
+        request.params.update(params)
+
+        # Get config for the root object/path.
+        request.config = base = cherrypy.config.copy()
+        curpath = ''
+
+        def merge(nodeconf):
+            if 'tools.staticdir.dir' in nodeconf:
+                nodeconf['tools.staticdir.section'] = curpath or '/'
+            base.update(nodeconf)
+
+        app = request.app
+        root = app.root
+        if hasattr(root, '_cp_config'):
+            merge(root._cp_config)
+        if '/' in app.config:
+            merge(app.config['/'])
+
+        # Mix in values from app.config.
+        atoms = [x for x in path_info.split('/') if x]
+        if atoms:
+            last = atoms.pop()
+        else:
+            last = None
+        for atom in atoms:
+            curpath = '/'.join((curpath, atom))
+            if curpath in app.config:
+                merge(app.config[curpath])
+
+        handler = None
+        if result:
+            controller = result.get('controller')
+            controller = self.controllers.get(controller, controller)
+            if controller:
+                if isinstance(controller, classtype):
+                    controller = controller()
+                # Get config from the controller.
+                if hasattr(controller, '_cp_config'):
+                    merge(controller._cp_config)
+
+            action = result.get('action')
+            if action is not None:
+                handler = getattr(controller, action, None)
+                # Get config from the handler
+                if hasattr(handler, '_cp_config'):
+                    merge(handler._cp_config)
+            else:
+                handler = controller
+
+        # Do the last path atom here so it can
+        # override the controller's _cp_config.
+        if last:
+            curpath = '/'.join((curpath, last))
+            if curpath in app.config:
+                merge(app.config[curpath])
+
+        return handler
+
+
+def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
+    from cherrypy.lib import xmlrpcutil
+
+    def xmlrpc_dispatch(path_info):
+        path_info = xmlrpcutil.patched_path(path_info)
+        return next_dispatcher(path_info)
+    return xmlrpc_dispatch
+
+
+def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
+                **domains):
+    """
+    Select a different handler based on the Host header.
+
+    This can be useful when running multiple sites within one CP server.
+    It allows several domains to point to different parts of a single
+    website structure. For example::
+
+        http://www.domain.example  ->  root
+        http://www.domain2.example  ->  root/domain2/
+        http://www.domain2.example:443  ->  root/secure
+
+    can be accomplished via the following config::
+
+        [/]
+        request.dispatch = cherrypy.dispatch.VirtualHost(
+            **{'www.domain2.example': '/domain2',
+               'www.domain2.example:443': '/secure',
+              })
+
+    next_dispatcher
+        The next dispatcher object in the dispatch chain.
+        The VirtualHost dispatcher adds a prefix to the URL and calls
+        another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
+
+    use_x_forwarded_host
+        If True (the default), any "X-Forwarded-Host"
+        request header will be used instead of the "Host" header. This
+        is commonly added by HTTP servers (such as Apache) when proxying.
+
+    ``**domains``
+        A dict of {host header value: virtual prefix} pairs.
+        The incoming "Host" request header is looked up in this dict,
+        and, if a match is found, the corresponding "virtual prefix"
+        value will be prepended to the URL path before calling the
+        next dispatcher. Note that you often need separate entries
+        for "example.com" and "www.example.com". In addition, "Host"
+        headers may contain the port number.
+    """
+    from cherrypy.lib import httputil
+
+    def vhost_dispatch(path_info):
+        request = cherrypy.serving.request
+        header = request.headers.get
+
+        domain = header('Host', '')
+        if use_x_forwarded_host:
+            domain = header('X-Forwarded-Host', domain)
+
+        prefix = domains.get(domain, '')
+        if prefix:
+            path_info = httputil.urljoin(prefix, path_info)
+
+        result = next_dispatcher(path_info)
+
+        # Touch up staticdir config. See
+        # https://github.com/cherrypy/cherrypy/issues/614.
+        section = request.config.get('tools.staticdir.section')
+        if section:
+            section = section[len(prefix):]
+            request.config['tools.staticdir.section'] = section
+
+        return result
+    return vhost_dispatch
diff --git a/libraries/cherrypy/_cperror.py b/libraries/cherrypy/_cperror.py
new file mode 100644
index 00000000..e2a8fad8
--- /dev/null
+++ b/libraries/cherrypy/_cperror.py
@@ -0,0 +1,619 @@
+"""Exception classes for CherryPy.
+
+CherryPy provides (and uses) exceptions for declaring that the HTTP response
+should be a status other than the default "200 OK". You can ``raise`` them like
+normal Python exceptions. You can also call them and they will raise
+themselves; this means you can set an
+:class:`HTTPError<cherrypy._cperror.HTTPError>`
+or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
+:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
+
+.. _redirectingpost:
+
+Redirecting POST
+================
+
+When you GET a resource and are redirected by the server to another Location,
+there's generally no problem since GET is both a "safe method" (there should
+be no side-effects) and an "idempotent method" (multiple calls are no different
+than a single call).
+
+POST, however, is neither safe nor idempotent--if you
+charge a credit card, you don't want to be charged twice by a redirect!
+
+For this reason, *none* of the 3xx responses permit a user-agent (browser) to
+resubmit a POST on redirection without first confirming the action with the
+user:
+
+=====    =================================    ===========
+300      Multiple Choices                     Confirm with the user
+301      Moved Permanently                    Confirm with the user
+302      Found (Object moved temporarily)     Confirm with the user
+303      See Other                            GET the new URI; no confirmation
+304      Not modified                         for conditional GET only;
+                                              POST should not raise this error
+305      Use Proxy                            Confirm with the user
+307      Temporary Redirect                   Confirm with the user
+=====    =================================    ===========
+
+However, browsers have historically implemented these restrictions poorly;
+in particular, many browsers do not force the user to confirm 301, 302
+or 307 when redirecting POST. For this reason, CherryPy defaults to 303,
+which most user-agents appear to have implemented correctly. Therefore, if
+you raise HTTPRedirect for a POST request, the user-agent will most likely
+attempt to GET the new URI (without asking for confirmation from the user).
+We realize this is confusing for developers, but it's the safest thing we
+could do. You are of course free to raise ``HTTPRedirect(uri, status=302)``
+or any other 3xx status if you know what you're doing, but given the
+environment, we couldn't let any of those be the default.
+
+Custom Error Handling
+=====================
+
+.. image:: /refman/cperrors.gif
+
+Anticipated HTTP responses
+--------------------------
+
+The 'error_page' config namespace can be used to provide custom HTML output for
+expected responses (like 404 Not Found). Supply a filename from which the
+output will be read. The contents will be interpolated with the values
+%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
+`string formatting
+<http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
+
+::
+
+    _cp_config = {
+        'error_page.404': os.path.join(localDir, "static/index.html")
+    }
+
+
+Beginning in version 3.1, you may also provide a function or other callable as
+an error_page entry. It will be passed the same status, message, traceback and
+version arguments that are interpolated into templates::
+
+    def error_page_402(status, message, traceback, version):
+        return "Error %s - Well, I'm very sorry but you haven't paid!" % status
+    cherrypy.config.update({'error_page.402': error_page_402})
+
+Also in 3.1, in addition to the numbered error codes, you may also supply
+"error_page.default" to handle all codes which do not have their own error_page
+entry.
+
+
+
+Unanticipated errors
+--------------------
+
+CherryPy also has a generic error handling mechanism: whenever an unanticipated
+error occurs in your code, it will call
+:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
+set the response status, headers, and body. By default, this is the same
+output as
+:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
+some other behavior, you generally replace "request.error_response".
+
+Here is some sample code that shows how to display a custom error message and
+send an e-mail containing the error::
+
+    from cherrypy import _cperror
+
+    def handle_error():
+        cherrypy.response.status = 500
+        cherrypy.response.body = [
+            "<html><body>Sorry, an error occurred</body></html>"
+        ]
+        sendMail('error@domain.com',
+                 'Error in your web app',
+                 _cperror.format_exc())
+
+    @cherrypy.config(**{'request.error_response': handle_error})
+    class Root:
+        pass
+
+Note that you have to explicitly set
+:attr:`response.body <cherrypy._cprequest.Response.body>`
+and not simply return an error message as a result.
+"""
+
+import io
+import contextlib
+from sys import exc_info as _exc_info
+from traceback import format_exception as _format_exception
+from xml.sax import saxutils
+
+import six
+from six.moves import urllib
+
+from more_itertools import always_iterable
+
+import cherrypy
+from cherrypy._cpcompat import escape_html
+from cherrypy._cpcompat import ntob
+from cherrypy._cpcompat import tonative
+from cherrypy._helper import classproperty
+from cherrypy.lib import httputil as _httputil
+
+
+class CherryPyException(Exception):
+
+    """A base class for CherryPy exceptions."""
+    pass
+
+
+class InternalRedirect(CherryPyException):
+
+    """Exception raised to switch to the handler for a different URL.
+
+    This exception will redirect processing to another path within the site
+    (without informing the client). Provide the new path as an argument when
+    raising the exception. Provide any params in the querystring for the new
+    URL.
+    """
+
+    def __init__(self, path, query_string=''):
+        self.request = cherrypy.serving.request
+
+        self.query_string = query_string
+        if '?' in path:
+            # Separate any params included in the path
+            path, self.query_string = path.split('?', 1)
+
+        # Note that urljoin will "do the right thing" whether url is:
+        #  1. a URL relative to root (e.g. "/dummy")
+        #  2. a URL relative to the current path
+        # Note that any query string will be discarded.
+        path = urllib.parse.urljoin(self.request.path_info, path)
+
+        # Set a 'path' member attribute so that code which traps this
+        # error can have access to it.
+        self.path = path
+
+        CherryPyException.__init__(self, path, self.query_string)
+
+
+class HTTPRedirect(CherryPyException):
+
+    """Exception raised when the request should be redirected.
+
+    This exception will force a HTTP redirect to the URL or URL's you give it.
+    The new URL must be passed as the first argument to the Exception,
+    e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
+    If a URL is absolute, it will be used as-is. If it is relative, it is
+    assumed to be relative to the current cherrypy.request.path_info.
+
+    If one of the provided URL is a unicode object, it will be encoded
+    using the default encoding or the one passed in parameter.
+
+    There are multiple types of redirect, from which you can select via the
+    ``status`` argument. If you do not provide a ``status`` arg, it defaults to
+    303 (or 302 if responding with HTTP/1.0).
+
+    Examples::
+
+        raise cherrypy.HTTPRedirect("")
+        raise cherrypy.HTTPRedirect("/abs/path", 307)
+        raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
+
+    See :ref:`redirectingpost` for additional caveats.
+    """
+
+    urls = None
+    """The list of URL's to emit."""
+
+    encoding = 'utf-8'
+    """The encoding when passed urls are not native strings"""
+
+    def __init__(self, urls, status=None, encoding=None):
+        self.urls = abs_urls = [
+            # Note that urljoin will "do the right thing" whether url is:
+            #  1. a complete URL with host (e.g. "http://www.example.com/test")
+            #  2. a URL relative to root (e.g. "/dummy")
+            #  3. a URL relative to the current path
+            # Note that any query string in cherrypy.request is discarded.
+            urllib.parse.urljoin(
+                cherrypy.url(),
+                tonative(url, encoding or self.encoding),
+            )
+            for url in always_iterable(urls)
+        ]
+
+        status = (
+            int(status)
+            if status is not None
+            else self.default_status
+        )
+        if not 300 <= status <= 399:
+            raise ValueError('status must be between 300 and 399.')
+
+        CherryPyException.__init__(self, abs_urls, status)
+
+    @classproperty
+    def default_status(cls):
+        """
+        The default redirect status for the request.
+
+        RFC 2616 indicates a 301 response code fits our goal; however,
+        browser support for 301 is quite messy. Use 302/303 instead. See
+        http://www.alanflavell.org.uk/www/post-redirect.html
+        """
+        return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302
+
+    @property
+    def status(self):
+        """The integer HTTP status code to emit."""
+        _, status = self.args[:2]
+        return status
+
+    def set_response(self):
+        """Modify cherrypy.response status, headers, and body to represent
+        self.
+
+        CherryPy uses this internally, but you can also use it to create an
+        HTTPRedirect object and set its output without *raising* the exception.
+        """
+        response = cherrypy.serving.response
+        response.status = status = self.status
+
+        if status in (300, 301, 302, 303, 307):
+            response.headers['Content-Type'] = 'text/html;charset=utf-8'
+            # "The ... URI SHOULD be given by the Location field
+            # in the response."
+            response.headers['Location'] = self.urls[0]
+
+            # "Unless the request method was HEAD, the entity of the response
+            # SHOULD contain a short hypertext note with a hyperlink to the
+            # new URI(s)."
+            msg = {
+                300: 'This resource can be found at ',
+                301: 'This resource has permanently moved to ',
+                302: 'This resource resides temporarily at ',
+                303: 'This resource can be found at ',
+                307: 'This resource has moved temporarily to ',
+            }[status]
+            msg += '<a href=%s>%s</a>.'
+            msgs = [
+                msg % (saxutils.quoteattr(u), escape_html(u))
+                for u in self.urls
+            ]
+            response.body = ntob('<br />\n'.join(msgs), 'utf-8')
+            # Previous code may have set C-L, so we have to reset it
+            # (allow finalize to set it).
+            response.headers.pop('Content-Length', None)
+        elif status == 304:
+            # Not Modified.
+            # "The response MUST include the following header fields:
+            # Date, unless its omission is required by section 14.18.1"
+            # The "Date" header should have been set in Response.__init__
+
+            # "...the response SHOULD NOT include other entity-headers."
+            for key in ('Allow', 'Content-Encoding', 'Content-Language',
+                        'Content-Length', 'Content-Location', 'Content-MD5',
+                        'Content-Range', 'Content-Type', 'Expires',
+                        'Last-Modified'):
+                if key in response.headers:
+                    del response.headers[key]
+
+            # "The 304 response MUST NOT contain a message-body."
+            response.body = None
+            # Previous code may have set C-L, so we have to reset it.
+            response.headers.pop('Content-Length', None)
+        elif status == 305:
+            # Use Proxy.
+            # self.urls[0] should be the URI of the proxy.
+            response.headers['Location'] = ntob(self.urls[0], 'utf-8')
+            response.body = None
+            # Previous code may have set C-L, so we have to reset it.
+            response.headers.pop('Content-Length', None)
+        else:
+            raise ValueError('The %s status code is unknown.' % status)
+
+    def __call__(self):
+        """Use this exception as a request.handler (raise self)."""
+        raise self
+
+
+def clean_headers(status):
+    """Remove any headers which should not apply to an error response."""
+    response = cherrypy.serving.response
+
+    # Remove headers which applied to the original content,
+    # but do not apply to the error page.
+    respheaders = response.headers
+    for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
+                'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
+                'Content-Location', 'Content-MD5', 'Last-Modified']:
+        if key in respheaders:
+            del respheaders[key]
+
+    if status != 416:
+        # A server sending a response with status code 416 (Requested
+        # range not satisfiable) SHOULD include a Content-Range field
+        # with a byte-range-resp-spec of "*". The instance-length
+        # specifies the current length of the selected resource.
+        # A response with status code 206 (Partial Content) MUST NOT
+        # include a Content-Range field with a byte-range- resp-spec of "*".
+        if 'Content-Range' in respheaders:
+            del respheaders['Content-Range']
+
+
+class HTTPError(CherryPyException):
+
+    """Exception used to return an HTTP error code (4xx-5xx) to the client.
+
+    This exception can be used to automatically send a response using a
+    http status code, with an appropriate error page. It takes an optional
+    ``status`` argument (which must be between 400 and 599); it defaults to 500
+    ("Internal Server Error"). It also takes an optional ``message`` argument,
+    which will be returned in the response body. See
+    `RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
+    for a complete list of available error codes and when to use them.
+
+    Examples::
+
+        raise cherrypy.HTTPError(403)
+        raise cherrypy.HTTPError(
+            "403 Forbidden", "You are not allowed to access this resource.")
+    """
+
+    status = None
+    """The HTTP status code. May be of type int or str (with a Reason-Phrase).
+    """
+
+    code = None
+    """The integer HTTP status code."""
+
+    reason = None
+    """The HTTP Reason-Phrase string."""
+
+    def __init__(self, status=500, message=None):
+        self.status = status
+        try:
+            self.code, self.reason, defaultmsg = _httputil.valid_status(status)
+        except ValueError:
+            raise self.__class__(500, _exc_info()[1].args[0])
+
+        if self.code < 400 or self.code > 599:
+            raise ValueError('status must be between 400 and 599.')
+
+        # See http://www.python.org/dev/peps/pep-0352/
+        # self.message = message
+        self._message = message or defaultmsg
+        CherryPyException.__init__(self, status, message)
+
+    def set_response(self):
+        """Modify cherrypy.response status, headers, and body to represent
+        self.
+
+        CherryPy uses this internally, but you can also use it to create an
+        HTTPError object and set its output without *raising* the exception.
+        """
+        response = cherrypy.serving.response
+
+        clean_headers(self.code)
+
+        # In all cases, finalize will be called after this method,
+        # so don't bother cleaning up response values here.
+        response.status = self.status
+        tb = None
+        if cherrypy.serving.request.show_tracebacks:
+            tb = format_exc()
+
+        response.headers.pop('Content-Length', None)
+
+        content = self.get_error_page(self.status, traceback=tb,
+                                      message=self._message)
+        response.body = content
+
+        _be_ie_unfriendly(self.code)
+
+    def get_error_page(self, *args, **kwargs):
+        return get_error_page(*args, **kwargs)
+
+    def __call__(self):
+        """Use this exception as a request.handler (raise self)."""
+        raise self
+
+    @classmethod
+    @contextlib.contextmanager
+    def handle(cls, exception, status=500, message=''):
+        """Translate exception into an HTTPError."""
+        try:
+            yield
+        except exception as exc:
+            raise cls(status, message or str(exc))
+
+
+class NotFound(HTTPError):
+
+    """Exception raised when a URL could not be mapped to any handler (404).
+
+    This is equivalent to raising
+    :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
+    """
+
+    def __init__(self, path=None):
+        if path is None:
+            request = cherrypy.serving.request
+            path = request.script_name + request.path_info
+        self.args = (path,)
+        HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
+
+
+_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
+"-//W3C//DTD XHTML 1.0 Transitional//EN"
+"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html>
+<head>
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
+    <title>%(status)s</title>
+    <style type="text/css">
+    #powered_by {
+        margin-top: 20px;
+        border-top: 2px solid black;
+        font-style: italic;
+    }
+
+    #traceback {
+        color: red;
+    }
+    </style>
+</head>
+    <body>
+        <h2>%(status)s</h2>
+        <p>%(message)s</p>
+        <pre id="traceback">%(traceback)s</pre>
+    <div id="powered_by">
+      <span>
+        Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
+      </span>
+    </div>
+    </body>
+</html>
+'''
+
+
+def get_error_page(status, **kwargs):
+    """Return an HTML page, containing a pretty error response.
+
+    status should be an int or a str.
+    kwargs will be interpolated into the page template.
+    """
+    try:
+        code, reason, message = _httputil.valid_status(status)
+    except ValueError:
+        raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
+
+    # We can't use setdefault here, because some
+    # callers send None for kwarg values.
+    if kwargs.get('status') is None:
+        kwargs['status'] = '%s %s' % (code, reason)
+    if kwargs.get('message') is None:
+        kwargs['message'] = message
+    if kwargs.get('traceback') is None:
+        kwargs['traceback'] = ''
+    if kwargs.get('version') is None:
+        kwargs['version'] = cherrypy.__version__
+
+    for k, v in six.iteritems(kwargs):
+        if v is None:
+            kwargs[k] = ''
+        else:
+            kwargs[k] = escape_html(kwargs[k])
+
+    # Use a custom template or callable for the error page?
+    pages = cherrypy.serving.request.error_page
+    error_page = pages.get(code) or pages.get('default')
+
+    # Default template, can be overridden below.
+    template = _HTTPErrorTemplate
+    if error_page:
+        try:
+            if hasattr(error_page, '__call__'):
+                # The caller function may be setting headers manually,
+                # so we delegate to it completely. We may be returning
+                # an iterator as well as a string here.
+                #
+                # We *must* make sure any content is not unicode.
+                result = error_page(**kwargs)
+                if cherrypy.lib.is_iterator(result):
+                    from cherrypy.lib.encoding import UTF8StreamEncoder
+                    return UTF8StreamEncoder(result)
+                elif isinstance(result, six.text_type):
+                    return result.encode('utf-8')
+                else:
+                    if not isinstance(result, bytes):
+                        raise ValueError(
+                            'error page function did not '
+                            'return a bytestring, six.text_type or an '
+                            'iterator - returned object of type %s.'
+                            % (type(result).__name__))
+                    return result
+            else:
+                # Load the template from this path.
+                template = io.open(error_page, newline='').read()
+        except Exception:
+            e = _format_exception(*_exc_info())[-1]
+            m = kwargs['message']
+            if m:
+                m += '<br />'
+            m += 'In addition, the custom error page failed:\n<br />%s' % e
+            kwargs['message'] = m
+
+    response = cherrypy.serving.response
+    response.headers['Content-Type'] = 'text/html;charset=utf-8'
+    result = template % kwargs
+    return result.encode('utf-8')
+
+
+_ie_friendly_error_sizes = {
+    400: 512, 403: 256, 404: 512, 405: 256,
+    406: 512, 408: 512, 409: 512, 410: 256,
+    500: 512, 501: 512, 505: 512,
+}
+
+
+def _be_ie_unfriendly(status):
+    response = cherrypy.serving.response
+
+    # For some statuses, Internet Explorer 5+ shows "friendly error
+    # messages" instead of our response.body if the body is smaller
+    # than a given size. Fix this by returning a body over that size
+    # (by adding whitespace).
+    # See http://support.microsoft.com/kb/q218155/
+    s = _ie_friendly_error_sizes.get(status, 0)
+    if s:
+        s += 1
+        # Since we are issuing an HTTP error status, we assume that
+        # the entity is short, and we should just collapse it.
+        content = response.collapse_body()
+        content_length = len(content)
+        if content_length and content_length < s:
+            # IN ADDITION: the response must be written to IE
+            # in one chunk or it will still get replaced! Bah.
+            content = content + (b' ' * (s - content_length))
+        response.body = content
+        response.headers['Content-Length'] = str(len(content))
+
+
+def format_exc(exc=None):
+    """Return exc (or sys.exc_info if None), formatted."""
+    try:
+        if exc is None:
+            exc = _exc_info()
+        if exc == (None, None, None):
+            return ''
+        import traceback
+        return ''.join(traceback.format_exception(*exc))
+    finally:
+        del exc
+
+
+def bare_error(extrabody=None):
+    """Produce status, headers, body for a critical error.
+
+    Returns a triple without calling any other questionable functions,
+    so it should be as error-free as possible. Call it from an HTTP server
+    if you get errors outside of the request.
+
+    If extrabody is None, a friendly but rather unhelpful error message
+    is set in the body. If extrabody is a string, it will be appended
+    as-is to the body.
+    """
+
+    # The whole point of this function is to be a last line-of-defense
+    # in handling errors. That is, it must not raise any errors itself;
+    # it cannot be allowed to fail. Therefore, don't add to it!
+    # In particular, don't call any other CP functions.
+
+    body = b'Unrecoverable error in the server.'
+    if extrabody is not None:
+        if not isinstance(extrabody, bytes):
+            extrabody = extrabody.encode('utf-8')
+        body += b'\n' + extrabody
+
+    return (b'500 Internal Server Error',
+            [(b'Content-Type', b'text/plain'),
+             (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))],
+            [body])
diff --git a/libraries/cherrypy/_cplogging.py b/libraries/cherrypy/_cplogging.py
new file mode 100644
index 00000000..53b9addb
--- /dev/null
+++ b/libraries/cherrypy/_cplogging.py
@@ -0,0 +1,482 @@
+"""
+Simple config
+=============
+
+Although CherryPy uses the :mod:`Python logging module <logging>`, it does so
+behind the scenes so that simple logging is simple, but complicated logging
+is still possible. "Simple" logging means that you can log to the screen
+(i.e. console/stdout) or to a file, and that you can easily have separate
+error and access log files.
+
+Here are the simplified logging settings. You use these by adding lines to
+your config file or dict. You should set these at either the global level or
+per application (see next), but generally not both.
+
+ * ``log.screen``: Set this to True to have both "error" and "access" messages
+   printed to stdout.
+ * ``log.access_file``: Set this to an absolute filename where you want
+   "access" messages written.
+ * ``log.error_file``: Set this to an absolute filename where you want "error"
+   messages written.
+
+Many events are automatically logged; to log your own application events, call
+:func:`cherrypy.log`.
+
+Architecture
+============
+
+Separate scopes
+---------------
+
+CherryPy provides log managers at both the global and application layers.
+This means you can have one set of logging rules for your entire site,
+and another set of rules specific to each application. The global log
+manager is found at :func:`cherrypy.log`, and the log manager for each
+application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
+If you're inside a request, the latter is reachable from
+``cherrypy.request.app.log``; if you're outside a request, you'll have to
+obtain a reference to the ``app``: either the return value of
+:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
+:func:`quickstart()<cherrypy.quickstart>` instead, via
+``cherrypy.tree.apps['/']``.
+
+By default, the global logs are named "cherrypy.error" and "cherrypy.access",
+and the application logs are named "cherrypy.error.2378745" and
+"cherrypy.access.2378745" (the number is the id of the Application object).
+This means that the application logs "bubble up" to the site logs, so if your
+application has no log handlers, the site-level handlers will still log the
+messages.
+
+Errors vs. Access
+-----------------
+
+Each log manager handles both "access" messages (one per HTTP request) and
+"error" messages (everything else). Note that the "error" log is not just for
+errors! The format of access messages is highly formalized, but the error log
+isn't--it receives messages from a variety of sources (including full error
+tracebacks, if enabled).
+
+If you are logging the access log and error log to the same source, then there
+is a possibility that a specially crafted error message may replicate an access
+log message as described in CWE-117.  In this case it is the application
+developer's responsibility to manually escape data before
+using CherryPy's log()
+functionality, or they may create an application that is vulnerable to CWE-117.
+This would be achieved by using a custom handler escape any special characters,
+and attached as described below.
+
+Custom Handlers
+===============
+
+The simple settings above work by manipulating Python's standard :mod:`logging`
+module. So when you need something more complex, the full power of the standard
+module is yours to exploit. You can borrow or create custom handlers, formats,
+filters, and much more. Here's an example that skips the standard FileHandler
+and uses a RotatingFileHandler instead:
+
+::
+
+    #python
+    log = app.log
+
+    # Remove the default FileHandlers if present.
+    log.error_file = ""
+    log.access_file = ""
+
+    maxBytes = getattr(log, "rot_maxBytes", 10000000)
+    backupCount = getattr(log, "rot_backupCount", 1000)
+
+    # Make a new RotatingFileHandler for the error log.
+    fname = getattr(log, "rot_error_file", "error.log")
+    h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
+    h.setLevel(DEBUG)
+    h.setFormatter(_cplogging.logfmt)
+    log.error_log.addHandler(h)
+
+    # Make a new RotatingFileHandler for the access log.
+    fname = getattr(log, "rot_access_file", "access.log")
+    h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
+    h.setLevel(DEBUG)
+    h.setFormatter(_cplogging.logfmt)
+    log.access_log.addHandler(h)
+
+
+The ``rot_*`` attributes are pulled straight from the application log object.
+Since "log.*" config entries simply set attributes on the log object, you can
+add custom attributes to your heart's content. Note that these handlers are
+used ''instead'' of the default, simple handlers outlined above (so don't set
+the "log.error_file" config entry, for example).
+"""
+
+import datetime
+import logging
+import os
+import sys
+
+import six
+
+import cherrypy
+from cherrypy import _cperror
+
+
+# Silence the no-handlers "warning" (stderr write!) in stdlib logging
+logging.Logger.manager.emittedNoHandlerWarning = 1
+logfmt = logging.Formatter('%(message)s')
+
+
+class NullHandler(logging.Handler):
+
+    """A no-op logging handler to silence the logging.lastResort handler."""
+
+    def handle(self, record):
+        pass
+
+    def emit(self, record):
+        pass
+
+    def createLock(self):
+        self.lock = None
+
+
+class LogManager(object):
+
+    """An object to assist both simple and advanced logging.
+
+    ``cherrypy.log`` is an instance of this class.
+    """
+
+    appid = None
+    """The id() of the Application object which owns this log manager. If this
+    is a global log manager, appid is None."""
+
+    error_log = None
+    """The actual :class:`logging.Logger` instance for error messages."""
+
+    access_log = None
+    """The actual :class:`logging.Logger` instance for access messages."""
+
+    access_log_format = (
+        '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
+        if six.PY3 else
+        '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
+    )
+
+    logger_root = None
+    """The "top-level" logger name.
+
+    This string will be used as the first segment in the Logger names.
+    The default is "cherrypy", for example, in which case the Logger names
+    will be of the form::
+
+        cherrypy.error.<appid>
+        cherrypy.access.<appid>
+    """
+
+    def __init__(self, appid=None, logger_root='cherrypy'):
+        self.logger_root = logger_root
+        self.appid = appid
+        if appid is None:
+            self.error_log = logging.getLogger('%s.error' % logger_root)
+            self.access_log = logging.getLogger('%s.access' % logger_root)
+        else:
+            self.error_log = logging.getLogger(
+                '%s.error.%s' % (logger_root, appid))
+            self.access_log = logging.getLogger(
+                '%s.access.%s' % (logger_root, appid))
+        self.error_log.setLevel(logging.INFO)
+        self.access_log.setLevel(logging.INFO)
+
+        # Silence the no-handlers "warning" (stderr write!) in stdlib logging
+        self.error_log.addHandler(NullHandler())
+        self.access_log.addHandler(NullHandler())
+
+        cherrypy.engine.subscribe('graceful', self.reopen_files)
+
+    def reopen_files(self):
+        """Close and reopen all file handlers."""
+        for log in (self.error_log, self.access_log):
+            for h in log.handlers:
+                if isinstance(h, logging.FileHandler):
+                    h.acquire()
+                    h.stream.close()
+                    h.stream = open(h.baseFilename, h.mode)
+                    h.release()
+
+    def error(self, msg='', context='', severity=logging.INFO,
+              traceback=False):
+        """Write the given ``msg`` to the error log.
+
+        This is not just for errors! Applications may call this at any time
+        to log application-specific information.
+
+        If ``traceback`` is True, the traceback of the current exception
+        (if any) will be appended to ``msg``.
+        """
+        exc_info = None
+        if traceback:
+            exc_info = _cperror._exc_info()
+
+        self.error_log.log(
+            severity,
+            ' '.join((self.time(), context, msg)),
+            exc_info=exc_info,
+        )
+
+    def __call__(self, *args, **kwargs):
+        """An alias for ``error``."""
+        return self.error(*args, **kwargs)
+
+    def access(self):
+        """Write to the access log (in Apache/NCSA Combined Log format).
+
+        See the
+        `apache documentation
+        <http://httpd.apache.org/docs/current/logs.html#combined>`_
+        for format details.
+
+        CherryPy calls this automatically for you. Note there are no arguments;
+        it collects the data itself from
+        :class:`cherrypy.request<cherrypy._cprequest.Request>`.
+
+        Like Apache started doing in 2.0.46, non-printable and other special
+        characters in %r (and we expand that to all parts) are escaped using
+        \\xhh sequences, where hh stands for the hexadecimal representation
+        of the raw byte. Exceptions from this rule are " and \\, which are
+        escaped by prepending a backslash, and all whitespace characters,
+        which are written in their C-style notation (\\n, \\t, etc).
+        """
+        request = cherrypy.serving.request
+        remote = request.remote
+        response = cherrypy.serving.response
+        outheaders = response.headers
+        inheaders = request.headers
+        if response.output_status is None:
+            status = '-'
+        else:
+            status = response.output_status.split(b' ', 1)[0]
+            if six.PY3:
+                status = status.decode('ISO-8859-1')
+
+        atoms = {'h': remote.name or remote.ip,
+                 'l': '-',
+                 'u': getattr(request, 'login', None) or '-',
+                 't': self.time(),
+                 'r': request.request_line,
+                 's': status,
+                 'b': dict.get(outheaders, 'Content-Length', '') or '-',
+                 'f': dict.get(inheaders, 'Referer', ''),
+                 'a': dict.get(inheaders, 'User-Agent', ''),
+                 'o': dict.get(inheaders, 'Host', '-'),
+                 'i': request.unique_id,
+                 'z': LazyRfc3339UtcTime(),
+                 }
+        if six.PY3:
+            for k, v in atoms.items():
+                if not isinstance(v, str):
+                    v = str(v)
+                v = v.replace('"', '\\"').encode('utf8')
+                # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
+                # and backslash for us. All we have to do is strip the quotes.
+                v = repr(v)[2:-1]
+
+                # in python 3.0 the repr of bytes (as returned by encode)
+                # uses double \'s.  But then the logger escapes them yet, again
+                # resulting in quadruple slashes.  Remove the extra one here.
+                v = v.replace('\\\\', '\\')
+
+                # Escape double-quote.
+                atoms[k] = v
+
+            try:
+                self.access_log.log(
+                    logging.INFO, self.access_log_format.format(**atoms))
+            except Exception:
+                self(traceback=True)
+        else:
+            for k, v in atoms.items():
+                if isinstance(v, six.text_type):
+                    v = v.encode('utf8')
+                elif not isinstance(v, str):
+                    v = str(v)
+                # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
+                # and backslash for us. All we have to do is strip the quotes.
+                v = repr(v)[1:-1]
+                # Escape double-quote.
+                atoms[k] = v.replace('"', '\\"')
+
+            try:
+                self.access_log.log(
+                    logging.INFO, self.access_log_format % atoms)
+            except Exception:
+                self(traceback=True)
+
+    def time(self):
+        """Return now() in Apache Common Log Format (no timezone)."""
+        now = datetime.datetime.now()
+        monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun',
+                      'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
+        month = monthnames[now.month - 1].capitalize()
+        return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
+                (now.day, month, now.year, now.hour, now.minute, now.second))
+
+    def _get_builtin_handler(self, log, key):
+        for h in log.handlers:
+            if getattr(h, '_cpbuiltin', None) == key:
+                return h
+
+    # ------------------------- Screen handlers ------------------------- #
+    def _set_screen_handler(self, log, enable, stream=None):
+        h = self._get_builtin_handler(log, 'screen')
+        if enable:
+            if not h:
+                if stream is None:
+                    stream = sys.stderr
+                h = logging.StreamHandler(stream)
+                h.setFormatter(logfmt)
+                h._cpbuiltin = 'screen'
+                log.addHandler(h)
+        elif h:
+            log.handlers.remove(h)
+
+    @property
+    def screen(self):
+        """Turn stderr/stdout logging on or off.
+
+        If you set this to True, it'll add the appropriate StreamHandler for
+        you. If you set it to False, it will remove the handler.
+        """
+        h = self._get_builtin_handler
+        has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
+        return bool(has_h)
+
+    @screen.setter
+    def screen(self, newvalue):
+        self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
+        self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
+
+    # -------------------------- File handlers -------------------------- #
+
+    def _add_builtin_file_handler(self, log, fname):
+        h = logging.FileHandler(fname)
+        h.setFormatter(logfmt)
+        h._cpbuiltin = 'file'
+        log.addHandler(h)
+
+    def _set_file_handler(self, log, filename):
+        h = self._get_builtin_handler(log, 'file')
+        if filename:
+            if h:
+                if h.baseFilename != os.path.abspath(filename):
+                    h.close()
+                    log.handlers.remove(h)
+                    self._add_builtin_file_handler(log, filename)
+            else:
+                self._add_builtin_file_handler(log, filename)
+        else:
+            if h:
+                h.close()
+                log.handlers.remove(h)
+
+    @property
+    def error_file(self):
+        """The filename for self.error_log.
+
+        If you set this to a string, it'll add the appropriate FileHandler for
+        you. If you set it to ``None`` or ``''``, it will remove the handler.
+        """
+        h = self._get_builtin_handler(self.error_log, 'file')
+        if h:
+            return h.baseFilename
+        return ''
+
+    @error_file.setter
+    def error_file(self, newvalue):
+        self._set_file_handler(self.error_log, newvalue)
+
+    @property
+    def access_file(self):
+        """The filename for self.access_log.
+
+        If you set this to a string, it'll add the appropriate FileHandler for
+        you. If you set it to ``None`` or ``''``, it will remove the handler.
+        """
+        h = self._get_builtin_handler(self.access_log, 'file')
+        if h:
+            return h.baseFilename
+        return ''
+
+    @access_file.setter
+    def access_file(self, newvalue):
+        self._set_file_handler(self.access_log, newvalue)
+
+    # ------------------------- WSGI handlers ------------------------- #
+
+    def _set_wsgi_handler(self, log, enable):
+        h = self._get_builtin_handler(log, 'wsgi')
+        if enable:
+            if not h:
+                h = WSGIErrorHandler()
+                h.setFormatter(logfmt)
+                h._cpbuiltin = 'wsgi'
+                log.addHandler(h)
+        elif h:
+            log.handlers.remove(h)
+
+    @property
+    def wsgi(self):
+        """Write errors to wsgi.errors.
+
+        If you set this to True, it'll add the appropriate
+        :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
+        (which writes errors to ``wsgi.errors``).
+        If you set it to False, it will remove the handler.
+        """
+        return bool(self._get_builtin_handler(self.error_log, 'wsgi'))
+
+    @wsgi.setter
+    def wsgi(self, newvalue):
+        self._set_wsgi_handler(self.error_log, newvalue)
+
+
+class WSGIErrorHandler(logging.Handler):
+
+    "A handler class which writes logging records to environ['wsgi.errors']."
+
+    def flush(self):
+        """Flushes the stream."""
+        try:
+            stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
+        except (AttributeError, KeyError):
+            pass
+        else:
+            stream.flush()
+
+    def emit(self, record):
+        """Emit a record."""
+        try:
+            stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
+        except (AttributeError, KeyError):
+            pass
+        else:
+            try:
+                msg = self.format(record)
+                fs = '%s\n'
+                import types
+                # if no unicode support...
+                if not hasattr(types, 'UnicodeType'):
+                    stream.write(fs % msg)
+                else:
+                    try:
+                        stream.write(fs % msg)
+                    except UnicodeError:
+                        stream.write(fs % msg.encode('UTF-8'))
+                self.flush()
+            except Exception:
+                self.handleError(record)
+
+
+class LazyRfc3339UtcTime(object):
+    def __str__(self):
+        """Return now() in RFC3339 UTC Format."""
+        now = datetime.datetime.now()
+        return now.isoformat('T') + 'Z'
diff --git a/libraries/cherrypy/_cpmodpy.py b/libraries/cherrypy/_cpmodpy.py
new file mode 100644
index 00000000..ac91e625
--- /dev/null
+++ b/libraries/cherrypy/_cpmodpy.py
@@ -0,0 +1,356 @@
+"""Native adapter for serving CherryPy via mod_python
+
+Basic usage:
+
+##########################################
+# Application in a module called myapp.py
+##########################################
+
+import cherrypy
+
+class Root:
+    @cherrypy.expose
+    def index(self):
+        return 'Hi there, Ho there, Hey there'
+
+
+# We will use this method from the mod_python configuration
+# as the entry point to our application
+def setup_server():
+    cherrypy.tree.mount(Root())
+    cherrypy.config.update({'environment': 'production',
+                            'log.screen': False,
+                            'show_tracebacks': False})
+
+##########################################
+# mod_python settings for apache2
+# This should reside in your httpd.conf
+# or a file that will be loaded at
+# apache startup
+##########################################
+
+# Start
+DocumentRoot "/"
+Listen 8080
+LoadModule python_module /usr/lib/apache2/modules/mod_python.so
+
+<Location "/">
+    PythonPath "sys.path+['/path/to/my/application']"
+    SetHandler python-program
+    PythonHandler cherrypy._cpmodpy::handler
+    PythonOption cherrypy.setup myapp::setup_server
+    PythonDebug On
+</Location>
+# End
+
+The actual path to your mod_python.so is dependent on your
+environment. In this case we suppose a global mod_python
+installation on a Linux distribution such as Ubuntu.
+
+We do set the PythonPath configuration setting so that
+your application can be found by from the user running
+the apache2 instance. Of course if your application
+resides in the global site-package this won't be needed.
+
+Then restart apache2 and access http://127.0.0.1:8080
+"""
+
+import io
+import logging
+import os
+import re
+import sys
+
+import six
+
+from more_itertools import always_iterable
+
+import cherrypy
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil
+
+
+# ------------------------------ Request-handling
+
+
+def setup(req):
+    from mod_python import apache
+
+    # Run any setup functions defined by a "PythonOption cherrypy.setup"
+    # directive.
+    options = req.get_options()
+    if 'cherrypy.setup' in options:
+        for function in options['cherrypy.setup'].split():
+            atoms = function.split('::', 1)
+            if len(atoms) == 1:
+                mod = __import__(atoms[0], globals(), locals())
+            else:
+                modname, fname = atoms
+                mod = __import__(modname, globals(), locals(), [fname])
+                func = getattr(mod, fname)
+                func()
+
+    cherrypy.config.update({'log.screen': False,
+                            'tools.ignore_headers.on': True,
+                            'tools.ignore_headers.headers': ['Range'],
+                            })
+
+    engine = cherrypy.engine
+    if hasattr(engine, 'signal_handler'):
+        engine.signal_handler.unsubscribe()
+    if hasattr(engine, 'console_control_handler'):
+        engine.console_control_handler.unsubscribe()
+    engine.autoreload.unsubscribe()
+    cherrypy.server.unsubscribe()
+
+    @engine.subscribe('log')
+    def _log(msg, level):
+        newlevel = apache.APLOG_ERR
+        if logging.DEBUG >= level:
+            newlevel = apache.APLOG_DEBUG
+        elif logging.INFO >= level:
+            newlevel = apache.APLOG_INFO
+        elif logging.WARNING >= level:
+            newlevel = apache.APLOG_WARNING
+        # On Windows, req.server is required or the msg will vanish. See
+        # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
+        # Also, "When server is not specified...LogLevel does not apply..."
+        apache.log_error(msg, newlevel, req.server)
+
+    engine.start()
+
+    def cherrypy_cleanup(data):
+        engine.exit()
+    try:
+        # apache.register_cleanup wasn't available until 3.1.4.
+        apache.register_cleanup(cherrypy_cleanup)
+    except AttributeError:
+        req.server.register_cleanup(req, cherrypy_cleanup)
+
+
+class _ReadOnlyRequest:
+    expose = ('read', 'readline', 'readlines')
+
+    def __init__(self, req):
+        for method in self.expose:
+            self.__dict__[method] = getattr(req, method)
+
+
+recursive = False
+
+_isSetUp = False
+
+
+def handler(req):
+    from mod_python import apache
+    try:
+        global _isSetUp
+        if not _isSetUp:
+            setup(req)
+            _isSetUp = True
+
+        # Obtain a Request object from CherryPy
+        local = req.connection.local_addr
+        local = httputil.Host(
+            local[0], local[1], req.connection.local_host or '')
+        remote = req.connection.remote_addr
+        remote = httputil.Host(
+            remote[0], remote[1], req.connection.remote_host or '')
+
+        scheme = req.parsed_uri[0] or 'http'
+        req.get_basic_auth_pw()
+
+        try:
+            # apache.mpm_query only became available in mod_python 3.1
+            q = apache.mpm_query
+            threaded = q(apache.AP_MPMQ_IS_THREADED)
+            forked = q(apache.AP_MPMQ_IS_FORKED)
+        except AttributeError:
+            bad_value = ("You must provide a PythonOption '%s', "
+                         "either 'on' or 'off', when running a version "
+                         'of mod_python < 3.1')
+
+            options = req.get_options()
+
+            threaded = options.get('multithread', '').lower()
+            if threaded == 'on':
+                threaded = True
+            elif threaded == 'off':
+                threaded = False
+            else:
+                raise ValueError(bad_value % 'multithread')
+
+            forked = options.get('multiprocess', '').lower()
+            if forked == 'on':
+                forked = True
+            elif forked == 'off':
+                forked = False
+            else:
+                raise ValueError(bad_value % 'multiprocess')
+
+        sn = cherrypy.tree.script_name(req.uri or '/')
+        if sn is None:
+            send_response(req, '404 Not Found', [], '')
+        else:
+            app = cherrypy.tree.apps[sn]
+            method = req.method
+            path = req.uri
+            qs = req.args or ''
+            reqproto = req.protocol
+            headers = list(six.iteritems(req.headers_in))
+            rfile = _ReadOnlyRequest(req)
+            prev = None
+
+            try:
+                redirections = []
+                while True:
+                    request, response = app.get_serving(local, remote, scheme,
+                                                        'HTTP/1.1')
+                    request.login = req.user
+                    request.multithread = bool(threaded)
+                    request.multiprocess = bool(forked)
+                    request.app = app
+                    request.prev = prev
+
+                    # Run the CherryPy Request object and obtain the response
+                    try:
+                        request.run(method, path, qs, reqproto, headers, rfile)
+                        break
+                    except cherrypy.InternalRedirect:
+                        ir = sys.exc_info()[1]
+                        app.release_serving()
+                        prev = request
+
+                        if not recursive:
+                            if ir.path in redirections:
+                                raise RuntimeError(
+                                    'InternalRedirector visited the same URL '
+                                    'twice: %r' % ir.path)
+                            else:
+                                # Add the *previous* path_info + qs to
+                                # redirections.
+                                if qs:
+                                    qs = '?' + qs
+                                redirections.append(sn + path + qs)
+
+                        # Munge environment and try again.
+                        method = 'GET'
+                        path = ir.path
+                        qs = ir.query_string
+                        rfile = io.BytesIO()
+
+                send_response(
+                    req, response.output_status, response.header_list,
+                    response.body, response.stream)
+            finally:
+                app.release_serving()
+    except Exception:
+        tb = format_exc()
+        cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
+        s, h, b = bare_error()
+        send_response(req, s, h, b)
+    return apache.OK
+
+
+def send_response(req, status, headers, body, stream=False):
+    # Set response status
+    req.status = int(status[:3])
+
+    # Set response headers
+    req.content_type = 'text/plain'
+    for header, value in headers:
+        if header.lower() == 'content-type':
+            req.content_type = value
+            continue
+        req.headers_out.add(header, value)
+
+    if stream:
+        # Flush now so the status and headers are sent immediately.
+        req.flush()
+
+    # Set response body
+    for seg in always_iterable(body):
+        req.write(seg)
+
+
+# --------------- Startup tools for CherryPy + mod_python --------------- #
+try:
+    import subprocess
+
+    def popen(fullcmd):
+        p = subprocess.Popen(fullcmd, shell=True,
+                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
+                             close_fds=True)
+        return p.stdout
+except ImportError:
+    def popen(fullcmd):
+        pipein, pipeout = os.popen4(fullcmd)
+        return pipeout
+
+
+def read_process(cmd, args=''):
+    fullcmd = '%s %s' % (cmd, args)
+    pipeout = popen(fullcmd)
+    try:
+        firstline = pipeout.readline()
+        cmd_not_found = re.search(
+            b'(not recognized|No such file|not found)',
+            firstline,
+            re.IGNORECASE
+        )
+        if cmd_not_found:
+            raise IOError('%s must be on your system path.' % cmd)
+        output = firstline + pipeout.read()
+    finally:
+        pipeout.close()
+    return output
+
+
+class ModPythonServer(object):
+
+    template = """
+# Apache2 server configuration file for running CherryPy with mod_python.
+
+DocumentRoot "/"
+Listen %(port)s
+LoadModule python_module modules/mod_python.so
+
+<Location %(loc)s>
+    SetHandler python-program
+    PythonHandler %(handler)s
+    PythonDebug On
+%(opts)s
+</Location>
+"""
+
+    def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
+                 handler='cherrypy._cpmodpy::handler'):
+        self.loc = loc
+        self.port = port
+        self.opts = opts
+        self.apache_path = apache_path
+        self.handler = handler
+
+    def start(self):
+        opts = ''.join(['    PythonOption %s %s\n' % (k, v)
+                        for k, v in self.opts])
+        conf_data = self.template % {'port': self.port,
+                                     'loc': self.loc,
+                                     'opts': opts,
+                                     'handler': self.handler,
+                                     }
+
+        mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
+        f = open(mpconf, 'wb')
+        try:
+            f.write(conf_data)
+        finally:
+            f.close()
+
+        response = read_process(self.apache_path, '-k start -f %s' % mpconf)
+        self.ready = True
+        return response
+
+    def stop(self):
+        os.popen('apache -k stop')
+        self.ready = False
diff --git a/libraries/cherrypy/_cpnative_server.py b/libraries/cherrypy/_cpnative_server.py
new file mode 100644
index 00000000..55653c35
--- /dev/null
+++ b/libraries/cherrypy/_cpnative_server.py
@@ -0,0 +1,160 @@
+"""Native adapter for serving CherryPy via its builtin server."""
+
+import logging
+import sys
+import io
+
+import cheroot.server
+
+import cherrypy
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil
+
+
+class NativeGateway(cheroot.server.Gateway):
+    """Native gateway implementation allowing to bypass WSGI."""
+
+    recursive = False
+
+    def respond(self):
+        """Obtain response from CherryPy machinery and then send it."""
+        req = self.req
+        try:
+            # Obtain a Request object from CherryPy
+            local = req.server.bind_addr
+            local = httputil.Host(local[0], local[1], '')
+            remote = req.conn.remote_addr, req.conn.remote_port
+            remote = httputil.Host(remote[0], remote[1], '')
+
+            scheme = req.scheme
+            sn = cherrypy.tree.script_name(req.uri or '/')
+            if sn is None:
+                self.send_response('404 Not Found', [], [''])
+            else:
+                app = cherrypy.tree.apps[sn]
+                method = req.method
+                path = req.path
+                qs = req.qs or ''
+                headers = req.inheaders.items()
+                rfile = req.rfile
+                prev = None
+
+                try:
+                    redirections = []
+                    while True:
+                        request, response = app.get_serving(
+                            local, remote, scheme, 'HTTP/1.1')
+                        request.multithread = True
+                        request.multiprocess = False
+                        request.app = app
+                        request.prev = prev
+
+                        # Run the CherryPy Request object and obtain the
+                        # response
+                        try:
+                            request.run(method, path, qs,
+                                        req.request_protocol, headers, rfile)
+                            break
+                        except cherrypy.InternalRedirect:
+                            ir = sys.exc_info()[1]
+                            app.release_serving()
+                            prev = request
+
+                            if not self.recursive:
+                                if ir.path in redirections:
+                                    raise RuntimeError(
+                                        'InternalRedirector visited the same '
+                                        'URL twice: %r' % ir.path)
+                                else:
+                                    # Add the *previous* path_info + qs to
+                                    # redirections.
+                                    if qs:
+                                        qs = '?' + qs
+                                    redirections.append(sn + path + qs)
+
+                            # Munge environment and try again.
+                            method = 'GET'
+                            path = ir.path
+                            qs = ir.query_string
+                            rfile = io.BytesIO()
+
+                    self.send_response(
+                        response.output_status, response.header_list,
+                        response.body)
+                finally:
+                    app.release_serving()
+        except Exception:
+            tb = format_exc()
+            # print tb
+            cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
+            s, h, b = bare_error()
+            self.send_response(s, h, b)
+
+    def send_response(self, status, headers, body):
+        """Send response to HTTP request."""
+        req = self.req
+
+        # Set response status
+        req.status = status or b'500 Server Error'
+
+        # Set response headers
+        for header, value in headers:
+            req.outheaders.append((header, value))
+        if (req.ready and not req.sent_headers):
+            req.sent_headers = True
+            req.send_headers()
+
+        # Set response body
+        for seg in body:
+            req.write(seg)
+
+
+class CPHTTPServer(cheroot.server.HTTPServer):
+    """Wrapper for cheroot.server.HTTPServer.
+
+    cheroot has been designed to not reference CherryPy in any way,
+    so that it can be used in other frameworks and applications.
+    Therefore, we wrap it here, so we can apply some attributes
+    from config -> cherrypy.server -> HTTPServer.
+    """
+
+    def __init__(self, server_adapter=cherrypy.server):
+        """Initialize CPHTTPServer."""
+        self.server_adapter = server_adapter
+
+        server_name = (self.server_adapter.socket_host or
+                       self.server_adapter.socket_file or
+                       None)
+
+        cheroot.server.HTTPServer.__init__(
+            self, server_adapter.bind_addr, NativeGateway,
+            minthreads=server_adapter.thread_pool,
+            maxthreads=server_adapter.thread_pool_max,
+            server_name=server_name)
+
+        self.max_request_header_size = (
+            self.server_adapter.max_request_header_size or 0)
+        self.max_request_body_size = (
+            self.server_adapter.max_request_body_size or 0)
+        self.request_queue_size = self.server_adapter.socket_queue_size
+        self.timeout = self.server_adapter.socket_timeout
+        self.shutdown_timeout = self.server_adapter.shutdown_timeout
+        self.protocol = self.server_adapter.protocol_version
+        self.nodelay = self.server_adapter.nodelay
+
+        ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
+        if self.server_adapter.ssl_context:
+            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain,
+                self.server_adapter.ssl_ciphers)
+            self.ssl_adapter.context = self.server_adapter.ssl_context
+        elif self.server_adapter.ssl_certificate:
+            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain,
+                self.server_adapter.ssl_ciphers)
diff --git a/libraries/cherrypy/_cpreqbody.py b/libraries/cherrypy/_cpreqbody.py
new file mode 100644
index 00000000..893fe5f5
--- /dev/null
+++ b/libraries/cherrypy/_cpreqbody.py
@@ -0,0 +1,1000 @@
+"""Request body processing for CherryPy.
+
+.. versionadded:: 3.2
+
+Application authors have complete control over the parsing of HTTP request
+entities. In short,
+:attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
+is now always set to an instance of
+:class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
+and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`.
+
+When an HTTP request includes an entity body, it is often desirable to
+provide that information to applications in a form other than the raw bytes.
+Different content types demand different approaches. Examples:
+
+ * For a GIF file, we want the raw bytes in a stream.
+ * An HTML form is better parsed into its component fields, and each text field
+   decoded from bytes to unicode.
+ * A JSON body should be deserialized into a Python dict or list.
+
+When the request contains a Content-Type header, the media type is used as a
+key to look up a value in the
+:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict.
+If the full media
+type is not found, then the major type is tried; for example, if no processor
+is found for the 'image/jpeg' type, then we look for a processor for the
+'image' types altogether. If neither the full type nor the major type has a
+matching processor, then a default processor is used
+(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most
+types, this means no processing is done, and the body is left unread as a
+raw byte stream. Processors are configurable in an 'on_start_resource' hook.
+
+Some processors, especially those for the 'text' types, attempt to decode bytes
+to unicode. If the Content-Type request header includes a 'charset' parameter,
+this is used to decode the entity. Otherwise, one or more default charsets may
+be attempted, although this decision is up to each processor. If a processor
+successfully decodes an Entity or Part, it should set the
+:attr:`charset<cherrypy._cpreqbody.Entity.charset>` attribute
+on the Entity or Part to the name of the successful charset, so that
+applications can easily re-encode or transcode the value if they wish.
+
+If the Content-Type of the request entity is of major type 'multipart', then
+the above parsing process, and possibly a decoding process, is performed for
+each part.
+
+For both the full entity and multipart parts, a Content-Disposition header may
+be used to fill :attr:`name<cherrypy._cpreqbody.Entity.name>` and
+:attr:`filename<cherrypy._cpreqbody.Entity.filename>` attributes on the
+request.body or the Part.
+
+.. _custombodyprocessors:
+
+Custom Processors
+=================
+
+You can add your own processors for any specific or major MIME type. Simply add
+it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
+hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
+Here's the built-in JSON tool for an example::
+
+    def json_in(force=True, debug=False):
+        request = cherrypy.serving.request
+        def json_processor(entity):
+            '''Read application/json data into request.json.'''
+            if not entity.headers.get("Content-Length", ""):
+                raise cherrypy.HTTPError(411)
+
+            body = entity.fp.read()
+            try:
+                request.json = json_decode(body)
+            except ValueError:
+                raise cherrypy.HTTPError(400, 'Invalid JSON document')
+        if force:
+            request.body.processors.clear()
+            request.body.default_proc = cherrypy.HTTPError(
+                415, 'Expected an application/json content type')
+        request.body.processors['application/json'] = json_processor
+
+We begin by defining a new ``json_processor`` function to stick in the
+``processors`` dictionary. All processor functions take a single argument,
+the ``Entity`` instance they are to process. It will be called whenever a
+request is received (for those URI's where the tool is turned on) which
+has a ``Content-Type`` of "application/json".
+
+First, it checks for a valid ``Content-Length`` (raising 411 if not valid),
+then reads the remaining bytes on the socket. The ``fp`` object knows its
+own length, so it won't hang waiting for data that never arrives. It will
+return when all data has been read. Then, we decode those bytes using
+Python's built-in ``json`` module, and stick the decoded result onto
+``request.json`` . If it cannot be decoded, we raise 400.
+
+If the "force" argument is True (the default), the ``Tool`` clears the
+``processors`` dict so that request entities of other ``Content-Types``
+aren't parsed at all. Since there's no entry for those invalid MIME
+types, the ``default_proc`` method of ``cherrypy.request.body`` is
+called. But this does nothing by default (usually to provide the page
+handler an opportunity to handle it.)
+But in our case, we want to raise 415, so we replace
+``request.body.default_proc``
+with the error (``HTTPError`` instances, when called, raise themselves).
+
+If we were defining a custom processor, we can do so without making a ``Tool``.
+Just add the config entry::
+
+    request.body.processors = {'application/json': json_processor}
+
+Note that you can only replace the ``processors`` dict wholesale this way,
+not update the existing one.
+"""
+
+try:
+    from io import DEFAULT_BUFFER_SIZE
+except ImportError:
+    DEFAULT_BUFFER_SIZE = 8192
+import re
+import sys
+import tempfile
+try:
+    from urllib import unquote_plus
+except ImportError:
+    def unquote_plus(bs):
+        """Bytes version of urllib.parse.unquote_plus."""
+        bs = bs.replace(b'+', b' ')
+        atoms = bs.split(b'%')
+        for i in range(1, len(atoms)):
+            item = atoms[i]
+            try:
+                pct = int(item[:2], 16)
+                atoms[i] = bytes([pct]) + item[2:]
+            except ValueError:
+                pass
+        return b''.join(atoms)
+
+import six
+import cheroot.server
+
+import cherrypy
+from cherrypy._cpcompat import ntou, unquote
+from cherrypy.lib import httputil
+
+
+# ------------------------------- Processors -------------------------------- #
+
+def process_urlencoded(entity):
+    """Read application/x-www-form-urlencoded data into entity.params."""
+    qs = entity.fp.read()
+    for charset in entity.attempt_charsets:
+        try:
+            params = {}
+            for aparam in qs.split(b'&'):
+                for pair in aparam.split(b';'):
+                    if not pair:
+                        continue
+
+                    atoms = pair.split(b'=', 1)
+                    if len(atoms) == 1:
+                        atoms.append(b'')
+
+                    key = unquote_plus(atoms[0]).decode(charset)
+                    value = unquote_plus(atoms[1]).decode(charset)
+
+                    if key in params:
+                        if not isinstance(params[key], list):
+                            params[key] = [params[key]]
+                        params[key].append(value)
+                    else:
+                        params[key] = value
+        except UnicodeDecodeError:
+            pass
+        else:
+            entity.charset = charset
+            break
+    else:
+        raise cherrypy.HTTPError(
+            400, 'The request entity could not be decoded. The following '
+            'charsets were attempted: %s' % repr(entity.attempt_charsets))
+
+    # Now that all values have been successfully parsed and decoded,
+    # apply them to the entity.params dict.
+    for key, value in params.items():
+        if key in entity.params:
+            if not isinstance(entity.params[key], list):
+                entity.params[key] = [entity.params[key]]
+            entity.params[key].append(value)
+        else:
+            entity.params[key] = value
+
+
+def process_multipart(entity):
+    """Read all multipart parts into entity.parts."""
+    ib = ''
+    if 'boundary' in entity.content_type.params:
+        # http://tools.ietf.org/html/rfc2046#section-5.1.1
+        # "The grammar for parameters on the Content-type field is such that it
+        # is often necessary to enclose the boundary parameter values in quotes
+        # on the Content-type line"
+        ib = entity.content_type.params['boundary'].strip('"')
+
+    if not re.match('^[ -~]{0,200}[!-~]$', ib):
+        raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
+
+    ib = ('--' + ib).encode('ascii')
+
+    # Find the first marker
+    while True:
+        b = entity.readline()
+        if not b:
+            return
+
+        b = b.strip()
+        if b == ib:
+            break
+
+    # Read all parts
+    while True:
+        part = entity.part_class.from_fp(entity.fp, ib)
+        entity.parts.append(part)
+        part.process()
+        if part.fp.done:
+            break
+
+
+def process_multipart_form_data(entity):
+    """Read all multipart/form-data parts into entity.parts or entity.params.
+    """
+    process_multipart(entity)
+
+    kept_parts = []
+    for part in entity.parts:
+        if part.name is None:
+            kept_parts.append(part)
+        else:
+            if part.filename is None:
+                # It's a regular field
+                value = part.fullvalue()
+            else:
+                # It's a file upload. Retain the whole part so consumer code
+                # has access to its .file and .filename attributes.
+                value = part
+
+            if part.name in entity.params:
+                if not isinstance(entity.params[part.name], list):
+                    entity.params[part.name] = [entity.params[part.name]]
+                entity.params[part.name].append(value)
+            else:
+                entity.params[part.name] = value
+
+    entity.parts = kept_parts
+
+
+def _old_process_multipart(entity):
+    """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
+    process_multipart(entity)
+
+    params = entity.params
+
+    for part in entity.parts:
+        if part.name is None:
+            key = ntou('parts')
+        else:
+            key = part.name
+
+        if part.filename is None:
+            # It's a regular field
+            value = part.fullvalue()
+        else:
+            # It's a file upload. Retain the whole part so consumer code
+            # has access to its .file and .filename attributes.
+            value = part
+
+        if key in params:
+            if not isinstance(params[key], list):
+                params[key] = [params[key]]
+            params[key].append(value)
+        else:
+            params[key] = value
+
+
+# -------------------------------- Entities --------------------------------- #
+class Entity(object):
+
+    """An HTTP request body, or MIME multipart body.
+
+    This class collects information about the HTTP request entity. When a
+    given entity is of MIME type "multipart", each part is parsed into its own
+    Entity instance, and the set of parts stored in
+    :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
+
+    Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
+    tries to process the request body (if any) by calling
+    :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`.
+    This uses the ``content_type`` of the Entity to look up a suitable
+    processor in
+    :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`,
+    a dict.
+    If a matching processor cannot be found for the complete Content-Type,
+    it tries again using the major type. For example, if a request with an
+    entity of type "image/jpeg" arrives, but no processor can be found for
+    that complete type, then one is sought for the major type "image". If a
+    processor is still not found, then the
+    :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method
+    of the Entity is called (which does nothing by default; you can
+    override this too).
+
+    CherryPy includes processors for the "application/x-www-form-urlencoded"
+    type, the "multipart/form-data" type, and the "multipart" major type.
+    CherryPy 3.2 processes these types almost exactly as older versions.
+    Parts are passed as arguments to the page handler using their
+    ``Content-Disposition.name`` if given, otherwise in a generic "parts"
+    argument. Each such part is either a string, or the
+    :class:`Part<cherrypy._cpreqbody.Part>` itself if it's a file. (In this
+    case it will have ``file`` and ``filename`` attributes, or possibly a
+    ``value`` attribute). Each Part is itself a subclass of
+    Entity, and has its own ``process`` method and ``processors`` dict.
+
+    There is a separate processor for the "multipart" major type which is more
+    flexible, and simply stores all multipart parts in
+    :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
+    enable it with::
+
+        cherrypy.request.body.processors['multipart'] = \
+            _cpreqbody.process_multipart
+
+    in an ``on_start_resource`` tool.
+    """
+
+    # http://tools.ietf.org/html/rfc2046#section-4.1.2:
+    # "The default character set, which must be assumed in the
+    # absence of a charset parameter, is US-ASCII."
+    # However, many browsers send data in utf-8 with no charset.
+    attempt_charsets = ['utf-8']
+    r"""A list of strings, each of which should be a known encoding.
+
+    When the Content-Type of the request body warrants it, each of the given
+    encodings will be tried in order. The first one to successfully decode the
+    entity without raising an error is stored as
+    :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
+    to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
+    `HTTP/1.1
+    <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
+    but ``['us-ascii', 'utf-8']`` for multipart parts.
+    """
+
+    charset = None
+    """The successful decoding; see "attempt_charsets" above."""
+
+    content_type = None
+    """The value of the Content-Type request header.
+
+    If the Entity is part of a multipart payload, this will be the Content-Type
+    given in the MIME headers for this part.
+    """
+
+    default_content_type = 'application/x-www-form-urlencoded'
+    """This defines a default ``Content-Type`` to use if no Content-Type header
+    is given. The empty string is used for RequestBody, which results in the
+    request body not being read or parsed at all. This is by design; a missing
+    ``Content-Type`` header in the HTTP request entity is an error at best,
+    and a security hole at worst. For multipart parts, however, the MIME spec
+    declares that a part with no Content-Type defaults to "text/plain"
+    (see :class:`Part<cherrypy._cpreqbody.Part>`).
+    """
+
+    filename = None
+    """The ``Content-Disposition.filename`` header, if available."""
+
+    fp = None
+    """The readable socket file object."""
+
+    headers = None
+    """A dict of request/multipart header names and values.
+
+    This is a copy of the ``request.headers`` for the ``request.body``;
+    for multipart parts, it is the set of headers for that part.
+    """
+
+    length = None
+    """The value of the ``Content-Length`` header, if provided."""
+
+    name = None
+    """The "name" parameter of the ``Content-Disposition`` header, if any."""
+
+    params = None
+    """
+    If the request Content-Type is 'application/x-www-form-urlencoded' or
+    multipart, this will be a dict of the params pulled from the entity
+    body; that is, it will be the portion of request.params that come
+    from the message body (sometimes called "POST params", although they
+    can be sent with various HTTP method verbs). This value is set between
+    the 'before_request_body' and 'before_handler' hooks (assuming that
+    process_request_body is True)."""
+
+    processors = {'application/x-www-form-urlencoded': process_urlencoded,
+                  'multipart/form-data': process_multipart_form_data,
+                  'multipart': process_multipart,
+                  }
+    """A dict of Content-Type names to processor methods."""
+
+    parts = None
+    """A list of Part instances if ``Content-Type`` is of major type
+    "multipart"."""
+
+    part_class = None
+    """The class used for multipart parts.
+
+    You can replace this with custom subclasses to alter the processing of
+    multipart parts.
+    """
+
+    def __init__(self, fp, headers, params=None, parts=None):
+        # Make an instance-specific copy of the class processors
+        # so Tools, etc. can replace them per-request.
+        self.processors = self.processors.copy()
+
+        self.fp = fp
+        self.headers = headers
+
+        if params is None:
+            params = {}
+        self.params = params
+
+        if parts is None:
+            parts = []
+        self.parts = parts
+
+        # Content-Type
+        self.content_type = headers.elements('Content-Type')
+        if self.content_type:
+            self.content_type = self.content_type[0]
+        else:
+            self.content_type = httputil.HeaderElement.from_str(
+                self.default_content_type)
+
+        # Copy the class 'attempt_charsets', prepending any Content-Type
+        # charset
+        dec = self.content_type.params.get('charset', None)
+        if dec:
+            self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
+                                             if c != dec]
+        else:
+            self.attempt_charsets = self.attempt_charsets[:]
+
+        # Length
+        self.length = None
+        clen = headers.get('Content-Length', None)
+        # If Transfer-Encoding is 'chunked', ignore any Content-Length.
+        if (
+            clen is not None and
+            'chunked' not in headers.get('Transfer-Encoding', '')
+        ):
+            try:
+                self.length = int(clen)
+            except ValueError:
+                pass
+
+        # Content-Disposition
+        self.name = None
+        self.filename = None
+        disp = headers.elements('Content-Disposition')
+        if disp:
+            disp = disp[0]
+            if 'name' in disp.params:
+                self.name = disp.params['name']
+                if self.name.startswith('"') and self.name.endswith('"'):
+                    self.name = self.name[1:-1]
+            if 'filename' in disp.params:
+                self.filename = disp.params['filename']
+                if (
+                    self.filename.startswith('"') and
+                    self.filename.endswith('"')
+                ):
+                    self.filename = self.filename[1:-1]
+            if 'filename*' in disp.params:
+                # @see https://tools.ietf.org/html/rfc5987
+                encoding, lang, filename = disp.params['filename*'].split("'")
+                self.filename = unquote(str(filename), encoding)
+
+    def read(self, size=None, fp_out=None):
+        return self.fp.read(size, fp_out)
+
+    def readline(self, size=None):
+        return self.fp.readline(size)
+
+    def readlines(self, sizehint=None):
+        return self.fp.readlines(sizehint)
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        line = self.readline()
+        if not line:
+            raise StopIteration
+        return line
+
+    def next(self):
+        return self.__next__()
+
+    def read_into_file(self, fp_out=None):
+        """Read the request body into fp_out (or make_file() if None).
+
+        Return fp_out.
+        """
+        if fp_out is None:
+            fp_out = self.make_file()
+        self.read(fp_out=fp_out)
+        return fp_out
+
+    def make_file(self):
+        """Return a file-like object into which the request body will be read.
+
+        By default, this will return a TemporaryFile. Override as needed.
+        See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
+        return tempfile.TemporaryFile()
+
+    def fullvalue(self):
+        """Return this entity as a string, whether stored in a file or not."""
+        if self.file:
+            # It was stored in a tempfile. Read it.
+            self.file.seek(0)
+            value = self.file.read()
+            self.file.seek(0)
+        else:
+            value = self.value
+        value = self.decode_entity(value)
+        return value
+
+    def decode_entity(self, value):
+        """Return a given byte encoded value as a string"""
+        for charset in self.attempt_charsets:
+            try:
+                value = value.decode(charset)
+            except UnicodeDecodeError:
+                pass
+            else:
+                self.charset = charset
+                return value
+        else:
+            raise cherrypy.HTTPError(
+                400,
+                'The request entity could not be decoded. The following '
+                'charsets were attempted: %s' % repr(self.attempt_charsets)
+            )
+
+    def process(self):
+        """Execute the best-match processor for the given media type."""
+        proc = None
+        ct = self.content_type.value
+        try:
+            proc = self.processors[ct]
+        except KeyError:
+            toptype = ct.split('/', 1)[0]
+            try:
+                proc = self.processors[toptype]
+            except KeyError:
+                pass
+        if proc is None:
+            self.default_proc()
+        else:
+            proc(self)
+
+    def default_proc(self):
+        """Called if a more-specific processor is not found for the
+        ``Content-Type``.
+        """
+        # Leave the fp alone for someone else to read. This works fine
+        # for request.body, but the Part subclasses need to override this
+        # so they can move on to the next part.
+        pass
+
+
+class Part(Entity):
+
+    """A MIME part entity, part of a multipart entity."""
+
+    # "The default character set, which must be assumed in the absence of a
+    # charset parameter, is US-ASCII."
+    attempt_charsets = ['us-ascii', 'utf-8']
+    r"""A list of strings, each of which should be a known encoding.
+
+    When the Content-Type of the request body warrants it, each of the given
+    encodings will be tried in order. The first one to successfully decode the
+    entity without raising an error is stored as
+    :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
+    to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
+    `HTTP/1.1
+    <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
+    but ``['us-ascii', 'utf-8']`` for multipart parts.
+    """
+
+    boundary = None
+    """The MIME multipart boundary."""
+
+    default_content_type = 'text/plain'
+    """This defines a default ``Content-Type`` to use if no Content-Type header
+    is given. The empty string is used for RequestBody, which results in the
+    request body not being read or parsed at all. This is by design; a missing
+    ``Content-Type`` header in the HTTP request entity is an error at best,
+    and a security hole at worst. For multipart parts, however (this class),
+    the MIME spec declares that a part with no Content-Type defaults to
+    "text/plain".
+    """
+
+    # This is the default in stdlib cgi. We may want to increase it.
+    maxrambytes = 1000
+    """The threshold of bytes after which point the ``Part`` will store
+    its data in a file (generated by
+    :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
+    instead of a string. Defaults to 1000, just like the :mod:`cgi`
+    module in Python's standard library.
+    """
+
+    def __init__(self, fp, headers, boundary):
+        Entity.__init__(self, fp, headers)
+        self.boundary = boundary
+        self.file = None
+        self.value = None
+
+    @classmethod
+    def from_fp(cls, fp, boundary):
+        headers = cls.read_headers(fp)
+        return cls(fp, headers, boundary)
+
+    @classmethod
+    def read_headers(cls, fp):
+        headers = httputil.HeaderMap()
+        while True:
+            line = fp.readline()
+            if not line:
+                # No more data--illegal end of headers
+                raise EOFError('Illegal end of headers.')
+
+            if line == b'\r\n':
+                # Normal end of headers
+                break
+            if not line.endswith(b'\r\n'):
+                raise ValueError('MIME requires CRLF terminators: %r' % line)
+
+            if line[0] in b' \t':
+                # It's a continuation line.
+                v = line.strip().decode('ISO-8859-1')
+            else:
+                k, v = line.split(b':', 1)
+                k = k.strip().decode('ISO-8859-1')
+                v = v.strip().decode('ISO-8859-1')
+
+            existing = headers.get(k)
+            if existing:
+                v = ', '.join((existing, v))
+            headers[k] = v
+
+        return headers
+
+    def read_lines_to_boundary(self, fp_out=None):
+        """Read bytes from self.fp and return or write them to a file.
+
+        If the 'fp_out' argument is None (the default), all bytes read are
+        returned in a single byte string.
+
+        If the 'fp_out' argument is not None, it must be a file-like
+        object that supports the 'write' method; all bytes read will be
+        written to the fp, and that fp is returned.
+        """
+        endmarker = self.boundary + b'--'
+        delim = b''
+        prev_lf = True
+        lines = []
+        seen = 0
+        while True:
+            line = self.fp.readline(1 << 16)
+            if not line:
+                raise EOFError('Illegal end of multipart body.')
+            if line.startswith(b'--') and prev_lf:
+                strippedline = line.strip()
+                if strippedline == self.boundary:
+                    break
+                if strippedline == endmarker:
+                    self.fp.finish()
+                    break
+
+            line = delim + line
+
+            if line.endswith(b'\r\n'):
+                delim = b'\r\n'
+                line = line[:-2]
+                prev_lf = True
+            elif line.endswith(b'\n'):
+                delim = b'\n'
+                line = line[:-1]
+                prev_lf = True
+            else:
+                delim = b''
+                prev_lf = False
+
+            if fp_out is None:
+                lines.append(line)
+                seen += len(line)
+                if seen > self.maxrambytes:
+                    fp_out = self.make_file()
+                    for line in lines:
+                        fp_out.write(line)
+            else:
+                fp_out.write(line)
+
+        if fp_out is None:
+            result = b''.join(lines)
+            return result
+        else:
+            fp_out.seek(0)
+            return fp_out
+
+    def default_proc(self):
+        """Called if a more-specific processor is not found for the
+        ``Content-Type``.
+        """
+        if self.filename:
+            # Always read into a file if a .filename was given.
+            self.file = self.read_into_file()
+        else:
+            result = self.read_lines_to_boundary()
+            if isinstance(result, bytes):
+                self.value = result
+            else:
+                self.file = result
+
+    def read_into_file(self, fp_out=None):
+        """Read the request body into fp_out (or make_file() if None).
+
+        Return fp_out.
+        """
+        if fp_out is None:
+            fp_out = self.make_file()
+        self.read_lines_to_boundary(fp_out=fp_out)
+        return fp_out
+
+
+Entity.part_class = Part
+
+inf = float('inf')
+
+
+class SizedReader:
+
+    def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
+                 has_trailers=False):
+        # Wrap our fp in a buffer so peek() works
+        self.fp = fp
+        self.length = length
+        self.maxbytes = maxbytes
+        self.buffer = b''
+        self.bufsize = bufsize
+        self.bytes_read = 0
+        self.done = False
+        self.has_trailers = has_trailers
+
+    def read(self, size=None, fp_out=None):
+        """Read bytes from the request body and return or write them to a file.
+
+        A number of bytes less than or equal to the 'size' argument are read
+        off the socket. The actual number of bytes read are tracked in
+        self.bytes_read. The number may be smaller than 'size' when 1) the
+        client sends fewer bytes, 2) the 'Content-Length' request header
+        specifies fewer bytes than requested, or 3) the number of bytes read
+        exceeds self.maxbytes (in which case, 413 is raised).
+
+        If the 'fp_out' argument is None (the default), all bytes read are
+        returned in a single byte string.
+
+        If the 'fp_out' argument is not None, it must be a file-like
+        object that supports the 'write' method; all bytes read will be
+        written to the fp, and None is returned.
+        """
+
+        if self.length is None:
+            if size is None:
+                remaining = inf
+            else:
+                remaining = size
+        else:
+            remaining = self.length - self.bytes_read
+            if size and size < remaining:
+                remaining = size
+        if remaining == 0:
+            self.finish()
+            if fp_out is None:
+                return b''
+            else:
+                return None
+
+        chunks = []
+
+        # Read bytes from the buffer.
+        if self.buffer:
+            if remaining is inf:
+                data = self.buffer
+                self.buffer = b''
+            else:
+                data = self.buffer[:remaining]
+                self.buffer = self.buffer[remaining:]
+            datalen = len(data)
+            remaining -= datalen
+
+            # Check lengths.
+            self.bytes_read += datalen
+            if self.maxbytes and self.bytes_read > self.maxbytes:
+                raise cherrypy.HTTPError(413)
+
+            # Store the data.
+            if fp_out is None:
+                chunks.append(data)
+            else:
+                fp_out.write(data)
+
+        # Read bytes from the socket.
+        while remaining > 0:
+            chunksize = min(remaining, self.bufsize)
+            try:
+                data = self.fp.read(chunksize)
+            except Exception:
+                e = sys.exc_info()[1]
+                if e.__class__.__name__ == 'MaxSizeExceeded':
+                    # Post data is too big
+                    raise cherrypy.HTTPError(
+                        413, 'Maximum request length: %r' % e.args[1])
+                else:
+                    raise
+            if not data:
+                self.finish()
+                break
+            datalen = len(data)
+            remaining -= datalen
+
+            # Check lengths.
+            self.bytes_read += datalen
+            if self.maxbytes and self.bytes_read > self.maxbytes:
+                raise cherrypy.HTTPError(413)
+
+            # Store the data.
+            if fp_out is None:
+                chunks.append(data)
+            else:
+                fp_out.write(data)
+
+        if fp_out is None:
+            return b''.join(chunks)
+
+    def readline(self, size=None):
+        """Read a line from the request body and return it."""
+        chunks = []
+        while size is None or size > 0:
+            chunksize = self.bufsize
+            if size is not None and size < self.bufsize:
+                chunksize = size
+            data = self.read(chunksize)
+            if not data:
+                break
+            pos = data.find(b'\n') + 1
+            if pos:
+                chunks.append(data[:pos])
+                remainder = data[pos:]
+                self.buffer += remainder
+                self.bytes_read -= len(remainder)
+                break
+            else:
+                chunks.append(data)
+        return b''.join(chunks)
+
+    def readlines(self, sizehint=None):
+        """Read lines from the request body and return them."""
+        if self.length is not None:
+            if sizehint is None:
+                sizehint = self.length - self.bytes_read
+            else:
+                sizehint = min(sizehint, self.length - self.bytes_read)
+
+        lines = []
+        seen = 0
+        while True:
+            line = self.readline()
+            if not line:
+                break
+            lines.append(line)
+            seen += len(line)
+            if seen >= sizehint:
+                break
+        return lines
+
+    def finish(self):
+        self.done = True
+        if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
+            self.trailers = {}
+
+            try:
+                for line in self.fp.read_trailer_lines():
+                    if line[0] in b' \t':
+                        # It's a continuation line.
+                        v = line.strip()
+                    else:
+                        try:
+                            k, v = line.split(b':', 1)
+                        except ValueError:
+                            raise ValueError('Illegal header line.')
+                        k = k.strip().title()
+                        v = v.strip()
+
+                    if k in cheroot.server.comma_separated_headers:
+                        existing = self.trailers.get(k)
+                        if existing:
+                            v = b', '.join((existing, v))
+                    self.trailers[k] = v
+            except Exception:
+                e = sys.exc_info()[1]
+                if e.__class__.__name__ == 'MaxSizeExceeded':
+                    # Post data is too big
+                    raise cherrypy.HTTPError(
+                        413, 'Maximum request length: %r' % e.args[1])
+                else:
+                    raise
+
+
+class RequestBody(Entity):
+
+    """The entity of the HTTP request."""
+
+    bufsize = 8 * 1024
+    """The buffer size used when reading the socket."""
+
+    # Don't parse the request body at all if the client didn't provide
+    # a Content-Type header. See
+    # https://github.com/cherrypy/cherrypy/issues/790
+    default_content_type = ''
+    """This defines a default ``Content-Type`` to use if no Content-Type header
+    is given. The empty string is used for RequestBody, which results in the
+    request body not being read or parsed at all. This is by design; a missing
+    ``Content-Type`` header in the HTTP request entity is an error at best,
+    and a security hole at worst. For multipart parts, however, the MIME spec
+    declares that a part with no Content-Type defaults to "text/plain"
+    (see :class:`Part<cherrypy._cpreqbody.Part>`).
+    """
+
+    maxbytes = None
+    """Raise ``MaxSizeExceeded`` if more bytes than this are read from
+    the socket.
+    """
+
+    def __init__(self, fp, headers, params=None, request_params=None):
+        Entity.__init__(self, fp, headers, params)
+
+        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
+        # When no explicit charset parameter is provided by the
+        # sender, media subtypes of the "text" type are defined
+        # to have a default charset value of "ISO-8859-1" when
+        # received via HTTP.
+        if self.content_type.value.startswith('text/'):
+            for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'):
+                if c in self.attempt_charsets:
+                    break
+            else:
+                self.attempt_charsets.append('ISO-8859-1')
+
+        # Temporary fix while deprecating passing .parts as .params.
+        self.processors['multipart'] = _old_process_multipart
+
+        if request_params is None:
+            request_params = {}
+        self.request_params = request_params
+
+    def process(self):
+        """Process the request entity based on its Content-Type."""
+        # "The presence of a message-body in a request is signaled by the
+        # inclusion of a Content-Length or Transfer-Encoding header field in
+        # the request's message-headers."
+        # It is possible to send a POST request with no body, for example;
+        # however, app developers are responsible in that case to set
+        # cherrypy.request.process_body to False so this method isn't called.
+        h = cherrypy.serving.request.headers
+        if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
+            raise cherrypy.HTTPError(411)
+
+        self.fp = SizedReader(self.fp, self.length,
+                              self.maxbytes, bufsize=self.bufsize,
+                              has_trailers='Trailer' in h)
+        super(RequestBody, self).process()
+
+        # Body params should also be a part of the request_params
+        # add them in here.
+        request_params = self.request_params
+        for key, value in self.params.items():
+            # Python 2 only: keyword arguments must be byte strings (type
+            # 'str').
+            if sys.version_info < (3, 0):
+                if isinstance(key, six.text_type):
+                    key = key.encode('ISO-8859-1')
+
+            if key in request_params:
+                if not isinstance(request_params[key], list):
+                    request_params[key] = [request_params[key]]
+                request_params[key].append(value)
+            else:
+                request_params[key] = value
diff --git a/libraries/cherrypy/_cprequest.py b/libraries/cherrypy/_cprequest.py
new file mode 100644
index 00000000..3cc0c811
--- /dev/null
+++ b/libraries/cherrypy/_cprequest.py
@@ -0,0 +1,930 @@
+import sys
+import time
+
+import uuid
+
+import six
+from six.moves.http_cookies import SimpleCookie, CookieError
+
+from more_itertools import consume
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy import _cpreqbody
+from cherrypy._cperror import format_exc, bare_error
+from cherrypy.lib import httputil, reprconf, encoding
+
+
+class Hook(object):
+
+    """A callback and its metadata: failsafe, priority, and kwargs."""
+
+    callback = None
+    """
+    The bare callable that this Hook object is wrapping, which will
+    be called when the Hook is called."""
+
+    failsafe = False
+    """
+    If True, the callback is guaranteed to run even if other callbacks
+    from the same call point raise exceptions."""
+
+    priority = 50
+    """
+    Defines the order of execution for a list of Hooks. Priority numbers
+    should be limited to the closed interval [0, 100], but values outside
+    this range are acceptable, as are fractional values."""
+
+    kwargs = {}
+    """
+    A set of keyword arguments that will be passed to the
+    callable on each call."""
+
+    def __init__(self, callback, failsafe=None, priority=None, **kwargs):
+        self.callback = callback
+
+        if failsafe is None:
+            failsafe = getattr(callback, 'failsafe', False)
+        self.failsafe = failsafe
+
+        if priority is None:
+            priority = getattr(callback, 'priority', 50)
+        self.priority = priority
+
+        self.kwargs = kwargs
+
+    def __lt__(self, other):
+        """
+        Hooks sort by priority, ascending, such that
+        hooks of lower priority are run first.
+        """
+        return self.priority < other.priority
+
+    def __call__(self):
+        """Run self.callback(**self.kwargs)."""
+        return self.callback(**self.kwargs)
+
+    def __repr__(self):
+        cls = self.__class__
+        return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
+                % (cls.__module__, cls.__name__, self.callback,
+                   self.failsafe, self.priority,
+                   ', '.join(['%s=%r' % (k, v)
+                              for k, v in self.kwargs.items()])))
+
+
+class HookMap(dict):
+
+    """A map of call points to lists of callbacks (Hook objects)."""
+
+    def __new__(cls, points=None):
+        d = dict.__new__(cls)
+        for p in points or []:
+            d[p] = []
+        return d
+
+    def __init__(self, *a, **kw):
+        pass
+
+    def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
+        """Append a new Hook made from the supplied arguments."""
+        self[point].append(Hook(callback, failsafe, priority, **kwargs))
+
+    def run(self, point):
+        """Execute all registered Hooks (callbacks) for the given point."""
+        exc = None
+        hooks = self[point]
+        hooks.sort()
+        for hook in hooks:
+            # Some hooks are guaranteed to run even if others at
+            # the same hookpoint fail. We will still log the failure,
+            # but proceed on to the next hook. The only way
+            # to stop all processing from one of these hooks is
+            # to raise SystemExit and stop the whole server.
+            if exc is None or hook.failsafe:
+                try:
+                    hook()
+                except (KeyboardInterrupt, SystemExit):
+                    raise
+                except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
+                        cherrypy.InternalRedirect):
+                    exc = sys.exc_info()[1]
+                except Exception:
+                    exc = sys.exc_info()[1]
+                    cherrypy.log(traceback=True, severity=40)
+        if exc:
+            raise exc
+
+    def __copy__(self):
+        newmap = self.__class__()
+        # We can't just use 'update' because we want copies of the
+        # mutable values (each is a list) as well.
+        for k, v in self.items():
+            newmap[k] = v[:]
+        return newmap
+    copy = __copy__
+
+    def __repr__(self):
+        cls = self.__class__
+        return '%s.%s(points=%r)' % (
+            cls.__module__,
+            cls.__name__,
+            list(self)
+        )
+
+
+# Config namespace handlers
+
+def hooks_namespace(k, v):
+    """Attach bare hooks declared in config."""
+    # Use split again to allow multiple hooks for a single
+    # hookpoint per path (e.g. "hooks.before_handler.1").
+    # Little-known fact you only get from reading source ;)
+    hookpoint = k.split('.', 1)[0]
+    if isinstance(v, six.string_types):
+        v = cherrypy.lib.reprconf.attributes(v)
+    if not isinstance(v, Hook):
+        v = Hook(v)
+    cherrypy.serving.request.hooks[hookpoint].append(v)
+
+
+def request_namespace(k, v):
+    """Attach request attributes declared in config."""
+    # Provides config entries to set request.body attrs (like
+    # attempt_charsets).
+    if k[:5] == 'body.':
+        setattr(cherrypy.serving.request.body, k[5:], v)
+    else:
+        setattr(cherrypy.serving.request, k, v)
+
+
+def response_namespace(k, v):
+    """Attach response attributes declared in config."""
+    # Provides config entries to set default response headers
+    # http://cherrypy.org/ticket/889
+    if k[:8] == 'headers.':
+        cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
+    else:
+        setattr(cherrypy.serving.response, k, v)
+
+
+def error_page_namespace(k, v):
+    """Attach error pages declared in config."""
+    if k != 'default':
+        k = int(k)
+    cherrypy.serving.request.error_page[k] = v
+
+
+hookpoints = ['on_start_resource', 'before_request_body',
+              'before_handler', 'before_finalize',
+              'on_end_resource', 'on_end_request',
+              'before_error_response', 'after_error_response']
+
+
+class Request(object):
+
+    """An HTTP request.
+
+    This object represents the metadata of an HTTP request message;
+    that is, it contains attributes which describe the environment
+    in which the request URL, headers, and body were sent (if you
+    want tools to interpret the headers and body, those are elsewhere,
+    mostly in Tools). This 'metadata' consists of socket data,
+    transport characteristics, and the Request-Line. This object
+    also contains data regarding the configuration in effect for
+    the given URL, and the execution plan for generating a response.
+    """
+
+    prev = None
+    """
+    The previous Request object (if any). This should be None
+    unless we are processing an InternalRedirect."""
+
+    # Conversation/connection attributes
+    local = httputil.Host('127.0.0.1', 80)
+    'An httputil.Host(ip, port, hostname) object for the server socket.'
+
+    remote = httputil.Host('127.0.0.1', 1111)
+    'An httputil.Host(ip, port, hostname) object for the client socket.'
+
+    scheme = 'http'
+    """
+    The protocol used between client and server. In most cases,
+    this will be either 'http' or 'https'."""
+
+    server_protocol = 'HTTP/1.1'
+    """
+    The HTTP version for which the HTTP server is at least
+    conditionally compliant."""
+
+    base = ''
+    """The (scheme://host) portion of the requested URL.
+    In some cases (e.g. when proxying via mod_rewrite), this may contain
+    path segments which cherrypy.url uses when constructing url's, but
+    which otherwise are ignored by CherryPy. Regardless, this value
+    MUST NOT end in a slash."""
+
+    # Request-Line attributes
+    request_line = ''
+    """
+    The complete Request-Line received from the client. This is a
+    single string consisting of the request method, URI, and protocol
+    version (joined by spaces). Any final CRLF is removed."""
+
+    method = 'GET'
+    """
+    Indicates the HTTP method to be performed on the resource identified
+    by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
+    DELETE. CherryPy allows any extension method; however, various HTTP
+    servers and gateways may restrict the set of allowable methods.
+    CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
+
+    query_string = ''
+    """
+    The query component of the Request-URI, a string of information to be
+    interpreted by the resource. The query portion of a URI follows the
+    path component, and is separated by a '?'. For example, the URI
+    'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
+    'a=3&b=4'."""
+
+    query_string_encoding = 'utf8'
+    """
+    The encoding expected for query string arguments after % HEX HEX decoding).
+    If a query string is provided that cannot be decoded with this encoding,
+    404 is raised (since technically it's a different URI). If you want
+    arbitrary encodings to not error, set this to 'Latin-1'; you can then
+    encode back to bytes and re-decode to whatever encoding you like later.
+    """
+
+    protocol = (1, 1)
+    """The HTTP protocol version corresponding to the set
+    of features which should be allowed in the response. If BOTH
+    the client's request message AND the server's level of HTTP
+    compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
+    If either is 1.0, this attribute will be the tuple (1, 0).
+    Lower HTTP protocol versions are not explicitly supported."""
+
+    params = {}
+    """
+    A dict which combines query string (GET) and request entity (POST)
+    variables. This is populated in two stages: GET params are added
+    before the 'on_start_resource' hook, and POST params are added
+    between the 'before_request_body' and 'before_handler' hooks."""
+
+    # Message attributes
+    header_list = []
+    """
+    A list of the HTTP request headers as (name, value) tuples.
+    In general, you should use request.headers (a dict) instead."""
+
+    headers = httputil.HeaderMap()
+    """
+    A dict-like object containing the request headers. Keys are header
+    names (in Title-Case format); however, you may get and set them in
+    a case-insensitive manner. That is, headers['Content-Type'] and
+    headers['content-type'] refer to the same value. Values are header
+    values (decoded according to :rfc:`2047` if necessary). See also:
+    httputil.HeaderMap, httputil.HeaderElement."""
+
+    cookie = SimpleCookie()
+    """See help(Cookie)."""
+
+    rfile = None
+    """
+    If the request included an entity (body), it will be available
+    as a stream in this attribute. However, the rfile will normally
+    be read for you between the 'before_request_body' hook and the
+    'before_handler' hook, and the resulting string is placed into
+    either request.params or the request.body attribute.
+
+    You may disable the automatic consumption of the rfile by setting
+    request.process_request_body to False, either in config for the desired
+    path, or in an 'on_start_resource' or 'before_request_body' hook.
+
+    WARNING: In almost every case, you should not attempt to read from the
+    rfile stream after CherryPy's automatic mechanism has read it. If you
+    turn off the automatic parsing of rfile, you should read exactly the
+    number of bytes specified in request.headers['Content-Length'].
+    Ignoring either of these warnings may result in a hung request thread
+    or in corruption of the next (pipelined) request.
+    """
+
+    process_request_body = True
+    """
+    If True, the rfile (if any) is automatically read and parsed,
+    and the result placed into request.params or request.body."""
+
+    methods_with_bodies = ('POST', 'PUT', 'PATCH')
+    """
+    A sequence of HTTP methods for which CherryPy will automatically
+    attempt to read a body from the rfile. If you are going to change
+    this property, modify it on the configuration (recommended)
+    or on the "hook point" `on_start_resource`.
+    """
+
+    body = None
+    """
+    If the request Content-Type is 'application/x-www-form-urlencoded'
+    or multipart, this will be None. Otherwise, this will be an instance
+    of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
+    can .read()); this value is set between the 'before_request_body' and
+    'before_handler' hooks (assuming that process_request_body is True)."""
+
+    # Dispatch attributes
+    dispatch = cherrypy.dispatch.Dispatcher()
+    """
+    The object which looks up the 'page handler' callable and collects
+    config for the current request based on the path_info, other
+    request attributes, and the application architecture. The core
+    calls the dispatcher as early as possible, passing it a 'path_info'
+    argument.
+
+    The default dispatcher discovers the page handler by matching path_info
+    to a hierarchical arrangement of objects, starting at request.app.root.
+    See help(cherrypy.dispatch) for more information."""
+
+    script_name = ''
+    """
+    The 'mount point' of the application which is handling this request.
+
+    This attribute MUST NOT end in a slash. If the script_name refers to
+    the root of the URI, it MUST be an empty string (not "/").
+    """
+
+    path_info = '/'
+    """
+    The 'relative path' portion of the Request-URI. This is relative
+    to the script_name ('mount point') of the application which is
+    handling this request."""
+
+    login = None
+    """
+    When authentication is used during the request processing this is
+    set to 'False' if it failed and to the 'username' value if it succeeded.
+    The default 'None' implies that no authentication happened."""
+
+    # Note that cherrypy.url uses "if request.app:" to determine whether
+    # the call is during a real HTTP request or not. So leave this None.
+    app = None
+    """The cherrypy.Application object which is handling this request."""
+
+    handler = None
+    """
+    The function, method, or other callable which CherryPy will call to
+    produce the response. The discovery of the handler and the arguments
+    it will receive are determined by the request.dispatch object.
+    By default, the handler is discovered by walking a tree of objects
+    starting at request.app.root, and is then passed all HTTP params
+    (from the query string and POST body) as keyword arguments."""
+
+    toolmaps = {}
+    """
+    A nested dict of all Toolboxes and Tools in effect for this request,
+    of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
+
+    config = None
+    """
+    A flat dict of all configuration entries which apply to the
+    current request. These entries are collected from global config,
+    application config (based on request.path_info), and from handler
+    config (exactly how is governed by the request.dispatch object in
+    effect for this request; by default, handler config can be attached
+    anywhere in the tree between request.app.root and the final handler,
+    and inherits downward)."""
+
+    is_index = None
+    """
+    This will be True if the current request is mapped to an 'index'
+    resource handler (also, a 'default' handler if path_info ends with
+    a slash). The value may be used to automatically redirect the
+    user-agent to a 'more canonical' URL which either adds or removes
+    the trailing slash. See cherrypy.tools.trailing_slash."""
+
+    hooks = HookMap(hookpoints)
+    """
+    A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
+    Each key is a str naming the hook point, and each value is a list
+    of hooks which will be called at that hook point during this request.
+    The list of hooks is generally populated as early as possible (mostly
+    from Tools specified in config), but may be extended at any time.
+    See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
+
+    error_response = cherrypy.HTTPError(500).set_response
+    """
+    The no-arg callable which will handle unexpected, untrapped errors
+    during request processing. This is not used for expected exceptions
+    (like NotFound, HTTPError, or HTTPRedirect) which are raised in
+    response to expected conditions (those should be customized either
+    via request.error_page or by overriding HTTPError.set_response).
+    By default, error_response uses HTTPError(500) to return a generic
+    error response to the user-agent."""
+
+    error_page = {}
+    """
+    A dict of {error code: response filename or callable} pairs.
+
+    The error code must be an int representing a given HTTP error code,
+    or the string 'default', which will be used if no matching entry
+    is found for a given numeric code.
+
+    If a filename is provided, the file should contain a Python string-
+    formatting template, and can expect by default to receive format
+    values with the mapping keys %(status)s, %(message)s, %(traceback)s,
+    and %(version)s. The set of format mappings can be extended by
+    overriding HTTPError.set_response.
+
+    If a callable is provided, it will be called by default with keyword
+    arguments 'status', 'message', 'traceback', and 'version', as for a
+    string-formatting template. The callable must return a string or
+    iterable of strings which will be set to response.body. It may also
+    override headers or perform any other processing.
+
+    If no entry is given for an error code, and no 'default' entry exists,
+    a default template will be used.
+    """
+
+    show_tracebacks = True
+    """
+    If True, unexpected errors encountered during request processing will
+    include a traceback in the response body."""
+
+    show_mismatched_params = True
+    """
+    If True, mismatched parameters encountered during PageHandler invocation
+    processing will be included in the response body."""
+
+    throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
+    """The sequence of exceptions which Request.run does not trap."""
+
+    throw_errors = False
+    """
+    If True, Request.run will not trap any errors (except HTTPRedirect and
+    HTTPError, which are more properly called 'exceptions', not errors)."""
+
+    closed = False
+    """True once the close method has been called, False otherwise."""
+
+    stage = None
+    """
+    A string containing the stage reached in the request-handling process.
+    This is useful when debugging a live server with hung requests."""
+
+    unique_id = None
+    """A lazy object generating and memorizing UUID4 on ``str()`` render."""
+
+    namespaces = reprconf.NamespaceSet(
+        **{'hooks': hooks_namespace,
+           'request': request_namespace,
+           'response': response_namespace,
+           'error_page': error_page_namespace,
+           'tools': cherrypy.tools,
+           })
+
+    def __init__(self, local_host, remote_host, scheme='http',
+                 server_protocol='HTTP/1.1'):
+        """Populate a new Request object.
+
+        local_host should be an httputil.Host object with the server info.
+        remote_host should be an httputil.Host object with the client info.
+        scheme should be a string, either "http" or "https".
+        """
+        self.local = local_host
+        self.remote = remote_host
+        self.scheme = scheme
+        self.server_protocol = server_protocol
+
+        self.closed = False
+
+        # Put a *copy* of the class error_page into self.
+        self.error_page = self.error_page.copy()
+
+        # Put a *copy* of the class namespaces into self.
+        self.namespaces = self.namespaces.copy()
+
+        self.stage = None
+
+        self.unique_id = LazyUUID4()
+
+    def close(self):
+        """Run cleanup code. (Core)"""
+        if not self.closed:
+            self.closed = True
+            self.stage = 'on_end_request'
+            self.hooks.run('on_end_request')
+            self.stage = 'close'
+
+    def run(self, method, path, query_string, req_protocol, headers, rfile):
+        r"""Process the Request. (Core)
+
+        method, path, query_string, and req_protocol should be pulled directly
+        from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
+
+        path
+            This should be %XX-unquoted, but query_string should not be.
+
+            When using Python 2, they both MUST be byte strings,
+            not unicode strings.
+
+            When using Python 3, they both MUST be unicode strings,
+            not byte strings, and preferably not bytes \x00-\xFF
+            disguised as unicode.
+
+        headers
+            A list of (name, value) tuples.
+
+        rfile
+            A file-like object containing the HTTP request entity.
+
+        When run() is done, the returned object should have 3 attributes:
+
+          * status, e.g. "200 OK"
+          * header_list, a list of (name, value) tuples
+          * body, an iterable yielding strings
+
+        Consumer code (HTTP servers) should then access these response
+        attributes to build the outbound stream.
+
+        """
+        response = cherrypy.serving.response
+        self.stage = 'run'
+        try:
+            self.error_response = cherrypy.HTTPError(500).set_response
+
+            self.method = method
+            path = path or '/'
+            self.query_string = query_string or ''
+            self.params = {}
+
+            # Compare request and server HTTP protocol versions, in case our
+            # server does not support the requested protocol. Limit our output
+            # to min(req, server). We want the following output:
+            #     request    server     actual written   supported response
+            #     protocol   protocol  response protocol    feature set
+            # a     1.0        1.0           1.0                1.0
+            # b     1.0        1.1           1.1                1.0
+            # c     1.1        1.0           1.0                1.0
+            # d     1.1        1.1           1.1                1.1
+            # Notice that, in (b), the response will be "HTTP/1.1" even though
+            # the client only understands 1.0. RFC 2616 10.5.6 says we should
+            # only return 505 if the _major_ version is different.
+            rp = int(req_protocol[5]), int(req_protocol[7])
+            sp = int(self.server_protocol[5]), int(self.server_protocol[7])
+            self.protocol = min(rp, sp)
+            response.headers.protocol = self.protocol
+
+            # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
+            url = path
+            if query_string:
+                url += '?' + query_string
+            self.request_line = '%s %s %s' % (method, url, req_protocol)
+
+            self.header_list = list(headers)
+            self.headers = httputil.HeaderMap()
+
+            self.rfile = rfile
+            self.body = None
+
+            self.cookie = SimpleCookie()
+            self.handler = None
+
+            # path_info should be the path from the
+            # app root (script_name) to the handler.
+            self.script_name = self.app.script_name
+            self.path_info = pi = path[len(self.script_name):]
+
+            self.stage = 'respond'
+            self.respond(pi)
+
+        except self.throws:
+            raise
+        except Exception:
+            if self.throw_errors:
+                raise
+            else:
+                # Failure in setup, error handler or finalize. Bypass them.
+                # Can't use handle_error because we may not have hooks yet.
+                cherrypy.log(traceback=True, severity=40)
+                if self.show_tracebacks:
+                    body = format_exc()
+                else:
+                    body = ''
+                r = bare_error(body)
+                response.output_status, response.header_list, response.body = r
+
+        if self.method == 'HEAD':
+            # HEAD requests MUST NOT return a message-body in the response.
+            response.body = []
+
+        try:
+            cherrypy.log.access()
+        except Exception:
+            cherrypy.log.error(traceback=True)
+
+        return response
+
+    def respond(self, path_info):
+        """Generate a response for the resource at self.path_info. (Core)"""
+        try:
+            try:
+                try:
+                    self._do_respond(path_info)
+                except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
+                    inst = sys.exc_info()[1]
+                    inst.set_response()
+                    self.stage = 'before_finalize (HTTPError)'
+                    self.hooks.run('before_finalize')
+                    cherrypy.serving.response.finalize()
+            finally:
+                self.stage = 'on_end_resource'
+                self.hooks.run('on_end_resource')
+        except self.throws:
+            raise
+        except Exception:
+            if self.throw_errors:
+                raise
+            self.handle_error()
+
+    def _do_respond(self, path_info):
+        response = cherrypy.serving.response
+
+        if self.app is None:
+            raise cherrypy.NotFound()
+
+        self.hooks = self.__class__.hooks.copy()
+        self.toolmaps = {}
+
+        # Get the 'Host' header, so we can HTTPRedirect properly.
+        self.stage = 'process_headers'
+        self.process_headers()
+
+        self.stage = 'get_resource'
+        self.get_resource(path_info)
+
+        self.body = _cpreqbody.RequestBody(
+            self.rfile, self.headers, request_params=self.params)
+
+        self.namespaces(self.config)
+
+        self.stage = 'on_start_resource'
+        self.hooks.run('on_start_resource')
+
+        # Parse the querystring
+        self.stage = 'process_query_string'
+        self.process_query_string()
+
+        # Process the body
+        if self.process_request_body:
+            if self.method not in self.methods_with_bodies:
+                self.process_request_body = False
+        self.stage = 'before_request_body'
+        self.hooks.run('before_request_body')
+        if self.process_request_body:
+            self.body.process()
+
+        # Run the handler
+        self.stage = 'before_handler'
+        self.hooks.run('before_handler')
+        if self.handler:
+            self.stage = 'handler'
+            response.body = self.handler()
+
+        # Finalize
+        self.stage = 'before_finalize'
+        self.hooks.run('before_finalize')
+        response.finalize()
+
+    def process_query_string(self):
+        """Parse the query string into Python structures. (Core)"""
+        try:
+            p = httputil.parse_query_string(
+                self.query_string, encoding=self.query_string_encoding)
+        except UnicodeDecodeError:
+            raise cherrypy.HTTPError(
+                404, 'The given query string could not be processed. Query '
+                'strings for this resource must be encoded with %r.' %
+                self.query_string_encoding)
+
+        # Python 2 only: keyword arguments must be byte strings (type 'str').
+        if six.PY2:
+            for key, value in p.items():
+                if isinstance(key, six.text_type):
+                    del p[key]
+                    p[key.encode(self.query_string_encoding)] = value
+        self.params.update(p)
+
+    def process_headers(self):
+        """Parse HTTP header data into Python structures. (Core)"""
+        # Process the headers into self.headers
+        headers = self.headers
+        for name, value in self.header_list:
+            # Call title() now (and use dict.__method__(headers))
+            # so title doesn't have to be called twice.
+            name = name.title()
+            value = value.strip()
+
+            headers[name] = httputil.decode_TEXT_maybe(value)
+
+            # Some clients, notably Konquoror, supply multiple
+            # cookies on different lines with the same key. To
+            # handle this case, store all cookies in self.cookie.
+            if name == 'Cookie':
+                try:
+                    self.cookie.load(value)
+                except CookieError as exc:
+                    raise cherrypy.HTTPError(400, str(exc))
+
+        if not dict.__contains__(headers, 'Host'):
+            # All Internet-based HTTP/1.1 servers MUST respond with a 400
+            # (Bad Request) status code to any HTTP/1.1 request message
+            # which lacks a Host header field.
+            if self.protocol >= (1, 1):
+                msg = "HTTP/1.1 requires a 'Host' request header."
+                raise cherrypy.HTTPError(400, msg)
+        host = dict.get(headers, 'Host')
+        if not host:
+            host = self.local.name or self.local.ip
+        self.base = '%s://%s' % (self.scheme, host)
+
+    def get_resource(self, path):
+        """Call a dispatcher (which sets self.handler and .config). (Core)"""
+        # First, see if there is a custom dispatch at this URI. Custom
+        # dispatchers can only be specified in app.config, not in _cp_config
+        # (since custom dispatchers may not even have an app.root).
+        dispatch = self.app.find_config(
+            path, 'request.dispatch', self.dispatch)
+
+        # dispatch() should set self.handler and self.config
+        dispatch(path)
+
+    def handle_error(self):
+        """Handle the last unanticipated exception. (Core)"""
+        try:
+            self.hooks.run('before_error_response')
+            if self.error_response:
+                self.error_response()
+            self.hooks.run('after_error_response')
+            cherrypy.serving.response.finalize()
+        except cherrypy.HTTPRedirect:
+            inst = sys.exc_info()[1]
+            inst.set_response()
+            cherrypy.serving.response.finalize()
+
+
+class ResponseBody(object):
+
+    """The body of the HTTP response (the response entity)."""
+
+    unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
+                   'if you wish to return unicode.')
+
+    def __get__(self, obj, objclass=None):
+        if obj is None:
+            # When calling on the class instead of an instance...
+            return self
+        else:
+            return obj._body
+
+    def __set__(self, obj, value):
+        # Convert the given value to an iterable object.
+        if isinstance(value, six.text_type):
+            raise ValueError(self.unicode_err)
+        elif isinstance(value, list):
+            # every item in a list must be bytes...
+            if any(isinstance(item, six.text_type) for item in value):
+                raise ValueError(self.unicode_err)
+
+        obj._body = encoding.prepare_iter(value)
+
+
+class Response(object):
+
+    """An HTTP Response, including status, headers, and body."""
+
+    status = ''
+    """The HTTP Status-Code and Reason-Phrase."""
+
+    header_list = []
+    """
+    A list of the HTTP response headers as (name, value) tuples.
+    In general, you should use response.headers (a dict) instead. This
+    attribute is generated from response.headers and is not valid until
+    after the finalize phase."""
+
+    headers = httputil.HeaderMap()
+    """
+    A dict-like object containing the response headers. Keys are header
+    names (in Title-Case format); however, you may get and set them in
+    a case-insensitive manner. That is, headers['Content-Type'] and
+    headers['content-type'] refer to the same value. Values are header
+    values (decoded according to :rfc:`2047` if necessary).
+
+    .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
+    """
+
+    cookie = SimpleCookie()
+    """See help(Cookie)."""
+
+    body = ResponseBody()
+    """The body (entity) of the HTTP response."""
+
+    time = None
+    """The value of time.time() when created. Use in HTTP dates."""
+
+    stream = False
+    """If False, buffer the response body."""
+
+    def __init__(self):
+        self.status = None
+        self.header_list = None
+        self._body = []
+        self.time = time.time()
+
+        self.headers = httputil.HeaderMap()
+        # Since we know all our keys are titled strings, we can
+        # bypass HeaderMap.update and get a big speed boost.
+        dict.update(self.headers, {
+            'Content-Type': 'text/html',
+            'Server': 'CherryPy/' + cherrypy.__version__,
+            'Date': httputil.HTTPDate(self.time),
+        })
+        self.cookie = SimpleCookie()
+
+    def collapse_body(self):
+        """Collapse self.body to a single string; replace it and return it."""
+        new_body = b''.join(self.body)
+        self.body = new_body
+        return new_body
+
+    def _flush_body(self):
+        """
+        Discard self.body but consume any generator such that
+        any finalization can occur, such as is required by
+        caching.tee_output().
+        """
+        consume(iter(self.body))
+
+    def finalize(self):
+        """Transform headers (and cookies) into self.header_list. (Core)"""
+        try:
+            code, reason, _ = httputil.valid_status(self.status)
+        except ValueError:
+            raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
+
+        headers = self.headers
+
+        self.status = '%s %s' % (code, reason)
+        self.output_status = ntob(str(code), 'ascii') + \
+            b' ' + headers.encode(reason)
+
+        if self.stream:
+            # The upshot: wsgiserver will chunk the response if
+            # you pop Content-Length (or set it explicitly to None).
+            # Note that lib.static sets C-L to the file's st_size.
+            if dict.get(headers, 'Content-Length') is None:
+                dict.pop(headers, 'Content-Length', None)
+        elif code < 200 or code in (204, 205, 304):
+            # "All 1xx (informational), 204 (no content),
+            # and 304 (not modified) responses MUST NOT
+            # include a message-body."
+            dict.pop(headers, 'Content-Length', None)
+            self._flush_body()
+            self.body = b''
+        else:
+            # Responses which are not streamed should have a Content-Length,
+            # but allow user code to set Content-Length if desired.
+            if dict.get(headers, 'Content-Length') is None:
+                content = self.collapse_body()
+                dict.__setitem__(headers, 'Content-Length', len(content))
+
+        # Transform our header dict into a list of tuples.
+        self.header_list = h = headers.output()
+
+        cookie = self.cookie.output()
+        if cookie:
+            for line in cookie.split('\r\n'):
+                name, value = line.split(': ', 1)
+                if isinstance(name, six.text_type):
+                    name = name.encode('ISO-8859-1')
+                if isinstance(value, six.text_type):
+                    value = headers.encode(value)
+                h.append((name, value))
+
+
+class LazyUUID4(object):
+    def __str__(self):
+        """Return UUID4 and keep it for future calls."""
+        return str(self.uuid4)
+
+    @property
+    def uuid4(self):
+        """Provide unique id on per-request basis using UUID4.
+
+        It's evaluated lazily on render.
+        """
+        try:
+            self._uuid4
+        except AttributeError:
+            # evaluate on first access
+            self._uuid4 = uuid.uuid4()
+
+        return self._uuid4
diff --git a/libraries/cherrypy/_cpserver.py b/libraries/cherrypy/_cpserver.py
new file mode 100644
index 00000000..0f60e2c8
--- /dev/null
+++ b/libraries/cherrypy/_cpserver.py
@@ -0,0 +1,252 @@
+"""Manage HTTP servers with CherryPy."""
+
+import six
+
+import cherrypy
+from cherrypy.lib.reprconf import attributes
+from cherrypy._cpcompat import text_or_bytes
+from cherrypy.process.servers import ServerAdapter
+
+
+__all__ = ('Server', )
+
+
+class Server(ServerAdapter):
+    """An adapter for an HTTP server.
+
+    You can set attributes (like socket_host and socket_port)
+    on *this* object (which is probably cherrypy.server), and call
+    quickstart. For example::
+
+        cherrypy.server.socket_port = 80
+        cherrypy.quickstart()
+    """
+
+    socket_port = 8080
+    """The TCP port on which to listen for connections."""
+
+    _socket_host = '127.0.0.1'
+
+    @property
+    def socket_host(self):  # noqa: D401; irrelevant for properties
+        """The hostname or IP address on which to listen for connections.
+
+        Host values may be any IPv4 or IPv6 address, or any valid hostname.
+        The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
+        your hosts file prefers IPv6). The string '0.0.0.0' is a special
+        IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
+        is the similar IN6ADDR_ANY for IPv6. The empty string or None are
+        not allowed.
+        """
+        return self._socket_host
+
+    @socket_host.setter
+    def socket_host(self, value):
+        if value == '':
+            raise ValueError("The empty string ('') is not an allowed value. "
+                             "Use '0.0.0.0' instead to listen on all active "
+                             'interfaces (INADDR_ANY).')
+        self._socket_host = value
+
+    socket_file = None
+    """If given, the name of the UNIX socket to use instead of TCP/IP.
+
+    When this option is not None, the `socket_host` and `socket_port` options
+    are ignored."""
+
+    socket_queue_size = 5
+    """The 'backlog' argument to socket.listen(); specifies the maximum number
+    of queued connections (default 5)."""
+
+    socket_timeout = 10
+    """The timeout in seconds for accepted connections (default 10)."""
+
+    accepted_queue_size = -1
+    """The maximum number of requests which will be queued up before
+    the server refuses to accept it (default -1, meaning no limit)."""
+
+    accepted_queue_timeout = 10
+    """The timeout in seconds for attempting to add a request to the
+    queue when the queue is full (default 10)."""
+
+    shutdown_timeout = 5
+    """The time to wait for HTTP worker threads to clean up."""
+
+    protocol_version = 'HTTP/1.1'
+    """The version string to write in the Status-Line of all HTTP responses,
+    for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
+    this should also limit the supported features used in the response."""
+
+    thread_pool = 10
+    """The number of worker threads to start up in the pool."""
+
+    thread_pool_max = -1
+    """The maximum size of the worker-thread pool. Use -1 to indicate no limit.
+    """
+
+    max_request_header_size = 500 * 1024
+    """The maximum number of bytes allowable in the request headers.
+    If exceeded, the HTTP server should return "413 Request Entity Too Large".
+    """
+
+    max_request_body_size = 100 * 1024 * 1024
+    """The maximum number of bytes allowable in the request body. If exceeded,
+    the HTTP server should return "413 Request Entity Too Large"."""
+
+    instance = None
+    """If not None, this should be an HTTP server instance (such as
+    cheroot.wsgi.Server) which cherrypy.server will control.
+    Use this when you need
+    more control over object instantiation than is available in the various
+    configuration options."""
+
+    ssl_context = None
+    """When using PyOpenSSL, an instance of SSL.Context."""
+
+    ssl_certificate = None
+    """The filename of the SSL certificate to use."""
+
+    ssl_certificate_chain = None
+    """When using PyOpenSSL, the certificate chain to pass to
+    Context.load_verify_locations."""
+
+    ssl_private_key = None
+    """The filename of the private key to use with SSL."""
+
+    ssl_ciphers = None
+    """The ciphers list of SSL."""
+
+    if six.PY3:
+        ssl_module = 'builtin'
+        """The name of a registered SSL adaptation module to use with
+        the builtin WSGI server. Builtin options are: 'builtin' (to
+        use the SSL library built into recent versions of Python).
+        You may also register your own classes in the
+        cheroot.server.ssl_adapters dict."""
+    else:
+        ssl_module = 'pyopenssl'
+        """The name of a registered SSL adaptation module to use with the
+        builtin WSGI server. Builtin options are 'builtin' (to use the SSL
+        library built into recent versions of Python) and 'pyopenssl' (to
+        use the PyOpenSSL project, which you must install separately). You
+        may also register your own classes in the cheroot.server.ssl_adapters
+        dict."""
+
+    statistics = False
+    """Turns statistics-gathering on or off for aware HTTP servers."""
+
+    nodelay = True
+    """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
+
+    wsgi_version = (1, 0)
+    """The WSGI version tuple to use with the builtin WSGI server.
+    The provided options are (1, 0) [which includes support for PEP 3333,
+    which declares it covers WSGI version 1.0.1 but still mandates the
+    wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
+    You may create and register your own experimental versions of the WSGI
+    protocol by adding custom classes to the cheroot.server.wsgi_gateways dict.
+    """
+
+    peercreds = False
+    """If True, peer cred lookup for UNIX domain socket will put to WSGI env.
+
+    This information will then be available through WSGI env vars:
+    * X_REMOTE_PID
+    * X_REMOTE_UID
+    * X_REMOTE_GID
+    """
+
+    peercreds_resolve = False
+    """If True, username/group will be looked up in the OS from peercreds.
+
+    This information will then be available through WSGI env vars:
+    * REMOTE_USER
+    * X_REMOTE_USER
+    * X_REMOTE_GROUP
+    """
+
+    def __init__(self):
+        """Initialize Server instance."""
+        self.bus = cherrypy.engine
+        self.httpserver = None
+        self.interrupt = None
+        self.running = False
+
+    def httpserver_from_self(self, httpserver=None):
+        """Return a (httpserver, bind_addr) pair based on self attributes."""
+        if httpserver is None:
+            httpserver = self.instance
+        if httpserver is None:
+            from cherrypy import _cpwsgi_server
+            httpserver = _cpwsgi_server.CPWSGIServer(self)
+        if isinstance(httpserver, text_or_bytes):
+            # Is anyone using this? Can I add an arg?
+            httpserver = attributes(httpserver)(self)
+        return httpserver, self.bind_addr
+
+    def start(self):
+        """Start the HTTP server."""
+        if not self.httpserver:
+            self.httpserver, self.bind_addr = self.httpserver_from_self()
+        super(Server, self).start()
+    start.priority = 75
+
+    @property
+    def bind_addr(self):
+        """Return bind address.
+
+        A (host, port) tuple for TCP sockets or a str for Unix domain sockts.
+        """
+        if self.socket_file:
+            return self.socket_file
+        if self.socket_host is None and self.socket_port is None:
+            return None
+        return (self.socket_host, self.socket_port)
+
+    @bind_addr.setter
+    def bind_addr(self, value):
+        if value is None:
+            self.socket_file = None
+            self.socket_host = None
+            self.socket_port = None
+        elif isinstance(value, text_or_bytes):
+            self.socket_file = value
+            self.socket_host = None
+            self.socket_port = None
+        else:
+            try:
+                self.socket_host, self.socket_port = value
+                self.socket_file = None
+            except ValueError:
+                raise ValueError('bind_addr must be a (host, port) tuple '
+                                 '(for TCP sockets) or a string (for Unix '
+                                 'domain sockets), not %r' % value)
+
+    def base(self):
+        """Return the base for this server.
+
+        e.i. scheme://host[:port] or sock file
+        """
+        if self.socket_file:
+            return self.socket_file
+
+        host = self.socket_host
+        if host in ('0.0.0.0', '::'):
+            # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
+            # Look up the host name, which should be the
+            # safest thing to spit out in a URL.
+            import socket
+            host = socket.gethostname()
+
+        port = self.socket_port
+
+        if self.ssl_certificate:
+            scheme = 'https'
+            if port != 443:
+                host += ':%s' % port
+        else:
+            scheme = 'http'
+            if port != 80:
+                host += ':%s' % port
+
+        return '%s://%s' % (scheme, host)
diff --git a/libraries/cherrypy/_cptools.py b/libraries/cherrypy/_cptools.py
new file mode 100644
index 00000000..57460285
--- /dev/null
+++ b/libraries/cherrypy/_cptools.py
@@ -0,0 +1,509 @@
+"""CherryPy tools. A "tool" is any helper, adapted to CP.
+
+Tools are usually designed to be used in a variety of ways (although some
+may only offer one if they choose):
+
+    Library calls
+        All tools are callables that can be used wherever needed.
+        The arguments are straightforward and should be detailed within the
+        docstring.
+
+    Function decorators
+        All tools, when called, may be used as decorators which configure
+        individual CherryPy page handlers (methods on the CherryPy tree).
+        That is, "@tools.anytool()" should "turn on" the tool via the
+        decorated function's _cp_config attribute.
+
+    CherryPy config
+        If a tool exposes a "_setup" callable, it will be called
+        once per Request (if the feature is "turned on" via config).
+
+Tools may be implemented as any object with a namespace. The builtins
+are generally either modules or instances of the tools.Tool class.
+"""
+
+import six
+
+import cherrypy
+from cherrypy._helper import expose
+
+from cherrypy.lib import cptools, encoding, static, jsontools
+from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
+from cherrypy.lib import caching as _caching
+from cherrypy.lib import auth_basic, auth_digest
+
+
+def _getargs(func):
+    """Return the names of all static arguments to the given function."""
+    # Use this instead of importing inspect for less mem overhead.
+    import types
+    if six.PY3:
+        if isinstance(func, types.MethodType):
+            func = func.__func__
+        co = func.__code__
+    else:
+        if isinstance(func, types.MethodType):
+            func = func.im_func
+        co = func.func_code
+    return co.co_varnames[:co.co_argcount]
+
+
+_attr_error = (
+    'CherryPy Tools cannot be turned on directly. Instead, turn them '
+    'on via config, or use them as decorators on your page handlers.'
+)
+
+
+class Tool(object):
+
+    """A registered function for use with CherryPy request-processing hooks.
+
+    help(tool.callable) should give you more information about this Tool.
+    """
+
+    namespace = 'tools'
+
+    def __init__(self, point, callable, name=None, priority=50):
+        self._point = point
+        self.callable = callable
+        self._name = name
+        self._priority = priority
+        self.__doc__ = self.callable.__doc__
+        self._setargs()
+
+    @property
+    def on(self):
+        raise AttributeError(_attr_error)
+
+    @on.setter
+    def on(self, value):
+        raise AttributeError(_attr_error)
+
+    def _setargs(self):
+        """Copy func parameter names to obj attributes."""
+        try:
+            for arg in _getargs(self.callable):
+                setattr(self, arg, None)
+        except (TypeError, AttributeError):
+            if hasattr(self.callable, '__call__'):
+                for arg in _getargs(self.callable.__call__):
+                    setattr(self, arg, None)
+        # IronPython 1.0 raises NotImplementedError because
+        # inspect.getargspec tries to access Python bytecode
+        # in co_code attribute.
+        except NotImplementedError:
+            pass
+        # IronPython 1B1 may raise IndexError in some cases,
+        # but if we trap it here it doesn't prevent CP from working.
+        except IndexError:
+            pass
+
+    def _merged_args(self, d=None):
+        """Return a dict of configuration entries for this Tool."""
+        if d:
+            conf = d.copy()
+        else:
+            conf = {}
+
+        tm = cherrypy.serving.request.toolmaps[self.namespace]
+        if self._name in tm:
+            conf.update(tm[self._name])
+
+        if 'on' in conf:
+            del conf['on']
+
+        return conf
+
+    def __call__(self, *args, **kwargs):
+        """Compile-time decorator (turn on the tool in config).
+
+        For example::
+
+            @expose
+            @tools.proxy()
+            def whats_my_base(self):
+                return cherrypy.request.base
+        """
+        if args:
+            raise TypeError('The %r Tool does not accept positional '
+                            'arguments; you must use keyword arguments.'
+                            % self._name)
+
+        def tool_decorator(f):
+            if not hasattr(f, '_cp_config'):
+                f._cp_config = {}
+            subspace = self.namespace + '.' + self._name + '.'
+            f._cp_config[subspace + 'on'] = True
+            for k, v in kwargs.items():
+                f._cp_config[subspace + k] = v
+            return f
+        return tool_decorator
+
+    def _setup(self):
+        """Hook this tool into cherrypy.request.
+
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        conf = self._merged_args()
+        p = conf.pop('priority', None)
+        if p is None:
+            p = getattr(self.callable, 'priority', self._priority)
+        cherrypy.serving.request.hooks.attach(self._point, self.callable,
+                                              priority=p, **conf)
+
+
+class HandlerTool(Tool):
+
+    """Tool which is called 'before main', that may skip normal handlers.
+
+    If the tool successfully handles the request (by setting response.body),
+    if should return True. This will cause CherryPy to skip any 'normal' page
+    handler. If the tool did not handle the request, it should return False
+    to tell CherryPy to continue on and call the normal page handler. If the
+    tool is declared AS a page handler (see the 'handler' method), returning
+    False will raise NotFound.
+    """
+
+    def __init__(self, callable, name=None):
+        Tool.__init__(self, 'before_handler', callable, name)
+
+    def handler(self, *args, **kwargs):
+        """Use this tool as a CherryPy page handler.
+
+        For example::
+
+            class Root:
+                nav = tools.staticdir.handler(section="/nav", dir="nav",
+                                              root=absDir)
+        """
+        @expose
+        def handle_func(*a, **kw):
+            handled = self.callable(*args, **self._merged_args(kwargs))
+            if not handled:
+                raise cherrypy.NotFound()
+            return cherrypy.serving.response.body
+        return handle_func
+
+    def _wrapper(self, **kwargs):
+        if self.callable(**kwargs):
+            cherrypy.serving.request.handler = None
+
+    def _setup(self):
+        """Hook this tool into cherrypy.request.
+
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        conf = self._merged_args()
+        p = conf.pop('priority', None)
+        if p is None:
+            p = getattr(self.callable, 'priority', self._priority)
+        cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
+                                              priority=p, **conf)
+
+
+class HandlerWrapperTool(Tool):
+
+    """Tool which wraps request.handler in a provided wrapper function.
+
+    The 'newhandler' arg must be a handler wrapper function that takes a
+    'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
+    page handler
+    functions, it must return an iterable for use as cherrypy.response.body.
+
+    For example, to allow your 'inner' page handlers to return dicts
+    which then get interpolated into a template::
+
+        def interpolator(next_handler, *args, **kwargs):
+            filename = cherrypy.request.config.get('template')
+            cherrypy.response.template = env.get_template(filename)
+            response_dict = next_handler(*args, **kwargs)
+            return cherrypy.response.template.render(**response_dict)
+        cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
+    """
+
+    def __init__(self, newhandler, point='before_handler', name=None,
+                 priority=50):
+        self.newhandler = newhandler
+        self._point = point
+        self._name = name
+        self._priority = priority
+
+    def callable(self, *args, **kwargs):
+        innerfunc = cherrypy.serving.request.handler
+
+        def wrap(*args, **kwargs):
+            return self.newhandler(innerfunc, *args, **kwargs)
+        cherrypy.serving.request.handler = wrap
+
+
+class ErrorTool(Tool):
+
+    """Tool which is used to replace the default request.error_response."""
+
+    def __init__(self, callable, name=None):
+        Tool.__init__(self, None, callable, name)
+
+    def _wrapper(self):
+        self.callable(**self._merged_args())
+
+    def _setup(self):
+        """Hook this tool into cherrypy.request.
+
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        cherrypy.serving.request.error_response = self._wrapper
+
+
+#                              Builtin tools                              #
+
+
+class SessionTool(Tool):
+
+    """Session Tool for CherryPy.
+
+    sessions.locking
+        When 'implicit' (the default), the session will be locked for you,
+        just before running the page handler.
+
+        When 'early', the session will be locked before reading the request
+        body. This is off by default for safety reasons; for example,
+        a large upload would block the session, denying an AJAX
+        progress meter
+        (`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
+
+        When 'explicit' (or any other value), you need to call
+        cherrypy.session.acquire_lock() yourself before using
+        session data.
+    """
+
+    def __init__(self):
+        # _sessions.init must be bound after headers are read
+        Tool.__init__(self, 'before_request_body', _sessions.init)
+
+    def _lock_session(self):
+        cherrypy.serving.session.acquire_lock()
+
+    def _setup(self):
+        """Hook this tool into cherrypy.request.
+
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        hooks = cherrypy.serving.request.hooks
+
+        conf = self._merged_args()
+
+        p = conf.pop('priority', None)
+        if p is None:
+            p = getattr(self.callable, 'priority', self._priority)
+
+        hooks.attach(self._point, self.callable, priority=p, **conf)
+
+        locking = conf.pop('locking', 'implicit')
+        if locking == 'implicit':
+            hooks.attach('before_handler', self._lock_session)
+        elif locking == 'early':
+            # Lock before the request body (but after _sessions.init runs!)
+            hooks.attach('before_request_body', self._lock_session,
+                         priority=60)
+        else:
+            # Don't lock
+            pass
+
+        hooks.attach('before_finalize', _sessions.save)
+        hooks.attach('on_end_request', _sessions.close)
+
+    def regenerate(self):
+        """Drop the current session and make a new one (with a new id)."""
+        sess = cherrypy.serving.session
+        sess.regenerate()
+
+        # Grab cookie-relevant tool args
+        relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure'
+        conf = dict(
+            (k, v)
+            for k, v in self._merged_args().items()
+            if k in relevant
+        )
+        _sessions.set_response_cookie(**conf)
+
+
+class XMLRPCController(object):
+
+    """A Controller (page handler collection) for XML-RPC.
+
+    To use it, have your controllers subclass this base class (it will
+    turn on the tool for you).
+
+    You can also supply the following optional config entries::
+
+        tools.xmlrpc.encoding: 'utf-8'
+        tools.xmlrpc.allow_none: 0
+
+    XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
+    appropriate handler must first be performed according to the URL, and
+    then a second dispatch step must take place according to the RPC method
+    specified in the request body. It also allows a superfluous "/RPC2"
+    prefix in the URL, supplies its own handler args in the body, and
+    requires a 200 OK "Fault" response instead of 404 when the desired
+    method is not found.
+
+    Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
+    This Controller acts as the dispatch target for the first half (based
+    on the URL); it then reads the RPC method from the request body and
+    does its own second dispatch step based on that method. It also reads
+    body params, and returns a Fault on error.
+
+    The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
+    in your URL's, you can safely skip turning on the XMLRPCDispatcher.
+    Otherwise, you need to use declare it in config::
+
+        request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
+    """
+
+    # Note we're hard-coding this into the 'tools' namespace. We could do
+    # a huge amount of work to make it relocatable, but the only reason why
+    # would be if someone actually disabled the default_toolbox. Meh.
+    _cp_config = {'tools.xmlrpc.on': True}
+
+    @expose
+    def default(self, *vpath, **params):
+        rpcparams, rpcmethod = _xmlrpc.process_body()
+
+        subhandler = self
+        for attr in str(rpcmethod).split('.'):
+            subhandler = getattr(subhandler, attr, None)
+
+        if subhandler and getattr(subhandler, 'exposed', False):
+            body = subhandler(*(vpath + rpcparams), **params)
+
+        else:
+            # https://github.com/cherrypy/cherrypy/issues/533
+            # if a method is not found, an xmlrpclib.Fault should be returned
+            # raising an exception here will do that; see
+            # cherrypy.lib.xmlrpcutil.on_error
+            raise Exception('method "%s" is not supported' % attr)
+
+        conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {})
+        _xmlrpc.respond(body,
+                        conf.get('encoding', 'utf-8'),
+                        conf.get('allow_none', 0))
+        return cherrypy.serving.response.body
+
+
+class SessionAuthTool(HandlerTool):
+    pass
+
+
+class CachingTool(Tool):
+
+    """Caching Tool for CherryPy."""
+
+    def _wrapper(self, **kwargs):
+        request = cherrypy.serving.request
+        if _caching.get(**kwargs):
+            request.handler = None
+        else:
+            if request.cacheable:
+                # Note the devious technique here of adding hooks on the fly
+                request.hooks.attach('before_finalize', _caching.tee_output,
+                                     priority=100)
+    _wrapper.priority = 90
+
+    def _setup(self):
+        """Hook caching into cherrypy.request."""
+        conf = self._merged_args()
+
+        p = conf.pop('priority', None)
+        cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
+                                              priority=p, **conf)
+
+
+class Toolbox(object):
+
+    """A collection of Tools.
+
+    This object also functions as a config namespace handler for itself.
+    Custom toolboxes should be added to each Application's toolboxes dict.
+    """
+
+    def __init__(self, namespace):
+        self.namespace = namespace
+
+    def __setattr__(self, name, value):
+        # If the Tool._name is None, supply it from the attribute name.
+        if isinstance(value, Tool):
+            if value._name is None:
+                value._name = name
+            value.namespace = self.namespace
+        object.__setattr__(self, name, value)
+
+    def __enter__(self):
+        """Populate request.toolmaps from tools specified in config."""
+        cherrypy.serving.request.toolmaps[self.namespace] = map = {}
+
+        def populate(k, v):
+            toolname, arg = k.split('.', 1)
+            bucket = map.setdefault(toolname, {})
+            bucket[arg] = v
+        return populate
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        """Run tool._setup() for each tool in our toolmap."""
+        map = cherrypy.serving.request.toolmaps.get(self.namespace)
+        if map:
+            for name, settings in map.items():
+                if settings.get('on', False):
+                    tool = getattr(self, name)
+                    tool._setup()
+
+    def register(self, point, **kwargs):
+        """
+        Return a decorator which registers the function
+        at the given hook point.
+        """
+        def decorator(func):
+            attr_name = kwargs.get('name', func.__name__)
+            tool = Tool(point, func, **kwargs)
+            setattr(self, attr_name, tool)
+            return func
+        return decorator
+
+
+default_toolbox = _d = Toolbox('tools')
+_d.session_auth = SessionAuthTool(cptools.session_auth)
+_d.allow = Tool('on_start_resource', cptools.allow)
+_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
+_d.response_headers = Tool('on_start_resource', cptools.response_headers)
+_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
+_d.log_headers = Tool('before_error_response', cptools.log_request_headers)
+_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
+_d.err_redirect = ErrorTool(cptools.redirect)
+_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
+_d.decode = Tool('before_request_body', encoding.decode)
+# the order of encoding, gzip, caching is important
+_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70)
+_d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
+_d.staticdir = HandlerTool(static.staticdir)
+_d.staticfile = HandlerTool(static.staticfile)
+_d.sessions = SessionTool()
+_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
+_d.caching = CachingTool('before_handler', _caching.get, 'caching')
+_d.expires = Tool('before_finalize', _caching.expires)
+_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
+_d.referer = Tool('before_request_body', cptools.referer)
+_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
+_d.flatten = Tool('before_finalize', cptools.flatten)
+_d.accept = Tool('on_start_resource', cptools.accept)
+_d.redirect = Tool('on_start_resource', cptools.redirect)
+_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0)
+_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
+_d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
+_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
+_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
+_d.params = Tool('before_handler', cptools.convert_params, priority=15)
+
+del _d, cptools, encoding, static
diff --git a/libraries/cherrypy/_cptree.py b/libraries/cherrypy/_cptree.py
new file mode 100644
index 00000000..ceb54379
--- /dev/null
+++ b/libraries/cherrypy/_cptree.py
@@ -0,0 +1,313 @@
+"""CherryPy Application and Tree objects."""
+
+import os
+
+import six
+
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
+from cherrypy.lib import httputil, reprconf
+
+
+class Application(object):
+    """A CherryPy Application.
+
+    Servers and gateways should not instantiate Request objects directly.
+    Instead, they should ask an Application object for a request object.
+
+    An instance of this class may also be used as a WSGI callable
+    (WSGI application object) for itself.
+    """
+
+    root = None
+    """The top-most container of page handlers for this app. Handlers should
+    be arranged in a hierarchy of attributes, matching the expected URI
+    hierarchy; the default dispatcher then searches this hierarchy for a
+    matching handler. When using a dispatcher other than the default,
+    this value may be None."""
+
+    config = {}
+    """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
+    of {key: value} pairs."""
+
+    namespaces = reprconf.NamespaceSet()
+    toolboxes = {'tools': cherrypy.tools}
+
+    log = None
+    """A LogManager instance. See _cplogging."""
+
+    wsgiapp = None
+    """A CPWSGIApp instance. See _cpwsgi."""
+
+    request_class = _cprequest.Request
+    response_class = _cprequest.Response
+
+    relative_urls = False
+
+    def __init__(self, root, script_name='', config=None):
+        """Initialize Application with given root."""
+        self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
+        self.root = root
+        self.script_name = script_name
+        self.wsgiapp = _cpwsgi.CPWSGIApp(self)
+
+        self.namespaces = self.namespaces.copy()
+        self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
+        self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
+
+        self.config = self.__class__.config.copy()
+        if config:
+            self.merge(config)
+
+    def __repr__(self):
+        """Generate a representation of the Application instance."""
+        return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
+                                  self.root, self.script_name)
+
+    script_name_doc = """The URI "mount point" for this app. A mount point
+    is that portion of the URI which is constant for all URIs that are
+    serviced by this application; it does not include scheme, host, or proxy
+    ("virtual host") portions of the URI.
+
+    For example, if script_name is "/my/cool/app", then the URL
+    "http://www.example.com/my/cool/app/page1" might be handled by a
+    "page1" method on the root object.
+
+    The value of script_name MUST NOT end in a slash. If the script_name
+    refers to the root of the URI, it MUST be an empty string (not "/").
+
+    If script_name is explicitly set to None, then the script_name will be
+    provided for each call from request.wsgi_environ['SCRIPT_NAME'].
+    """
+
+    @property
+    def script_name(self):  # noqa: D401; irrelevant for properties
+        """The URI "mount point" for this app.
+
+        A mount point is that portion of the URI which is constant for all URIs
+        that are serviced by this application; it does not include scheme,
+        host, or proxy ("virtual host") portions of the URI.
+
+        For example, if script_name is "/my/cool/app", then the URL
+        "http://www.example.com/my/cool/app/page1" might be handled by a
+        "page1" method on the root object.
+
+        The value of script_name MUST NOT end in a slash. If the script_name
+        refers to the root of the URI, it MUST be an empty string (not "/").
+
+        If script_name is explicitly set to None, then the script_name will be
+        provided for each call from request.wsgi_environ['SCRIPT_NAME'].
+        """
+        if self._script_name is not None:
+            return self._script_name
+
+        # A `_script_name` with a value of None signals that the script name
+        # should be pulled from WSGI environ.
+        return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
+
+    @script_name.setter
+    def script_name(self, value):
+        if value:
+            value = value.rstrip('/')
+        self._script_name = value
+
+    def merge(self, config):
+        """Merge the given config into self.config."""
+        _cpconfig.merge(self.config, config)
+
+        # Handle namespaces specified in config.
+        self.namespaces(self.config.get('/', {}))
+
+    def find_config(self, path, key, default=None):
+        """Return the most-specific value for key along path, or default."""
+        trail = path or '/'
+        while trail:
+            nodeconf = self.config.get(trail, {})
+
+            if key in nodeconf:
+                return nodeconf[key]
+
+            lastslash = trail.rfind('/')
+            if lastslash == -1:
+                break
+            elif lastslash == 0 and trail != '/':
+                trail = '/'
+            else:
+                trail = trail[:lastslash]
+
+        return default
+
+    def get_serving(self, local, remote, scheme, sproto):
+        """Create and return a Request and Response object."""
+        req = self.request_class(local, remote, scheme, sproto)
+        req.app = self
+
+        for name, toolbox in self.toolboxes.items():
+            req.namespaces[name] = toolbox
+
+        resp = self.response_class()
+        cherrypy.serving.load(req, resp)
+        cherrypy.engine.publish('acquire_thread')
+        cherrypy.engine.publish('before_request')
+
+        return req, resp
+
+    def release_serving(self):
+        """Release the current serving (request and response)."""
+        req = cherrypy.serving.request
+
+        cherrypy.engine.publish('after_request')
+
+        try:
+            req.close()
+        except Exception:
+            cherrypy.log(traceback=True, severity=40)
+
+        cherrypy.serving.clear()
+
+    def __call__(self, environ, start_response):
+        """Call a WSGI-callable."""
+        return self.wsgiapp(environ, start_response)
+
+
+class Tree(object):
+    """A registry of CherryPy applications, mounted at diverse points.
+
+    An instance of this class may also be used as a WSGI callable
+    (WSGI application object), in which case it dispatches to all
+    mounted apps.
+    """
+
+    apps = {}
+    """
+    A dict of the form {script name: application}, where "script name"
+    is a string declaring the URI mount point (no trailing slash), and
+    "application" is an instance of cherrypy.Application (or an arbitrary
+    WSGI callable if you happen to be using a WSGI server)."""
+
+    def __init__(self):
+        """Initialize registry Tree."""
+        self.apps = {}
+
+    def mount(self, root, script_name='', config=None):
+        """Mount a new app from a root object, script_name, and config.
+
+        root
+            An instance of a "controller class" (a collection of page
+            handler methods) which represents the root of the application.
+            This may also be an Application instance, or None if using
+            a dispatcher other than the default.
+
+        script_name
+            A string containing the "mount point" of the application.
+            This should start with a slash, and be the path portion of the
+            URL at which to mount the given root. For example, if root.index()
+            will handle requests to "http://www.example.com:8080/dept/app1/",
+            then the script_name argument would be "/dept/app1".
+
+            It MUST NOT end in a slash. If the script_name refers to the
+            root of the URI, it MUST be an empty string (not "/").
+
+        config
+            A file or dict containing application config.
+        """
+        if script_name is None:
+            raise TypeError(
+                "The 'script_name' argument may not be None. Application "
+                'objects may, however, possess a script_name of None (in '
+                'order to inpect the WSGI environ for SCRIPT_NAME upon each '
+                'request). You cannot mount such Applications on this Tree; '
+                'you must pass them to a WSGI server interface directly.')
+
+        # Next line both 1) strips trailing slash and 2) maps "/" -> "".
+        script_name = script_name.rstrip('/')
+
+        if isinstance(root, Application):
+            app = root
+            if script_name != '' and script_name != app.script_name:
+                raise ValueError(
+                    'Cannot specify a different script name and pass an '
+                    'Application instance to cherrypy.mount')
+            script_name = app.script_name
+        else:
+            app = Application(root, script_name)
+
+            # If mounted at "", add favicon.ico
+            needs_favicon = (
+                script_name == ''
+                and root is not None
+                and not hasattr(root, 'favicon_ico')
+            )
+            if needs_favicon:
+                favicon = os.path.join(
+                    os.getcwd(),
+                    os.path.dirname(__file__),
+                    'favicon.ico',
+                )
+                root.favicon_ico = tools.staticfile.handler(favicon)
+
+        if config:
+            app.merge(config)
+
+        self.apps[script_name] = app
+
+        return app
+
+    def graft(self, wsgi_callable, script_name=''):
+        """Mount a wsgi callable at the given script_name."""
+        # Next line both 1) strips trailing slash and 2) maps "/" -> "".
+        script_name = script_name.rstrip('/')
+        self.apps[script_name] = wsgi_callable
+
+    def script_name(self, path=None):
+        """Return the script_name of the app at the given path, or None.
+
+        If path is None, cherrypy.request is used.
+        """
+        if path is None:
+            try:
+                request = cherrypy.serving.request
+                path = httputil.urljoin(request.script_name,
+                                        request.path_info)
+            except AttributeError:
+                return None
+
+        while True:
+            if path in self.apps:
+                return path
+
+            if path == '':
+                return None
+
+            # Move one node up the tree and try again.
+            path = path[:path.rfind('/')]
+
+    def __call__(self, environ, start_response):
+        """Pre-initialize WSGI env and call WSGI-callable."""
+        # If you're calling this, then you're probably setting SCRIPT_NAME
+        # to '' (some WSGI servers always set SCRIPT_NAME to '').
+        # Try to look up the app using the full path.
+        env1x = environ
+        if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
+            env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
+        path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
+                                env1x.get('PATH_INFO', ''))
+        sn = self.script_name(path or '/')
+        if sn is None:
+            start_response('404 Not Found', [])
+            return []
+
+        app = self.apps[sn]
+
+        # Correct the SCRIPT_NAME and PATH_INFO environ entries.
+        environ = environ.copy()
+        if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
+            # Python 2/WSGI u.0: all strings MUST be of type unicode
+            enc = environ[ntou('wsgi.url_encoding')]
+            environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
+            environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
+        else:
+            environ['SCRIPT_NAME'] = sn
+            environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
+        return app(environ, start_response)
diff --git a/libraries/cherrypy/_cpwsgi.py b/libraries/cherrypy/_cpwsgi.py
new file mode 100644
index 00000000..0b4942ff
--- /dev/null
+++ b/libraries/cherrypy/_cpwsgi.py
@@ -0,0 +1,467 @@
+"""WSGI interface (see PEP 333 and 3333).
+
+Note that WSGI environ keys and values are 'native strings'; that is,
+whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
+it's a unicode string. But PEP 3333 says: "even if Python's str type is
+actually Unicode "under the hood", the content of native strings must
+still be translatable to bytes via the Latin-1 encoding!"
+"""
+
+import sys as _sys
+import io
+
+import six
+
+import cherrypy as _cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy import _cperror
+from cherrypy.lib import httputil
+from cherrypy.lib import is_closable_iterator
+
+
+def downgrade_wsgi_ux_to_1x(environ):
+    """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
+    """
+    env1x = {}
+
+    url_encoding = environ[ntou('wsgi.url_encoding')]
+    for k, v in list(environ.items()):
+        if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
+            v = v.encode(url_encoding)
+        elif isinstance(v, six.text_type):
+            v = v.encode('ISO-8859-1')
+        env1x[k.encode('ISO-8859-1')] = v
+
+    return env1x
+
+
+class VirtualHost(object):
+
+    """Select a different WSGI application based on the Host header.
+
+    This can be useful when running multiple sites within one CP server.
+    It allows several domains to point to different applications. For example::
+
+        root = Root()
+        RootApp = cherrypy.Application(root)
+        Domain2App = cherrypy.Application(root)
+        SecureApp = cherrypy.Application(Secure())
+
+        vhost = cherrypy._cpwsgi.VirtualHost(
+            RootApp,
+            domains={
+                'www.domain2.example': Domain2App,
+                'www.domain2.example:443': SecureApp,
+            },
+        )
+
+        cherrypy.tree.graft(vhost)
+    """
+    default = None
+    """Required. The default WSGI application."""
+
+    use_x_forwarded_host = True
+    """If True (the default), any "X-Forwarded-Host"
+    request header will be used instead of the "Host" header. This
+    is commonly added by HTTP servers (such as Apache) when proxying."""
+
+    domains = {}
+    """A dict of {host header value: application} pairs.
+    The incoming "Host" request header is looked up in this dict,
+    and, if a match is found, the corresponding WSGI application
+    will be called instead of the default. Note that you often need
+    separate entries for "example.com" and "www.example.com".
+    In addition, "Host" headers may contain the port number.
+    """
+
+    def __init__(self, default, domains=None, use_x_forwarded_host=True):
+        self.default = default
+        self.domains = domains or {}
+        self.use_x_forwarded_host = use_x_forwarded_host
+
+    def __call__(self, environ, start_response):
+        domain = environ.get('HTTP_HOST', '')
+        if self.use_x_forwarded_host:
+            domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
+
+        nextapp = self.domains.get(domain)
+        if nextapp is None:
+            nextapp = self.default
+        return nextapp(environ, start_response)
+
+
+class InternalRedirector(object):
+
+    """WSGI middleware that handles raised cherrypy.InternalRedirect."""
+
+    def __init__(self, nextapp, recursive=False):
+        self.nextapp = nextapp
+        self.recursive = recursive
+
+    def __call__(self, environ, start_response):
+        redirections = []
+        while True:
+            environ = environ.copy()
+            try:
+                return self.nextapp(environ, start_response)
+            except _cherrypy.InternalRedirect:
+                ir = _sys.exc_info()[1]
+                sn = environ.get('SCRIPT_NAME', '')
+                path = environ.get('PATH_INFO', '')
+                qs = environ.get('QUERY_STRING', '')
+
+                # Add the *previous* path_info + qs to redirections.
+                old_uri = sn + path
+                if qs:
+                    old_uri += '?' + qs
+                redirections.append(old_uri)
+
+                if not self.recursive:
+                    # Check to see if the new URI has been redirected to
+                    # already
+                    new_uri = sn + ir.path
+                    if ir.query_string:
+                        new_uri += '?' + ir.query_string
+                    if new_uri in redirections:
+                        ir.request.close()
+                        tmpl = (
+                            'InternalRedirector visited the same URL twice: %r'
+                        )
+                        raise RuntimeError(tmpl % new_uri)
+
+                # Munge the environment and try again.
+                environ['REQUEST_METHOD'] = 'GET'
+                environ['PATH_INFO'] = ir.path
+                environ['QUERY_STRING'] = ir.query_string
+                environ['wsgi.input'] = io.BytesIO()
+                environ['CONTENT_LENGTH'] = '0'
+                environ['cherrypy.previous_request'] = ir.request
+
+
+class ExceptionTrapper(object):
+
+    """WSGI middleware that traps exceptions."""
+
+    def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
+        self.nextapp = nextapp
+        self.throws = throws
+
+    def __call__(self, environ, start_response):
+        return _TrappedResponse(
+            self.nextapp,
+            environ,
+            start_response,
+            self.throws
+        )
+
+
+class _TrappedResponse(object):
+
+    response = iter([])
+
+    def __init__(self, nextapp, environ, start_response, throws):
+        self.nextapp = nextapp
+        self.environ = environ
+        self.start_response = start_response
+        self.throws = throws
+        self.started_response = False
+        self.response = self.trap(
+            self.nextapp, self.environ, self.start_response,
+        )
+        self.iter_response = iter(self.response)
+
+    def __iter__(self):
+        self.started_response = True
+        return self
+
+    def __next__(self):
+        return self.trap(next, self.iter_response)
+
+    # todo: https://pythonhosted.org/six/#six.Iterator
+    if six.PY2:
+        next = __next__
+
+    def close(self):
+        if hasattr(self.response, 'close'):
+            self.response.close()
+
+    def trap(self, func, *args, **kwargs):
+        try:
+            return func(*args, **kwargs)
+        except self.throws:
+            raise
+        except StopIteration:
+            raise
+        except Exception:
+            tb = _cperror.format_exc()
+            _cherrypy.log(tb, severity=40)
+            if not _cherrypy.request.show_tracebacks:
+                tb = ''
+            s, h, b = _cperror.bare_error(tb)
+            if six.PY3:
+                # What fun.
+                s = s.decode('ISO-8859-1')
+                h = [
+                    (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
+                    for k, v in h
+                ]
+            if self.started_response:
+                # Empty our iterable (so future calls raise StopIteration)
+                self.iter_response = iter([])
+            else:
+                self.iter_response = iter(b)
+
+            try:
+                self.start_response(s, h, _sys.exc_info())
+            except Exception:
+                # "The application must not trap any exceptions raised by
+                # start_response, if it called start_response with exc_info.
+                # Instead, it should allow such exceptions to propagate
+                # back to the server or gateway."
+                # But we still log and call close() to clean up ourselves.
+                _cherrypy.log(traceback=True, severity=40)
+                raise
+
+            if self.started_response:
+                return b''.join(b)
+            else:
+                return b
+
+
+#                           WSGI-to-CP Adapter                           #
+
+
+class AppResponse(object):
+
+    """WSGI response iterable for CherryPy applications."""
+
+    def __init__(self, environ, start_response, cpapp):
+        self.cpapp = cpapp
+        try:
+            if six.PY2:
+                if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
+                    environ = downgrade_wsgi_ux_to_1x(environ)
+            self.environ = environ
+            self.run()
+
+            r = _cherrypy.serving.response
+
+            outstatus = r.output_status
+            if not isinstance(outstatus, bytes):
+                raise TypeError('response.output_status is not a byte string.')
+
+            outheaders = []
+            for k, v in r.header_list:
+                if not isinstance(k, bytes):
+                    tmpl = 'response.header_list key %r is not a byte string.'
+                    raise TypeError(tmpl % k)
+                if not isinstance(v, bytes):
+                    tmpl = (
+                        'response.header_list value %r is not a byte string.'
+                    )
+                    raise TypeError(tmpl % v)
+                outheaders.append((k, v))
+
+            if six.PY3:
+                # According to PEP 3333, when using Python 3, the response
+                # status and headers must be bytes masquerading as unicode;
+                # that is, they must be of type "str" but are restricted to
+                # code points in the "latin-1" set.
+                outstatus = outstatus.decode('ISO-8859-1')
+                outheaders = [
+                    (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
+                    for k, v in outheaders
+                ]
+
+            self.iter_response = iter(r.body)
+            self.write = start_response(outstatus, outheaders)
+        except BaseException:
+            self.close()
+            raise
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        return next(self.iter_response)
+
+    # todo: https://pythonhosted.org/six/#six.Iterator
+    if six.PY2:
+        next = __next__
+
+    def close(self):
+        """Close and de-reference the current request and response. (Core)"""
+        streaming = _cherrypy.serving.response.stream
+        self.cpapp.release_serving()
+
+        # We avoid the expense of examining the iterator to see if it's
+        # closable unless we are streaming the response, as that's the
+        # only situation where we are going to have an iterator which
+        # may not have been exhausted yet.
+        if streaming and is_closable_iterator(self.iter_response):
+            iter_close = self.iter_response.close
+            try:
+                iter_close()
+            except Exception:
+                _cherrypy.log(traceback=True, severity=40)
+
+    def run(self):
+        """Create a Request object using environ."""
+        env = self.environ.get
+
+        local = httputil.Host(
+            '',
+            int(env('SERVER_PORT', 80) or -1),
+            env('SERVER_NAME', ''),
+        )
+        remote = httputil.Host(
+            env('REMOTE_ADDR', ''),
+            int(env('REMOTE_PORT', -1) or -1),
+            env('REMOTE_HOST', ''),
+        )
+        scheme = env('wsgi.url_scheme')
+        sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
+        request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
+
+        # LOGON_USER is served by IIS, and is the name of the
+        # user after having been mapped to a local account.
+        # Both IIS and Apache set REMOTE_USER, when possible.
+        request.login = env('LOGON_USER') or env('REMOTE_USER') or None
+        request.multithread = self.environ['wsgi.multithread']
+        request.multiprocess = self.environ['wsgi.multiprocess']
+        request.wsgi_environ = self.environ
+        request.prev = env('cherrypy.previous_request', None)
+
+        meth = self.environ['REQUEST_METHOD']
+
+        path = httputil.urljoin(
+            self.environ.get('SCRIPT_NAME', ''),
+            self.environ.get('PATH_INFO', ''),
+        )
+        qs = self.environ.get('QUERY_STRING', '')
+
+        path, qs = self.recode_path_qs(path, qs) or (path, qs)
+
+        rproto = self.environ.get('SERVER_PROTOCOL')
+        headers = self.translate_headers(self.environ)
+        rfile = self.environ['wsgi.input']
+        request.run(meth, path, qs, rproto, headers, rfile)
+
+    headerNames = {
+        'HTTP_CGI_AUTHORIZATION': 'Authorization',
+        'CONTENT_LENGTH': 'Content-Length',
+        'CONTENT_TYPE': 'Content-Type',
+        'REMOTE_HOST': 'Remote-Host',
+        'REMOTE_ADDR': 'Remote-Addr',
+    }
+
+    def recode_path_qs(self, path, qs):
+        if not six.PY3:
+            return
+
+        # This isn't perfect; if the given PATH_INFO is in the
+        # wrong encoding, it may fail to match the appropriate config
+        # section URI. But meh.
+        old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
+        new_enc = self.cpapp.find_config(
+            self.environ.get('PATH_INFO', ''),
+            'request.uri_encoding', 'utf-8',
+        )
+        if new_enc.lower() == old_enc.lower():
+            return
+
+        # Even though the path and qs are unicode, the WSGI server
+        # is required by PEP 3333 to coerce them to ISO-8859-1
+        # masquerading as unicode. So we have to encode back to
+        # bytes and then decode again using the "correct" encoding.
+        try:
+            return (
+                path.encode(old_enc).decode(new_enc),
+                qs.encode(old_enc).decode(new_enc),
+            )
+        except (UnicodeEncodeError, UnicodeDecodeError):
+            # Just pass them through without transcoding and hope.
+            pass
+
+    def translate_headers(self, environ):
+        """Translate CGI-environ header names to HTTP header names."""
+        for cgiName in environ:
+            # We assume all incoming header keys are uppercase already.
+            if cgiName in self.headerNames:
+                yield self.headerNames[cgiName], environ[cgiName]
+            elif cgiName[:5] == 'HTTP_':
+                # Hackish attempt at recovering original header names.
+                translatedHeader = cgiName[5:].replace('_', '-')
+                yield translatedHeader, environ[cgiName]
+
+
+class CPWSGIApp(object):
+
+    """A WSGI application object for a CherryPy Application."""
+
+    pipeline = [
+        ('ExceptionTrapper', ExceptionTrapper),
+        ('InternalRedirector', InternalRedirector),
+    ]
+    """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
+    constructor that takes an initial, positional 'nextapp' argument,
+    plus optional keyword arguments, and returns a WSGI application
+    (that takes environ and start_response arguments). The 'name' can
+    be any you choose, and will correspond to keys in self.config."""
+
+    head = None
+    """Rather than nest all apps in the pipeline on each call, it's only
+    done the first time, and the result is memoized into self.head. Set
+    this to None again if you change self.pipeline after calling self."""
+
+    config = {}
+    """A dict whose keys match names listed in the pipeline. Each
+    value is a further dict which will be passed to the corresponding
+    named WSGI callable (from the pipeline) as keyword arguments."""
+
+    response_class = AppResponse
+    """The class to instantiate and return as the next app in the WSGI chain.
+    """
+
+    def __init__(self, cpapp, pipeline=None):
+        self.cpapp = cpapp
+        self.pipeline = self.pipeline[:]
+        if pipeline:
+            self.pipeline.extend(pipeline)
+        self.config = self.config.copy()
+
+    def tail(self, environ, start_response):
+        """WSGI application callable for the actual CherryPy application.
+
+        You probably shouldn't call this; call self.__call__ instead,
+        so that any WSGI middleware in self.pipeline can run first.
+        """
+        return self.response_class(environ, start_response, self.cpapp)
+
+    def __call__(self, environ, start_response):
+        head = self.head
+        if head is None:
+            # Create and nest the WSGI apps in our pipeline (in reverse order).
+            # Then memoize the result in self.head.
+            head = self.tail
+            for name, callable in self.pipeline[::-1]:
+                conf = self.config.get(name, {})
+                head = callable(head, **conf)
+            self.head = head
+        return head(environ, start_response)
+
+    def namespace_handler(self, k, v):
+        """Config handler for the 'wsgi' namespace."""
+        if k == 'pipeline':
+            # Note this allows multiple 'wsgi.pipeline' config entries
+            # (but each entry will be processed in a 'random' order).
+            # It should also allow developers to set default middleware
+            # in code (passed to self.__init__) that deployers can add to
+            # (but not remove) via config.
+            self.pipeline.extend(v)
+        elif k == 'response_class':
+            self.response_class = v
+        else:
+            name, arg = k.split('.', 1)
+            bucket = self.config.setdefault(name, {})
+            bucket[arg] = v
diff --git a/libraries/cherrypy/_cpwsgi_server.py b/libraries/cherrypy/_cpwsgi_server.py
new file mode 100644
index 00000000..11dd846a
--- /dev/null
+++ b/libraries/cherrypy/_cpwsgi_server.py
@@ -0,0 +1,110 @@
+"""
+WSGI server interface (see PEP 333).
+
+This adds some CP-specific bits to the framework-agnostic cheroot package.
+"""
+import sys
+
+import cheroot.wsgi
+import cheroot.server
+
+import cherrypy
+
+
+class CPWSGIHTTPRequest(cheroot.server.HTTPRequest):
+    """Wrapper for cheroot.server.HTTPRequest.
+
+    This is a layer, which preserves URI parsing mode like it which was
+    before Cheroot v5.8.0.
+    """
+
+    def __init__(self, server, conn):
+        """Initialize HTTP request container instance.
+
+        Args:
+            server (cheroot.server.HTTPServer):
+                web server object receiving this request
+            conn (cheroot.server.HTTPConnection):
+                HTTP connection object for this request
+        """
+        super(CPWSGIHTTPRequest, self).__init__(
+            server, conn, proxy_mode=True
+        )
+
+
+class CPWSGIServer(cheroot.wsgi.Server):
+    """Wrapper for cheroot.wsgi.Server.
+
+    cheroot has been designed to not reference CherryPy in any way,
+    so that it can be used in other frameworks and applications. Therefore,
+    we wrap it here, so we can set our own mount points from cherrypy.tree
+    and apply some attributes from config -> cherrypy.server -> wsgi.Server.
+    """
+
+    fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}'
+    version = fmt.format(**globals())
+
+    def __init__(self, server_adapter=cherrypy.server):
+        """Initialize CPWSGIServer instance.
+
+        Args:
+            server_adapter (cherrypy._cpserver.Server): ...
+        """
+        self.server_adapter = server_adapter
+        self.max_request_header_size = (
+            self.server_adapter.max_request_header_size or 0
+        )
+        self.max_request_body_size = (
+            self.server_adapter.max_request_body_size or 0
+        )
+
+        server_name = (self.server_adapter.socket_host or
+                       self.server_adapter.socket_file or
+                       None)
+
+        self.wsgi_version = self.server_adapter.wsgi_version
+
+        super(CPWSGIServer, self).__init__(
+            server_adapter.bind_addr, cherrypy.tree,
+            self.server_adapter.thread_pool,
+            server_name,
+            max=self.server_adapter.thread_pool_max,
+            request_queue_size=self.server_adapter.socket_queue_size,
+            timeout=self.server_adapter.socket_timeout,
+            shutdown_timeout=self.server_adapter.shutdown_timeout,
+            accepted_queue_size=self.server_adapter.accepted_queue_size,
+            accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
+            peercreds_enabled=self.server_adapter.peercreds,
+            peercreds_resolve_enabled=self.server_adapter.peercreds_resolve,
+        )
+        self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest
+
+        self.protocol = self.server_adapter.protocol_version
+        self.nodelay = self.server_adapter.nodelay
+
+        if sys.version_info >= (3, 0):
+            ssl_module = self.server_adapter.ssl_module or 'builtin'
+        else:
+            ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
+        if self.server_adapter.ssl_context:
+            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain,
+                self.server_adapter.ssl_ciphers)
+            self.ssl_adapter.context = self.server_adapter.ssl_context
+        elif self.server_adapter.ssl_certificate:
+            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
+            self.ssl_adapter = adapter_class(
+                self.server_adapter.ssl_certificate,
+                self.server_adapter.ssl_private_key,
+                self.server_adapter.ssl_certificate_chain,
+                self.server_adapter.ssl_ciphers)
+
+        self.stats['Enabled'] = getattr(
+            self.server_adapter, 'statistics', False)
+
+    def error_log(self, msg='', level=20, traceback=False):
+        """Write given message to the error log."""
+        cherrypy.engine.log(msg, level, traceback)
diff --git a/libraries/cherrypy/_helper.py b/libraries/cherrypy/_helper.py
new file mode 100644
index 00000000..314550cb
--- /dev/null
+++ b/libraries/cherrypy/_helper.py
@@ -0,0 +1,344 @@
+"""Helper functions for CP apps."""
+
+import six
+from six.moves import urllib
+
+from cherrypy._cpcompat import text_or_bytes
+
+import cherrypy
+
+
+def expose(func=None, alias=None):
+    """Expose the function or class.
+
+    Optionally provide an alias or set of aliases.
+    """
+    def expose_(func):
+        func.exposed = True
+        if alias is not None:
+            if isinstance(alias, text_or_bytes):
+                parents[alias.replace('.', '_')] = func
+            else:
+                for a in alias:
+                    parents[a.replace('.', '_')] = func
+        return func
+
+    import sys
+    import types
+    decoratable_types = types.FunctionType, types.MethodType, type,
+    if six.PY2:
+        # Old-style classes are type types.ClassType.
+        decoratable_types += types.ClassType,
+    if isinstance(func, decoratable_types):
+        if alias is None:
+            # @expose
+            func.exposed = True
+            return func
+        else:
+            # func = expose(func, alias)
+            parents = sys._getframe(1).f_locals
+            return expose_(func)
+    elif func is None:
+        if alias is None:
+            # @expose()
+            parents = sys._getframe(1).f_locals
+            return expose_
+        else:
+            # @expose(alias="alias") or
+            # @expose(alias=["alias1", "alias2"])
+            parents = sys._getframe(1).f_locals
+            return expose_
+    else:
+        # @expose("alias") or
+        # @expose(["alias1", "alias2"])
+        parents = sys._getframe(1).f_locals
+        alias = func
+        return expose_
+
+
+def popargs(*args, **kwargs):
+    """Decorate _cp_dispatch.
+
+    (cherrypy.dispatch.Dispatcher.dispatch_method_name)
+
+    Optional keyword argument: handler=(Object or Function)
+
+    Provides a _cp_dispatch function that pops off path segments into
+    cherrypy.request.params under the names specified.  The dispatch
+    is then forwarded on to the next vpath element.
+
+    Note that any existing (and exposed) member function of the class that
+    popargs is applied to will override that value of the argument.  For
+    instance, if you have a method named "list" on the class decorated with
+    popargs, then accessing "/list" will call that function instead of popping
+    it off as the requested parameter.  This restriction applies to all
+    _cp_dispatch functions.  The only way around this restriction is to create
+    a "blank class" whose only function is to provide _cp_dispatch.
+
+    If there are path elements after the arguments, or more arguments
+    are requested than are available in the vpath, then the 'handler'
+    keyword argument specifies the next object to handle the parameterized
+    request.  If handler is not specified or is None, then self is used.
+    If handler is a function rather than an instance, then that function
+    will be called with the args specified and the return value from that
+    function used as the next object INSTEAD of adding the parameters to
+    cherrypy.request.args.
+
+    This decorator may be used in one of two ways:
+
+    As a class decorator:
+    @cherrypy.popargs('year', 'month', 'day')
+    class Blog:
+        def index(self, year=None, month=None, day=None):
+            #Process the parameters here; any url like
+            #/, /2009, /2009/12, or /2009/12/31
+            #will fill in the appropriate parameters.
+
+        def create(self):
+            #This link will still be available at /create.  Defined functions
+            #take precedence over arguments.
+
+    Or as a member of a class:
+    class Blog:
+        _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
+        #...
+
+    The handler argument may be used to mix arguments with built in functions.
+    For instance, the following setup allows different activities at the
+    day, month, and year level:
+
+    class DayHandler:
+        def index(self, year, month, day):
+            #Do something with this day; probably list entries
+
+        def delete(self, year, month, day):
+            #Delete all entries for this day
+
+    @cherrypy.popargs('day', handler=DayHandler())
+    class MonthHandler:
+        def index(self, year, month):
+            #Do something with this month; probably list entries
+
+        def delete(self, year, month):
+            #Delete all entries for this month
+
+    @cherrypy.popargs('month', handler=MonthHandler())
+    class YearHandler:
+        def index(self, year):
+            #Do something with this year
+
+        #...
+
+    @cherrypy.popargs('year', handler=YearHandler())
+    class Root:
+        def index(self):
+            #...
+
+    """
+    # Since keyword arg comes after *args, we have to process it ourselves
+    # for lower versions of python.
+
+    handler = None
+    handler_call = False
+    for k, v in kwargs.items():
+        if k == 'handler':
+            handler = v
+        else:
+            tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'"
+            raise TypeError(tm.format(k))
+
+    import inspect
+
+    if handler is not None \
+            and (hasattr(handler, '__call__') or inspect.isclass(handler)):
+        handler_call = True
+
+    def decorated(cls_or_self=None, vpath=None):
+        if inspect.isclass(cls_or_self):
+            # cherrypy.popargs is a class decorator
+            cls = cls_or_self
+            name = cherrypy.dispatch.Dispatcher.dispatch_method_name
+            setattr(cls, name, decorated)
+            return cls
+
+        # We're in the actual function
+        self = cls_or_self
+        parms = {}
+        for arg in args:
+            if not vpath:
+                break
+            parms[arg] = vpath.pop(0)
+
+        if handler is not None:
+            if handler_call:
+                return handler(**parms)
+            else:
+                cherrypy.request.params.update(parms)
+                return handler
+
+        cherrypy.request.params.update(parms)
+
+        # If we are the ultimate handler, then to prevent our _cp_dispatch
+        # from being called again, we will resolve remaining elements through
+        # getattr() directly.
+        if vpath:
+            return getattr(self, vpath.pop(0), None)
+        else:
+            return self
+
+    return decorated
+
+
+def url(path='', qs='', script_name=None, base=None, relative=None):
+    """Create an absolute URL for the given path.
+
+    If 'path' starts with a slash ('/'), this will return
+        (base + script_name + path + qs).
+    If it does not start with a slash, this returns
+        (base + script_name [+ request.path_info] + path + qs).
+
+    If script_name is None, cherrypy.request will be used
+    to find a script_name, if available.
+
+    If base is None, cherrypy.request.base will be used (if available).
+    Note that you can use cherrypy.tools.proxy to change this.
+
+    Finally, note that this function can be used to obtain an absolute URL
+    for the current request path (minus the querystring) by passing no args.
+    If you call url(qs=cherrypy.request.query_string), you should get the
+    original browser URL (assuming no internal redirections).
+
+    If relative is None or not provided, request.app.relative_urls will
+    be used (if available, else False). If False, the output will be an
+    absolute URL (including the scheme, host, vhost, and script_name).
+    If True, the output will instead be a URL that is relative to the
+    current request path, perhaps including '..' atoms. If relative is
+    the string 'server', the output will instead be a URL that is
+    relative to the server root; i.e., it will start with a slash.
+    """
+    if isinstance(qs, (tuple, list, dict)):
+        qs = urllib.parse.urlencode(qs)
+    if qs:
+        qs = '?' + qs
+
+    if cherrypy.request.app:
+        if not path.startswith('/'):
+            # Append/remove trailing slash from path_info as needed
+            # (this is to support mistyped URL's without redirecting;
+            # if you want to redirect, use tools.trailing_slash).
+            pi = cherrypy.request.path_info
+            if cherrypy.request.is_index is True:
+                if not pi.endswith('/'):
+                    pi = pi + '/'
+            elif cherrypy.request.is_index is False:
+                if pi.endswith('/') and pi != '/':
+                    pi = pi[:-1]
+
+            if path == '':
+                path = pi
+            else:
+                path = urllib.parse.urljoin(pi, path)
+
+        if script_name is None:
+            script_name = cherrypy.request.script_name
+        if base is None:
+            base = cherrypy.request.base
+
+        newurl = base + script_name + normalize_path(path) + qs
+    else:
+        # No request.app (we're being called outside a request).
+        # We'll have to guess the base from server.* attributes.
+        # This will produce very different results from the above
+        # if you're using vhosts or tools.proxy.
+        if base is None:
+            base = cherrypy.server.base()
+
+        path = (script_name or '') + path
+        newurl = base + normalize_path(path) + qs
+
+    # At this point, we should have a fully-qualified absolute URL.
+
+    if relative is None:
+        relative = getattr(cherrypy.request.app, 'relative_urls', False)
+
+    # See http://www.ietf.org/rfc/rfc2396.txt
+    if relative == 'server':
+        # "A relative reference beginning with a single slash character is
+        # termed an absolute-path reference, as defined by <abs_path>..."
+        # This is also sometimes called "server-relative".
+        newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
+    elif relative:
+        # "A relative reference that does not begin with a scheme name
+        # or a slash character is termed a relative-path reference."
+        old = url(relative=False).split('/')[:-1]
+        new = newurl.split('/')
+        while old and new:
+            a, b = old[0], new[0]
+            if a != b:
+                break
+            old.pop(0)
+            new.pop(0)
+        new = (['..'] * len(old)) + new
+        newurl = '/'.join(new)
+
+    return newurl
+
+
+def normalize_path(path):
+    """Resolve given path from relative into absolute form."""
+    if './' not in path:
+        return path
+
+    # Normalize the URL by removing ./ and ../
+    atoms = []
+    for atom in path.split('/'):
+        if atom == '.':
+            pass
+        elif atom == '..':
+            # Don't pop from empty list
+            # (i.e. ignore redundant '..')
+            if atoms:
+                atoms.pop()
+        elif atom:
+            atoms.append(atom)
+
+    newpath = '/'.join(atoms)
+    # Preserve leading '/'
+    if path.startswith('/'):
+        newpath = '/' + newpath
+
+    return newpath
+
+
+####
+# Inlined from jaraco.classes 1.4.3
+# Ref #1673
+class _ClassPropertyDescriptor(object):
+    """Descript for read-only class-based property.
+
+    Turns a classmethod-decorated func into a read-only property of that class
+    type (means the value cannot be set).
+    """
+
+    def __init__(self, fget, fset=None):
+        """Initialize a class property descriptor.
+
+        Instantiated by ``_helper.classproperty``.
+        """
+        self.fget = fget
+        self.fset = fset
+
+    def __get__(self, obj, klass=None):
+        """Return property value."""
+        if klass is None:
+            klass = type(obj)
+        return self.fget.__get__(obj, klass)()
+
+
+def classproperty(func):  # noqa: D401; irrelevant for properties
+    """Decorator like classmethod to implement a static class property."""
+    if not isinstance(func, (classmethod, staticmethod)):
+        func = classmethod(func)
+
+    return _ClassPropertyDescriptor(func)
+####
diff --git a/libraries/cherrypy/daemon.py b/libraries/cherrypy/daemon.py
new file mode 100644
index 00000000..74488c06
--- /dev/null
+++ b/libraries/cherrypy/daemon.py
@@ -0,0 +1,107 @@
+"""The CherryPy daemon."""
+
+import sys
+
+import cherrypy
+from cherrypy.process import plugins, servers
+from cherrypy import Application
+
+
+def start(configfiles=None, daemonize=False, environment=None,
+          fastcgi=False, scgi=False, pidfile=None, imports=None,
+          cgi=False):
+    """Subscribe all engine plugins and start the engine."""
+    sys.path = [''] + sys.path
+    for i in imports or []:
+        exec('import %s' % i)
+
+    for c in configfiles or []:
+        cherrypy.config.update(c)
+        # If there's only one app mounted, merge config into it.
+        if len(cherrypy.tree.apps) == 1:
+            for app in cherrypy.tree.apps.values():
+                if isinstance(app, Application):
+                    app.merge(c)
+
+    engine = cherrypy.engine
+
+    if environment is not None:
+        cherrypy.config.update({'environment': environment})
+
+    # Only daemonize if asked to.
+    if daemonize:
+        # Don't print anything to stdout/sterr.
+        cherrypy.config.update({'log.screen': False})
+        plugins.Daemonizer(engine).subscribe()
+
+    if pidfile:
+        plugins.PIDFile(engine, pidfile).subscribe()
+
+    if hasattr(engine, 'signal_handler'):
+        engine.signal_handler.subscribe()
+    if hasattr(engine, 'console_control_handler'):
+        engine.console_control_handler.subscribe()
+
+    if (fastcgi and (scgi or cgi)) or (scgi and cgi):
+        cherrypy.log.error('You may only specify one of the cgi, fastcgi, and '
+                           'scgi options.', 'ENGINE')
+        sys.exit(1)
+    elif fastcgi or scgi or cgi:
+        # Turn off autoreload when using *cgi.
+        cherrypy.config.update({'engine.autoreload.on': False})
+        # Turn off the default HTTP server (which is subscribed by default).
+        cherrypy.server.unsubscribe()
+
+        addr = cherrypy.server.bind_addr
+        cls = (
+            servers.FlupFCGIServer if fastcgi else
+            servers.FlupSCGIServer if scgi else
+            servers.FlupCGIServer
+        )
+        f = cls(application=cherrypy.tree, bindAddress=addr)
+        s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
+        s.subscribe()
+
+    # Always start the engine; this will start all other services
+    try:
+        engine.start()
+    except Exception:
+        # Assume the error has been logged already via bus.log.
+        sys.exit(1)
+    else:
+        engine.block()
+
+
+def run():
+    """Run cherryd CLI."""
+    from optparse import OptionParser
+
+    p = OptionParser()
+    p.add_option('-c', '--config', action='append', dest='config',
+                 help='specify config file(s)')
+    p.add_option('-d', action='store_true', dest='daemonize',
+                 help='run the server as a daemon')
+    p.add_option('-e', '--environment', dest='environment', default=None,
+                 help='apply the given config environment')
+    p.add_option('-f', action='store_true', dest='fastcgi',
+                 help='start a fastcgi server instead of the default HTTP '
+                      'server')
+    p.add_option('-s', action='store_true', dest='scgi',
+                 help='start a scgi server instead of the default HTTP server')
+    p.add_option('-x', action='store_true', dest='cgi',
+                 help='start a cgi server instead of the default HTTP server')
+    p.add_option('-i', '--import', action='append', dest='imports',
+                 help='specify modules to import')
+    p.add_option('-p', '--pidfile', dest='pidfile', default=None,
+                 help='store the process id in the given file')
+    p.add_option('-P', '--Path', action='append', dest='Path',
+                 help='add the given paths to sys.path')
+    options, args = p.parse_args()
+
+    if options.Path:
+        for p in options.Path:
+            sys.path.insert(0, p)
+
+    start(options.config, options.daemonize,
+          options.environment, options.fastcgi, options.scgi,
+          options.pidfile, options.imports, options.cgi)
diff --git a/libraries/cherrypy/favicon.ico b/libraries/cherrypy/favicon.ico
new file mode 100644
index 0000000000000000000000000000000000000000..f0d7e61badad3f332cf1e663efb97c0b5be80f5e
GIT binary patch
literal 1406
zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K
zC3>Y)vZNK!rhR_zJd<bU|M}kMoO{0Cd!GB;KkoU0NLVT=2)RAxa?lm%Cxjr;TL_un
z3VorFg&_WomX;PQBmFpaDwn?M(PWhuk)2hAmSzH76vOeACShffNm)rfo~iK|`@7<l
z97Rw-F{L$?MC3~`w{s_5nul444-L&tm<D=KQCCfMeH|u7X&CxBQCVF}Ku!kswmEpk
zq+ns~j;2N&v4wf)=_FFu*od~b4Gs0p1m~oYTv$M3V++>tfiw$m?BBmX0|pFW;J|@s
zYHBiQ&>#j69?Xy-Ll`=AD8q&gWBBmlj2JNjEiElZjvUFTQKJ|=dNgCkjA889v5Xrx
z4sC61baZqWKYlzDCQM-B#EDFrGznc@T_#VSjGmqzQ>IK|>eQ)Bn>G!7eSHiJ446KB
zIx}X>VCKx37#bQfYt}4g&z{YkIdhmhcP>UoM$DTxkNNZGvtYpjjE#+1xNspRCMGOe
zw1~xv7h`H_%915ZSh{p6%a$!;`SRtgSh0eYD_62=)hf))%vim8HEY(aVeQ(rtXsDZ
zb8~anuV0Uag#{ZnY+&QYjaXV*vT4&MHgDdHm6a7+wrpYR)~#&YwvFxEx3go%4tDO`
z$*x_y*u8r<d-m*M@7}%a+qVyEYisuJ-_L;q2e7fR!PeFmJ3BiL9z4jQLx-@px99NT
z!yGws1P2EPjvhVAv17+Le*8F&j*gr-aRMhNCr+L`Dg2pJoIZV;GiT1=?Cgw-iwmx<
zuDH3m;qLCv*|TTy@bJLX(-SW*FV3Ai$NBT;xp3hE-rn9^ym*mImoDMs<HP04m$`D~
z3ckL+T)ldgYuB!E{rYwM{QS6a;|4cx-sIM;Tim{V8-IU)?%cUUKtKS2fq~q;dzYY~
zAnx6}M{sa3At52$zki?5&``p{!Uzu!Cn6$($jC^dqN0e7jwU81hS=Cx;^N|nkB=uI
zA%O=E9`NwtLmoYP#N)@0NlZ*6DJh93Po9vRoJ>ke3QwOtB{embw6rwR)6;qO>=_vu
z89aafoEI-%keQi@R4V1=%a>$jW%26OE3&h*$;rv#_3PK<=H`-@mq&hnK5yQ<p`f6E
zw{PF_?%g}yzkkn%4<9HjEac<Ij}#RZQCwV1Nl6K%rKOaWl~G<^&ZkeG`26`ZU%q^y
zqN0MYU%&G0+czpJE2*lgqPn`8nwlDFYis%b{X2Dab=23_)6mc$%v2*yO-(d6HzS*U
z0dgG$&-ej}LNBpc{SO)MBTEYMVzF4hjZi6d5R#;wK*i!-5`O<pW+8GyxA@oY?c};D
z_D~ayM<VcE>T(K__B|98+X@Zp^73MZjvW!HsVT|~sc)O^ZGM)}O{A)-)@n=bmFdz?
zRaLc>D=T%Qr>f`&r)>x2Kb1tH)^h|wLs?1GQ@HOR^ik=lq13yT$@Z=qojU#WZ-LIg
Kbp8+jKgeIP@z;_7

literal 0
HcmV?d00001

diff --git a/libraries/cherrypy/lib/__init__.py b/libraries/cherrypy/lib/__init__.py
new file mode 100644
index 00000000..f815f76a
--- /dev/null
+++ b/libraries/cherrypy/lib/__init__.py
@@ -0,0 +1,96 @@
+"""CherryPy Library."""
+
+
+def is_iterator(obj):
+    """Detect if the object provided implements the iterator protocol.
+
+    (i.e. like a generator).
+
+    This will return False for objects which are iterable,
+    but not iterators themselves.
+    """
+    from types import GeneratorType
+    if isinstance(obj, GeneratorType):
+        return True
+    elif not hasattr(obj, '__iter__'):
+        return False
+    else:
+        # Types which implement the protocol must return themselves when
+        # invoking 'iter' upon them.
+        return iter(obj) is obj
+
+
+def is_closable_iterator(obj):
+    """Detect if the given object is both closable and iterator."""
+    # Not an iterator.
+    if not is_iterator(obj):
+        return False
+
+    # A generator - the easiest thing to deal with.
+    import inspect
+    if inspect.isgenerator(obj):
+        return True
+
+    # A custom iterator. Look for a close method...
+    if not (hasattr(obj, 'close') and callable(obj.close)):
+        return False
+
+    #  ... which doesn't require any arguments.
+    try:
+        inspect.getcallargs(obj.close)
+    except TypeError:
+        return False
+    else:
+        return True
+
+
+class file_generator(object):
+    """Yield the given input (a file object) in chunks (default 64k).
+
+    (Core)
+    """
+
+    def __init__(self, input, chunkSize=65536):
+        """Initialize file_generator with file ``input`` for chunked access."""
+        self.input = input
+        self.chunkSize = chunkSize
+
+    def __iter__(self):
+        """Return iterator."""
+        return self
+
+    def __next__(self):
+        """Return next chunk of file."""
+        chunk = self.input.read(self.chunkSize)
+        if chunk:
+            return chunk
+        else:
+            if hasattr(self.input, 'close'):
+                self.input.close()
+            raise StopIteration()
+    next = __next__
+
+
+def file_generator_limited(fileobj, count, chunk_size=65536):
+    """Yield the given file object in chunks.
+
+    Stopps after `count` bytes has been emitted.
+    Default chunk size is 64kB. (Core)
+    """
+    remaining = count
+    while remaining > 0:
+        chunk = fileobj.read(min(chunk_size, remaining))
+        chunklen = len(chunk)
+        if chunklen == 0:
+            return
+        remaining -= chunklen
+        yield chunk
+
+
+def set_vary_header(response, header_name):
+    """Add a Vary header to a response."""
+    varies = response.headers.get('Vary', '')
+    varies = [x.strip() for x in varies.split(',') if x.strip()]
+    if header_name not in varies:
+        varies.append(header_name)
+    response.headers['Vary'] = ', '.join(varies)
diff --git a/libraries/cherrypy/lib/auth_basic.py b/libraries/cherrypy/lib/auth_basic.py
new file mode 100644
index 00000000..ad379a26
--- /dev/null
+++ b/libraries/cherrypy/lib/auth_basic.py
@@ -0,0 +1,120 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+"""HTTP Basic Authentication tool.
+
+This module provides a CherryPy 3.x tool which implements
+the server-side of HTTP Basic Access Authentication, as described in
+:rfc:`2617`.
+
+Example usage, using the built-in checkpassword_dict function which uses a dict
+as the credentials store::
+
+    userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'}
+    checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict)
+    basic_auth = {'tools.auth_basic.on': True,
+                  'tools.auth_basic.realm': 'earth',
+                  'tools.auth_basic.checkpassword': checkpassword,
+                  'tools.auth_basic.accept_charset': 'UTF-8',
+    }
+    app_config = { '/' : basic_auth }
+
+"""
+
+import binascii
+import unicodedata
+import base64
+
+import cherrypy
+from cherrypy._cpcompat import ntou, tonative
+
+
+__author__ = 'visteya'
+__date__ = 'April 2009'
+
+
+def checkpassword_dict(user_password_dict):
+    """Returns a checkpassword function which checks credentials
+    against a dictionary of the form: {username : password}.
+
+    If you want a simple dictionary-based authentication scheme, use
+    checkpassword_dict(my_credentials_dict) as the value for the
+    checkpassword argument to basic_auth().
+    """
+    def checkpassword(realm, user, password):
+        p = user_password_dict.get(user)
+        return p and p == password or False
+
+    return checkpassword
+
+
+def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
+    """A CherryPy tool which hooks at before_handler to perform
+    HTTP Basic Access Authentication, as specified in :rfc:`2617`
+    and :rfc:`7617`.
+
+    If the request has an 'authorization' header with a 'Basic' scheme, this
+    tool attempts to authenticate the credentials supplied in that header.  If
+    the request has no 'authorization' header, or if it does but the scheme is
+    not 'Basic', or if authentication fails, the tool sends a 401 response with
+    a 'WWW-Authenticate' Basic header.
+
+    realm
+        A string containing the authentication realm.
+
+    checkpassword
+        A callable which checks the authentication credentials.
+        Its signature is checkpassword(realm, username, password). where
+        username and password are the values obtained from the request's
+        'authorization' header.  If authentication succeeds, checkpassword
+        returns True, else it returns False.
+
+    """
+
+    fallback_charset = 'ISO-8859-1'
+
+    if '"' in realm:
+        raise ValueError('Realm cannot contain the " (quote) character.')
+    request = cherrypy.serving.request
+
+    auth_header = request.headers.get('authorization')
+    if auth_header is not None:
+        # split() error, base64.decodestring() error
+        msg = 'Bad Request'
+        with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg):
+            scheme, params = auth_header.split(' ', 1)
+            if scheme.lower() == 'basic':
+                charsets = accept_charset, fallback_charset
+                decoded_params = base64.b64decode(params.encode('ascii'))
+                decoded_params = _try_decode(decoded_params, charsets)
+                decoded_params = ntou(decoded_params)
+                decoded_params = unicodedata.normalize('NFC', decoded_params)
+                decoded_params = tonative(decoded_params)
+                username, password = decoded_params.split(':', 1)
+                if checkpassword(realm, username, password):
+                    if debug:
+                        cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
+                    request.login = username
+                    return  # successful authentication
+
+    charset = accept_charset.upper()
+    charset_declaration = (
+        (', charset="%s"' % charset)
+        if charset != fallback_charset
+        else ''
+    )
+    # Respond with 401 status and a WWW-Authenticate header
+    cherrypy.serving.response.headers['www-authenticate'] = (
+        'Basic realm="%s"%s' % (realm, charset_declaration)
+    )
+    raise cherrypy.HTTPError(
+        401, 'You are not authorized to access that resource')
+
+
+def _try_decode(subject, charsets):
+    for charset in charsets[:-1]:
+        try:
+            return tonative(subject, charset)
+        except ValueError:
+            pass
+    return tonative(subject, charsets[-1])
diff --git a/libraries/cherrypy/lib/auth_digest.py b/libraries/cherrypy/lib/auth_digest.py
new file mode 100644
index 00000000..9b4f55c8
--- /dev/null
+++ b/libraries/cherrypy/lib/auth_digest.py
@@ -0,0 +1,464 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+"""HTTP Digest Authentication tool.
+
+An implementation of the server-side of HTTP Digest Access
+Authentication, which is described in :rfc:`2617`.
+
+Example usage, using the built-in get_ha1_dict_plain function which uses a dict
+of plaintext passwords as the credentials store::
+
+    userpassdict = {'alice' : '4x5istwelve'}
+    get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict)
+    digest_auth = {'tools.auth_digest.on': True,
+                   'tools.auth_digest.realm': 'wonderland',
+                   'tools.auth_digest.get_ha1': get_ha1,
+                   'tools.auth_digest.key': 'a565c27146791cfb',
+                   'tools.auth_digest.accept_charset': 'UTF-8',
+    }
+    app_config = { '/' : digest_auth }
+"""
+
+import time
+import functools
+from hashlib import md5
+
+from six.moves.urllib.request import parse_http_list, parse_keqv_list
+
+import cherrypy
+from cherrypy._cpcompat import ntob, tonative
+
+
+__author__ = 'visteya'
+__date__ = 'April 2009'
+
+
+def md5_hex(s):
+    return md5(ntob(s, 'utf-8')).hexdigest()
+
+
+qop_auth = 'auth'
+qop_auth_int = 'auth-int'
+valid_qops = (qop_auth, qop_auth_int)
+
+valid_algorithms = ('MD5', 'MD5-sess')
+
+FALLBACK_CHARSET = 'ISO-8859-1'
+DEFAULT_CHARSET = 'UTF-8'
+
+
+def TRACE(msg):
+    cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
+
+# Three helper functions for users of the tool, providing three variants
+# of get_ha1() functions for three different kinds of credential stores.
+
+
+def get_ha1_dict_plain(user_password_dict):
+    """Returns a get_ha1 function which obtains a plaintext password from a
+    dictionary of the form: {username : password}.
+
+    If you want a simple dictionary-based authentication scheme, with plaintext
+    passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
+    get_ha1 argument to digest_auth().
+    """
+    def get_ha1(realm, username):
+        password = user_password_dict.get(username)
+        if password:
+            return md5_hex('%s:%s:%s' % (username, realm, password))
+        return None
+
+    return get_ha1
+
+
+def get_ha1_dict(user_ha1_dict):
+    """Returns a get_ha1 function which obtains a HA1 password hash from a
+    dictionary of the form: {username : HA1}.
+
+    If you want a dictionary-based authentication scheme, but with
+    pre-computed HA1 hashes instead of plain-text passwords, use
+    get_ha1_dict(my_userha1_dict) as the value for the get_ha1
+    argument to digest_auth().
+    """
+    def get_ha1(realm, username):
+        return user_ha1_dict.get(username)
+
+    return get_ha1
+
+
+def get_ha1_file_htdigest(filename):
+    """Returns a get_ha1 function which obtains a HA1 password hash from a
+    flat file with lines of the same format as that produced by the Apache
+    htdigest utility. For example, for realm 'wonderland', username 'alice',
+    and password '4x5istwelve', the htdigest line would be::
+
+        alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c
+
+    If you want to use an Apache htdigest file as the credentials store,
+    then use get_ha1_file_htdigest(my_htdigest_file) as the value for the
+    get_ha1 argument to digest_auth().  It is recommended that the filename
+    argument be an absolute path, to avoid problems.
+    """
+    def get_ha1(realm, username):
+        result = None
+        f = open(filename, 'r')
+        for line in f:
+            u, r, ha1 = line.rstrip().split(':')
+            if u == username and r == realm:
+                result = ha1
+                break
+        f.close()
+        return result
+
+    return get_ha1
+
+
+def synthesize_nonce(s, key, timestamp=None):
+    """Synthesize a nonce value which resists spoofing and can be checked
+    for staleness. Returns a string suitable as the value for 'nonce' in
+    the www-authenticate header.
+
+    s
+        A string related to the resource, such as the hostname of the server.
+
+    key
+        A secret string known only to the server.
+
+    timestamp
+        An integer seconds-since-the-epoch timestamp
+
+    """
+    if timestamp is None:
+        timestamp = int(time.time())
+    h = md5_hex('%s:%s:%s' % (timestamp, s, key))
+    nonce = '%s:%s' % (timestamp, h)
+    return nonce
+
+
+def H(s):
+    """The hash function H"""
+    return md5_hex(s)
+
+
+def _try_decode_header(header, charset):
+    global FALLBACK_CHARSET
+
+    for enc in (charset, FALLBACK_CHARSET):
+        try:
+            return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc)
+        except ValueError as ve:
+            last_err = ve
+    else:
+        raise last_err
+
+
+class HttpDigestAuthorization(object):
+    """
+    Parses a Digest Authorization header and performs
+    re-calculation of the digest.
+    """
+
+    scheme = 'digest'
+
+    def errmsg(self, s):
+        return 'Digest Authorization header: %s' % s
+
+    @classmethod
+    def matches(cls, header):
+        scheme, _, _ = header.partition(' ')
+        return scheme.lower() == cls.scheme
+
+    def __init__(
+        self, auth_header, http_method,
+        debug=False, accept_charset=DEFAULT_CHARSET[:],
+    ):
+        self.http_method = http_method
+        self.debug = debug
+
+        if not self.matches(auth_header):
+            raise ValueError('Authorization scheme is not "Digest"')
+
+        self.auth_header = _try_decode_header(auth_header, accept_charset)
+
+        scheme, params = self.auth_header.split(' ', 1)
+
+        # make a dict of the params
+        items = parse_http_list(params)
+        paramsd = parse_keqv_list(items)
+
+        self.realm = paramsd.get('realm')
+        self.username = paramsd.get('username')
+        self.nonce = paramsd.get('nonce')
+        self.uri = paramsd.get('uri')
+        self.method = paramsd.get('method')
+        self.response = paramsd.get('response')  # the response digest
+        self.algorithm = paramsd.get('algorithm', 'MD5').upper()
+        self.cnonce = paramsd.get('cnonce')
+        self.opaque = paramsd.get('opaque')
+        self.qop = paramsd.get('qop')  # qop
+        self.nc = paramsd.get('nc')  # nonce count
+
+        # perform some correctness checks
+        if self.algorithm not in valid_algorithms:
+            raise ValueError(
+                self.errmsg("Unsupported value for algorithm: '%s'" %
+                            self.algorithm))
+
+        has_reqd = (
+            self.username and
+            self.realm and
+            self.nonce and
+            self.uri and
+            self.response
+        )
+        if not has_reqd:
+            raise ValueError(
+                self.errmsg('Not all required parameters are present.'))
+
+        if self.qop:
+            if self.qop not in valid_qops:
+                raise ValueError(
+                    self.errmsg("Unsupported value for qop: '%s'" % self.qop))
+            if not (self.cnonce and self.nc):
+                raise ValueError(
+                    self.errmsg('If qop is sent then '
+                                'cnonce and nc MUST be present'))
+        else:
+            if self.cnonce or self.nc:
+                raise ValueError(
+                    self.errmsg('If qop is not sent, '
+                                'neither cnonce nor nc can be present'))
+
+    def __str__(self):
+        return 'authorization : %s' % self.auth_header
+
+    def validate_nonce(self, s, key):
+        """Validate the nonce.
+        Returns True if nonce was generated by synthesize_nonce() and the
+        timestamp is not spoofed, else returns False.
+
+        s
+            A string related to the resource, such as the hostname of
+            the server.
+
+        key
+            A secret string known only to the server.
+
+        Both s and key must be the same values which were used to synthesize
+        the nonce we are trying to validate.
+        """
+        try:
+            timestamp, hashpart = self.nonce.split(':', 1)
+            s_timestamp, s_hashpart = synthesize_nonce(
+                s, key, timestamp).split(':', 1)
+            is_valid = s_hashpart == hashpart
+            if self.debug:
+                TRACE('validate_nonce: %s' % is_valid)
+            return is_valid
+        except ValueError:  # split() error
+            pass
+        return False
+
+    def is_nonce_stale(self, max_age_seconds=600):
+        """Returns True if a validated nonce is stale. The nonce contains a
+        timestamp in plaintext and also a secure hash of the timestamp.
+        You should first validate the nonce to ensure the plaintext
+        timestamp is not spoofed.
+        """
+        try:
+            timestamp, hashpart = self.nonce.split(':', 1)
+            if int(timestamp) + max_age_seconds > int(time.time()):
+                return False
+        except ValueError:  # int() error
+            pass
+        if self.debug:
+            TRACE('nonce is stale')
+        return True
+
+    def HA2(self, entity_body=''):
+        """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
+        # RFC 2617 3.2.2.3
+        # If the "qop" directive's value is "auth" or is unspecified,
+        # then A2 is:
+        #    A2 = method ":" digest-uri-value
+        #
+        # If the "qop" value is "auth-int", then A2 is:
+        #    A2 = method ":" digest-uri-value ":" H(entity-body)
+        if self.qop is None or self.qop == 'auth':
+            a2 = '%s:%s' % (self.http_method, self.uri)
+        elif self.qop == 'auth-int':
+            a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
+        else:
+            # in theory, this should never happen, since I validate qop in
+            # __init__()
+            raise ValueError(self.errmsg('Unrecognized value for qop!'))
+        return H(a2)
+
+    def request_digest(self, ha1, entity_body=''):
+        """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
+
+        ha1
+            The HA1 string obtained from the credentials store.
+
+        entity_body
+            If 'qop' is set to 'auth-int', then A2 includes a hash
+            of the "entity body".  The entity body is the part of the
+            message which follows the HTTP headers. See :rfc:`2617` section
+            4.3.  This refers to the entity the user agent sent in the
+            request which has the Authorization header. Typically GET
+            requests don't have an entity, and POST requests do.
+
+        """
+        ha2 = self.HA2(entity_body)
+        # Request-Digest -- RFC 2617 3.2.2.1
+        if self.qop:
+            req = '%s:%s:%s:%s:%s' % (
+                self.nonce, self.nc, self.cnonce, self.qop, ha2)
+        else:
+            req = '%s:%s' % (self.nonce, ha2)
+
+        # RFC 2617 3.2.2.2
+        #
+        # If the "algorithm" directive's value is "MD5" or is unspecified,
+        # then A1 is:
+        #    A1 = unq(username-value) ":" unq(realm-value) ":" passwd
+        #
+        # If the "algorithm" directive's value is "MD5-sess", then A1 is
+        # calculated only once - on the first request by the client following
+        # receipt of a WWW-Authenticate challenge from the server.
+        # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
+        #         ":" unq(nonce-value) ":" unq(cnonce-value)
+        if self.algorithm == 'MD5-sess':
+            ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce))
+
+        digest = H('%s:%s' % (ha1, req))
+        return digest
+
+
+def _get_charset_declaration(charset):
+    global FALLBACK_CHARSET
+    charset = charset.upper()
+    return (
+        (', charset="%s"' % charset)
+        if charset != FALLBACK_CHARSET
+        else ''
+    )
+
+
+def www_authenticate(
+    realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
+    stale=False, accept_charset=DEFAULT_CHARSET[:],
+):
+    """Constructs a WWW-Authenticate header for Digest authentication."""
+    if qop not in valid_qops:
+        raise ValueError("Unsupported value for qop: '%s'" % qop)
+    if algorithm not in valid_algorithms:
+        raise ValueError("Unsupported value for algorithm: '%s'" % algorithm)
+
+    HEADER_PATTERN = (
+        'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s'
+    )
+
+    if nonce is None:
+        nonce = synthesize_nonce(realm, key)
+
+    stale_param = ', stale="true"' if stale else ''
+
+    charset_declaration = _get_charset_declaration(accept_charset)
+
+    return HEADER_PATTERN % (
+        realm, nonce, algorithm, qop, stale_param, charset_declaration,
+    )
+
+
+def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
+    """A CherryPy tool that hooks at before_handler to perform
+    HTTP Digest Access Authentication, as specified in :rfc:`2617`.
+
+    If the request has an 'authorization' header with a 'Digest' scheme,
+    this tool authenticates the credentials supplied in that header.
+    If the request has no 'authorization' header, or if it does but the
+    scheme is not "Digest", or if authentication fails, the tool sends
+    a 401 response with a 'WWW-Authenticate' Digest header.
+
+    realm
+        A string containing the authentication realm.
+
+    get_ha1
+        A callable that looks up a username in a credentials store
+        and returns the HA1 string, which is defined in the RFC to be
+        MD5(username : realm : password).  The function's signature is:
+        ``get_ha1(realm, username)``
+        where username is obtained from the request's 'authorization' header.
+        If username is not found in the credentials store, get_ha1() returns
+        None.
+
+    key
+        A secret string known only to the server, used in the synthesis
+        of nonces.
+
+    """
+    request = cherrypy.serving.request
+
+    auth_header = request.headers.get('authorization')
+
+    respond_401 = functools.partial(
+        _respond_401, realm, key, accept_charset, debug)
+
+    if not HttpDigestAuthorization.matches(auth_header or ''):
+        respond_401()
+
+    msg = 'The Authorization header could not be parsed.'
+    with cherrypy.HTTPError.handle(ValueError, 400, msg):
+        auth = HttpDigestAuthorization(
+            auth_header, request.method,
+            debug=debug, accept_charset=accept_charset,
+        )
+
+    if debug:
+        TRACE(str(auth))
+
+    if not auth.validate_nonce(realm, key):
+        respond_401()
+
+    ha1 = get_ha1(realm, auth.username)
+
+    if ha1 is None:
+        respond_401()
+
+    # note that for request.body to be available we need to
+    # hook in at before_handler, not on_start_resource like
+    # 3.1.x digest_auth does.
+    digest = auth.request_digest(ha1, entity_body=request.body)
+    if digest != auth.response:
+        respond_401()
+
+    # authenticated
+    if debug:
+        TRACE('digest matches auth.response')
+    # Now check if nonce is stale.
+    # The choice of ten minutes' lifetime for nonce is somewhat
+    # arbitrary
+    if auth.is_nonce_stale(max_age_seconds=600):
+        respond_401(stale=True)
+
+    request.login = auth.username
+    if debug:
+        TRACE('authentication of %s successful' % auth.username)
+
+
+def _respond_401(realm, key, accept_charset, debug, **kwargs):
+    """
+    Respond with 401 status and a WWW-Authenticate header
+    """
+    header = www_authenticate(
+        realm, key,
+        accept_charset=accept_charset,
+        **kwargs
+    )
+    if debug:
+        TRACE(header)
+    cherrypy.serving.response.headers['WWW-Authenticate'] = header
+    raise cherrypy.HTTPError(
+        401, 'You are not authorized to access that resource')
diff --git a/libraries/cherrypy/lib/caching.py b/libraries/cherrypy/lib/caching.py
new file mode 100644
index 00000000..fed325a6
--- /dev/null
+++ b/libraries/cherrypy/lib/caching.py
@@ -0,0 +1,482 @@
+"""
+CherryPy implements a simple caching system as a pluggable Tool. This tool
+tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
+yet, but it's probably good enough for most sites.
+
+In general, GET responses are cached (along with selecting headers) and, if
+another request arrives for the same resource, the caching Tool will return 304
+Not Modified if possible, or serve the cached response otherwise. It also sets
+request.cached to True if serving a cached representation, and sets
+request.cacheable to False (so it doesn't get cached again).
+
+If POST, PUT, or DELETE requests are made for a cached resource, they
+invalidate (delete) any cached response.
+
+Usage
+=====
+
+Configuration file example::
+
+    [/]
+    tools.caching.on = True
+    tools.caching.delay = 3600
+
+You may use a class other than the default
+:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
+entry ``cache_class``; supply the full dotted name of the replacement class
+as the config value. It must implement the basic methods ``get``, ``put``,
+``delete``, and ``clear``.
+
+You may set any attribute, including overriding methods, on the cache
+instance by providing them in config. The above sets the
+:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
+"""
+
+import datetime
+import sys
+import threading
+import time
+
+import six
+
+import cherrypy
+from cherrypy.lib import cptools, httputil
+from cherrypy._cpcompat import Event
+
+
+class Cache(object):
+
+    """Base class for Cache implementations."""
+
+    def get(self):
+        """Return the current variant if in the cache, else None."""
+        raise NotImplemented
+
+    def put(self, obj, size):
+        """Store the current variant in the cache."""
+        raise NotImplemented
+
+    def delete(self):
+        """Remove ALL cached variants of the current resource."""
+        raise NotImplemented
+
+    def clear(self):
+        """Reset the cache to its initial, empty state."""
+        raise NotImplemented
+
+
+# ------------------------------ Memory Cache ------------------------------- #
+class AntiStampedeCache(dict):
+
+    """A storage system for cached items which reduces stampede collisions."""
+
+    def wait(self, key, timeout=5, debug=False):
+        """Return the cached value for the given key, or None.
+
+        If timeout is not None, and the value is already
+        being calculated by another thread, wait until the given timeout has
+        elapsed. If the value is available before the timeout expires, it is
+        returned. If not, None is returned, and a sentinel placed in the cache
+        to signal other threads to wait.
+
+        If timeout is None, no waiting is performed nor sentinels used.
+        """
+        value = self.get(key)
+        if isinstance(value, Event):
+            if timeout is None:
+                # Ignore the other thread and recalc it ourselves.
+                if debug:
+                    cherrypy.log('No timeout', 'TOOLS.CACHING')
+                return None
+
+            # Wait until it's done or times out.
+            if debug:
+                cherrypy.log('Waiting up to %s seconds' %
+                             timeout, 'TOOLS.CACHING')
+            value.wait(timeout)
+            if value.result is not None:
+                # The other thread finished its calculation. Use it.
+                if debug:
+                    cherrypy.log('Result!', 'TOOLS.CACHING')
+                return value.result
+            # Timed out. Stick an Event in the slot so other threads wait
+            # on this one to finish calculating the value.
+            if debug:
+                cherrypy.log('Timed out', 'TOOLS.CACHING')
+            e = threading.Event()
+            e.result = None
+            dict.__setitem__(self, key, e)
+
+            return None
+        elif value is None:
+            # Stick an Event in the slot so other threads wait
+            # on this one to finish calculating the value.
+            if debug:
+                cherrypy.log('Timed out', 'TOOLS.CACHING')
+            e = threading.Event()
+            e.result = None
+            dict.__setitem__(self, key, e)
+        return value
+
+    def __setitem__(self, key, value):
+        """Set the cached value for the given key."""
+        existing = self.get(key)
+        dict.__setitem__(self, key, value)
+        if isinstance(existing, Event):
+            # Set Event.result so other threads waiting on it have
+            # immediate access without needing to poll the cache again.
+            existing.result = value
+            existing.set()
+
+
+class MemoryCache(Cache):
+
+    """An in-memory cache for varying response content.
+
+    Each key in self.store is a URI, and each value is an AntiStampedeCache.
+    The response for any given URI may vary based on the values of
+    "selecting request headers"; that is, those named in the Vary
+    response header. We assume the list of header names to be constant
+    for each URI throughout the lifetime of the application, and store
+    that list in ``self.store[uri].selecting_headers``.
+
+    The items contained in ``self.store[uri]`` have keys which are tuples of
+    request header values (in the same order as the names in its
+    selecting_headers), and values which are the actual responses.
+    """
+
+    maxobjects = 1000
+    """The maximum number of cached objects; defaults to 1000."""
+
+    maxobj_size = 100000
+    """The maximum size of each cached object in bytes; defaults to 100 KB."""
+
+    maxsize = 10000000
+    """The maximum size of the entire cache in bytes; defaults to 10 MB."""
+
+    delay = 600
+    """Seconds until the cached content expires; defaults to 600 (10 minutes).
+    """
+
+    antistampede_timeout = 5
+    """Seconds to wait for other threads to release a cache lock."""
+
+    expire_freq = 0.1
+    """Seconds to sleep between cache expiration sweeps."""
+
+    debug = False
+
+    def __init__(self):
+        self.clear()
+
+        # Run self.expire_cache in a separate daemon thread.
+        t = threading.Thread(target=self.expire_cache, name='expire_cache')
+        self.expiration_thread = t
+        t.daemon = True
+        t.start()
+
+    def clear(self):
+        """Reset the cache to its initial, empty state."""
+        self.store = {}
+        self.expirations = {}
+        self.tot_puts = 0
+        self.tot_gets = 0
+        self.tot_hist = 0
+        self.tot_expires = 0
+        self.tot_non_modified = 0
+        self.cursize = 0
+
+    def expire_cache(self):
+        """Continuously examine cached objects, expiring stale ones.
+
+        This function is designed to be run in its own daemon thread,
+        referenced at ``self.expiration_thread``.
+        """
+        # It's possible that "time" will be set to None
+        # arbitrarily, so we check "while time" to avoid exceptions.
+        # See tickets #99 and #180 for more information.
+        while time:
+            now = time.time()
+            # Must make a copy of expirations so it doesn't change size
+            # during iteration
+            items = list(six.iteritems(self.expirations))
+            for expiration_time, objects in items:
+                if expiration_time <= now:
+                    for obj_size, uri, sel_header_values in objects:
+                        try:
+                            del self.store[uri][tuple(sel_header_values)]
+                            self.tot_expires += 1
+                            self.cursize -= obj_size
+                        except KeyError:
+                            # the key may have been deleted elsewhere
+                            pass
+                    del self.expirations[expiration_time]
+            time.sleep(self.expire_freq)
+
+    def get(self):
+        """Return the current variant if in the cache, else None."""
+        request = cherrypy.serving.request
+        self.tot_gets += 1
+
+        uri = cherrypy.url(qs=request.query_string)
+        uricache = self.store.get(uri)
+        if uricache is None:
+            return None
+
+        header_values = [request.headers.get(h, '')
+                         for h in uricache.selecting_headers]
+        variant = uricache.wait(key=tuple(sorted(header_values)),
+                                timeout=self.antistampede_timeout,
+                                debug=self.debug)
+        if variant is not None:
+            self.tot_hist += 1
+        return variant
+
+    def put(self, variant, size):
+        """Store the current variant in the cache."""
+        request = cherrypy.serving.request
+        response = cherrypy.serving.response
+
+        uri = cherrypy.url(qs=request.query_string)
+        uricache = self.store.get(uri)
+        if uricache is None:
+            uricache = AntiStampedeCache()
+            uricache.selecting_headers = [
+                e.value for e in response.headers.elements('Vary')]
+            self.store[uri] = uricache
+
+        if len(self.store) < self.maxobjects:
+            total_size = self.cursize + size
+
+            # checks if there's space for the object
+            if (size < self.maxobj_size and total_size < self.maxsize):
+                # add to the expirations list
+                expiration_time = response.time + self.delay
+                bucket = self.expirations.setdefault(expiration_time, [])
+                bucket.append((size, uri, uricache.selecting_headers))
+
+                # add to the cache
+                header_values = [request.headers.get(h, '')
+                                 for h in uricache.selecting_headers]
+                uricache[tuple(sorted(header_values))] = variant
+                self.tot_puts += 1
+                self.cursize = total_size
+
+    def delete(self):
+        """Remove ALL cached variants of the current resource."""
+        uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
+        self.store.pop(uri, None)
+
+
+def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
+    """Try to obtain cached output. If fresh enough, raise HTTPError(304).
+
+    If POST, PUT, or DELETE:
+        * invalidates (deletes) any cached response for this resource
+        * sets request.cached = False
+        * sets request.cacheable = False
+
+    else if a cached copy exists:
+        * sets request.cached = True
+        * sets request.cacheable = False
+        * sets response.headers to the cached values
+        * checks the cached Last-Modified response header against the
+          current If-(Un)Modified-Since request headers; raises 304
+          if necessary.
+        * sets response.status and response.body to the cached values
+        * returns True
+
+    otherwise:
+        * sets request.cached = False
+        * sets request.cacheable = True
+        * returns False
+    """
+    request = cherrypy.serving.request
+    response = cherrypy.serving.response
+
+    if not hasattr(cherrypy, '_cache'):
+        # Make a process-wide Cache object.
+        cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
+
+        # Take all remaining kwargs and set them on the Cache object.
+        for k, v in kwargs.items():
+            setattr(cherrypy._cache, k, v)
+        cherrypy._cache.debug = debug
+
+    # POST, PUT, DELETE should invalidate (delete) the cached copy.
+    # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
+    if request.method in invalid_methods:
+        if debug:
+            cherrypy.log('request.method %r in invalid_methods %r' %
+                         (request.method, invalid_methods), 'TOOLS.CACHING')
+        cherrypy._cache.delete()
+        request.cached = False
+        request.cacheable = False
+        return False
+
+    if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
+        request.cached = False
+        request.cacheable = True
+        return False
+
+    cache_data = cherrypy._cache.get()
+    request.cached = bool(cache_data)
+    request.cacheable = not request.cached
+    if request.cached:
+        # Serve the cached copy.
+        max_age = cherrypy._cache.delay
+        for v in [e.value for e in request.headers.elements('Cache-Control')]:
+            atoms = v.split('=', 1)
+            directive = atoms.pop(0)
+            if directive == 'max-age':
+                if len(atoms) != 1 or not atoms[0].isdigit():
+                    raise cherrypy.HTTPError(
+                        400, 'Invalid Cache-Control header')
+                max_age = int(atoms[0])
+                break
+            elif directive == 'no-cache':
+                if debug:
+                    cherrypy.log(
+                        'Ignoring cache due to Cache-Control: no-cache',
+                        'TOOLS.CACHING')
+                request.cached = False
+                request.cacheable = True
+                return False
+
+        if debug:
+            cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
+        s, h, b, create_time = cache_data
+        age = int(response.time - create_time)
+        if (age > max_age):
+            if debug:
+                cherrypy.log('Ignoring cache due to age > %d' % max_age,
+                             'TOOLS.CACHING')
+            request.cached = False
+            request.cacheable = True
+            return False
+
+        # Copy the response headers. See
+        # https://github.com/cherrypy/cherrypy/issues/721.
+        response.headers = rh = httputil.HeaderMap()
+        for k in h:
+            dict.__setitem__(rh, k, dict.__getitem__(h, k))
+
+        # Add the required Age header
+        response.headers['Age'] = str(age)
+
+        try:
+            # Note that validate_since depends on a Last-Modified header;
+            # this was put into the cached copy, and should have been
+            # resurrected just above (response.headers = cache_data[1]).
+            cptools.validate_since()
+        except cherrypy.HTTPRedirect:
+            x = sys.exc_info()[1]
+            if x.status == 304:
+                cherrypy._cache.tot_non_modified += 1
+            raise
+
+        # serve it & get out from the request
+        response.status = s
+        response.body = b
+    else:
+        if debug:
+            cherrypy.log('request is not cached', 'TOOLS.CACHING')
+    return request.cached
+
+
+def tee_output():
+    """Tee response output to cache storage. Internal."""
+    # Used by CachingTool by attaching to request.hooks
+
+    request = cherrypy.serving.request
+    if 'no-store' in request.headers.values('Cache-Control'):
+        return
+
+    def tee(body):
+        """Tee response.body into a list."""
+        if ('no-cache' in response.headers.values('Pragma') or
+                'no-store' in response.headers.values('Cache-Control')):
+            for chunk in body:
+                yield chunk
+            return
+
+        output = []
+        for chunk in body:
+            output.append(chunk)
+            yield chunk
+
+        # Save the cache data, but only if the body isn't empty.
+        # e.g. a 304 Not Modified on a static file response will
+        # have an empty body.
+        # If the body is empty, delete the cache because it
+        # contains a stale Threading._Event object that will
+        # stall all consecutive requests until the _Event times
+        # out
+        body = b''.join(output)
+        if not body:
+            cherrypy._cache.delete()
+        else:
+            cherrypy._cache.put((response.status, response.headers or {},
+                                 body, response.time), len(body))
+
+    response = cherrypy.serving.response
+    response.body = tee(response.body)
+
+
+def expires(secs=0, force=False, debug=False):
+    """Tool for influencing cache mechanisms using the 'Expires' header.
+
+    secs
+        Must be either an int or a datetime.timedelta, and indicates the
+        number of seconds between response.time and when the response should
+        expire. The 'Expires' header will be set to response.time + secs.
+        If secs is zero, the 'Expires' header is set one year in the past, and
+        the following "cache prevention" headers are also set:
+
+            * Pragma: no-cache
+            * Cache-Control': no-cache, must-revalidate
+
+    force
+        If False, the following headers are checked:
+
+            * Etag
+            * Last-Modified
+            * Age
+            * Expires
+
+        If any are already present, none of the above response headers are set.
+
+    """
+
+    response = cherrypy.serving.response
+    headers = response.headers
+
+    cacheable = False
+    if not force:
+        # some header names that indicate that the response can be cached
+        for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
+            if indicator in headers:
+                cacheable = True
+                break
+
+    if not cacheable and not force:
+        if debug:
+            cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
+    else:
+        if debug:
+            cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
+        if isinstance(secs, datetime.timedelta):
+            secs = (86400 * secs.days) + secs.seconds
+
+        if secs == 0:
+            if force or ('Pragma' not in headers):
+                headers['Pragma'] = 'no-cache'
+            if cherrypy.serving.request.protocol >= (1, 1):
+                if force or 'Cache-Control' not in headers:
+                    headers['Cache-Control'] = 'no-cache, must-revalidate'
+            # Set an explicit Expires date in the past.
+            expiry = httputil.HTTPDate(1169942400.0)
+        else:
+            expiry = httputil.HTTPDate(response.time + secs)
+        if force or 'Expires' not in headers:
+            headers['Expires'] = expiry
diff --git a/libraries/cherrypy/lib/covercp.py b/libraries/cherrypy/lib/covercp.py
new file mode 100644
index 00000000..0bafca13
--- /dev/null
+++ b/libraries/cherrypy/lib/covercp.py
@@ -0,0 +1,391 @@
+"""Code-coverage tools for CherryPy.
+
+To use this module, or the coverage tools in the test suite,
+you need to download 'coverage.py', either Gareth Rees' `original
+implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
+or Ned Batchelder's `enhanced version:
+<http://www.nedbatchelder.com/code/modules/coverage.html>`_
+
+To turn on coverage tracing, use the following code::
+
+    cherrypy.engine.subscribe('start', covercp.start)
+
+DO NOT subscribe anything on the 'start_thread' channel, as previously
+recommended. Calling start once in the main thread should be sufficient
+to start coverage on all threads. Calling start again in each thread
+effectively clears any coverage data gathered up to that point.
+
+Run your code, then use the ``covercp.serve()`` function to browse the
+results in a web browser. If you run this module from the command line,
+it will call ``serve()`` for you.
+"""
+
+import re
+import sys
+import cgi
+import os
+import os.path
+
+from six.moves import urllib
+
+import cherrypy
+
+
+localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
+
+the_coverage = None
+try:
+    from coverage import coverage
+    the_coverage = coverage(data_file=localFile)
+
+    def start():
+        the_coverage.start()
+except ImportError:
+    # Setting the_coverage to None will raise errors
+    # that need to be trapped downstream.
+    the_coverage = None
+
+    import warnings
+    warnings.warn(
+        'No code coverage will be performed; '
+        'coverage.py could not be imported.')
+
+    def start():
+        pass
+start.priority = 20
+
+TEMPLATE_MENU = """<html>
+<head>
+    <title>CherryPy Coverage Menu</title>
+    <style>
+        body {font: 9pt Arial, serif;}
+        #tree {
+            font-size: 8pt;
+            font-family: Andale Mono, monospace;
+            white-space: pre;
+            }
+        #tree a:active, a:focus {
+            background-color: black;
+            padding: 1px;
+            color: white;
+            border: 0px solid #9999FF;
+            -moz-outline-style: none;
+            }
+        .fail { color: red;}
+        .pass { color: #888;}
+        #pct { text-align: right;}
+        h3 {
+            font-size: small;
+            font-weight: bold;
+            font-style: italic;
+            margin-top: 5px;
+            }
+        input { border: 1px solid #ccc; padding: 2px; }
+        .directory {
+            color: #933;
+            font-style: italic;
+            font-weight: bold;
+            font-size: 10pt;
+            }
+        .file {
+            color: #400;
+            }
+        a { text-decoration: none; }
+        #crumbs {
+            color: white;
+            font-size: 8pt;
+            font-family: Andale Mono, monospace;
+            width: 100%;
+            background-color: black;
+            }
+        #crumbs a {
+            color: #f88;
+            }
+        #options {
+            line-height: 2.3em;
+            border: 1px solid black;
+            background-color: #eee;
+            padding: 4px;
+            }
+        #exclude {
+            width: 100%;
+            margin-bottom: 3px;
+            border: 1px solid #999;
+            }
+        #submit {
+            background-color: black;
+            color: white;
+            border: 0;
+            margin-bottom: -9px;
+            }
+    </style>
+</head>
+<body>
+<h2>CherryPy Coverage</h2>"""
+
+TEMPLATE_FORM = """
+<div id="options">
+<form action='menu' method=GET>
+    <input type='hidden' name='base' value='%(base)s' />
+    Show percentages
+    <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
+    Hide files over
+    <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
+    Exclude files matching<br />
+    <input type='text' id='exclude' name='exclude'
+     value='%(exclude)s' size='20' />
+    <br />
+
+    <input type='submit' value='Change view' id="submit"/>
+</form>
+</div>"""
+
+TEMPLATE_FRAMESET = """<html>
+<head><title>CherryPy coverage data</title></head>
+<frameset cols='250, 1*'>
+    <frame src='menu?base=%s' />
+    <frame name='main' src='' />
+</frameset>
+</html>
+"""
+
+TEMPLATE_COVERAGE = """<html>
+<head>
+    <title>Coverage for %(name)s</title>
+    <style>
+        h2 { margin-bottom: .25em; }
+        p { margin: .25em; }
+        .covered { color: #000; background-color: #fff; }
+        .notcovered { color: #fee; background-color: #500; }
+        .excluded { color: #00f; background-color: #fff; }
+         table .covered, table .notcovered, table .excluded
+             { font-family: Andale Mono, monospace;
+               font-size: 10pt; white-space: pre; }
+
+         .lineno { background-color: #eee;}
+         .notcovered .lineno { background-color: #000;}
+         table { border-collapse: collapse;
+    </style>
+</head>
+<body>
+<h2>%(name)s</h2>
+<p>%(fullpath)s</p>
+<p>Coverage: %(pc)s%%</p>"""
+
+TEMPLATE_LOC_COVERED = """<tr class="covered">
+    <td class="lineno">%s&nbsp;</td>
+    <td>%s</td>
+</tr>\n"""
+TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
+    <td class="lineno">%s&nbsp;</td>
+    <td>%s</td>
+</tr>\n"""
+TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
+    <td class="lineno">%s&nbsp;</td>
+    <td>%s</td>
+</tr>\n"""
+
+TEMPLATE_ITEM = (
+    "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
+)
+
+
+def _percent(statements, missing):
+    s = len(statements)
+    e = s - len(missing)
+    if s > 0:
+        return int(round(100.0 * e / s))
+    return 0
+
+
+def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
+                 coverage=the_coverage):
+
+    # Show the directory name and any of our children
+    dirs = [k for k, v in root.items() if v]
+    dirs.sort()
+    for name in dirs:
+        newpath = os.path.join(path, name)
+
+        if newpath.lower().startswith(base):
+            relpath = newpath[len(base):]
+            yield '| ' * relpath.count(os.sep)
+            yield (
+                "<a class='directory' "
+                "href='menu?base=%s&exclude=%s'>%s</a>\n" %
+                (newpath, urllib.parse.quote_plus(exclude), name)
+            )
+
+        for chunk in _show_branch(
+            root[name], base, newpath, pct, showpct,
+            exclude, coverage=coverage
+        ):
+            yield chunk
+
+    # Now list the files
+    if path.lower().startswith(base):
+        relpath = path[len(base):]
+        files = [k for k, v in root.items() if not v]
+        files.sort()
+        for name in files:
+            newpath = os.path.join(path, name)
+
+            pc_str = ''
+            if showpct:
+                try:
+                    _, statements, _, missing, _ = coverage.analysis2(newpath)
+                except Exception:
+                    # Yes, we really want to pass on all errors.
+                    pass
+                else:
+                    pc = _percent(statements, missing)
+                    pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
+                    if pc < float(pct) or pc == -1:
+                        pc_str = "<span class='fail'>%s</span>" % pc_str
+                    else:
+                        pc_str = "<span class='pass'>%s</span>" % pc_str
+
+            yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
+                                   pc_str, newpath, name)
+
+
+def _skip_file(path, exclude):
+    if exclude:
+        return bool(re.search(exclude, path))
+
+
+def _graft(path, tree):
+    d = tree
+
+    p = path
+    atoms = []
+    while True:
+        p, tail = os.path.split(p)
+        if not tail:
+            break
+        atoms.append(tail)
+    atoms.append(p)
+    if p != '/':
+        atoms.append('/')
+
+    atoms.reverse()
+    for node in atoms:
+        if node:
+            d = d.setdefault(node, {})
+
+
+def get_tree(base, exclude, coverage=the_coverage):
+    """Return covered module names as a nested dict."""
+    tree = {}
+    runs = coverage.data.executed_files()
+    for path in runs:
+        if not _skip_file(path, exclude) and not os.path.isdir(path):
+            _graft(path, tree)
+    return tree
+
+
+class CoverStats(object):
+
+    def __init__(self, coverage, root=None):
+        self.coverage = coverage
+        if root is None:
+            # Guess initial depth. Files outside this path will not be
+            # reachable from the web interface.
+            root = os.path.dirname(cherrypy.__file__)
+        self.root = root
+
+    @cherrypy.expose
+    def index(self):
+        return TEMPLATE_FRAMESET % self.root.lower()
+
+    @cherrypy.expose
+    def menu(self, base='/', pct='50', showpct='',
+             exclude=r'python\d\.\d|test|tut\d|tutorial'):
+
+        # The coverage module uses all-lower-case names.
+        base = base.lower().rstrip(os.sep)
+
+        yield TEMPLATE_MENU
+        yield TEMPLATE_FORM % locals()
+
+        # Start by showing links for parent paths
+        yield "<div id='crumbs'>"
+        path = ''
+        atoms = base.split(os.sep)
+        atoms.pop()
+        for atom in atoms:
+            path += atom + os.sep
+            yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
+                   % (path, urllib.parse.quote_plus(exclude), atom, os.sep))
+        yield '</div>'
+
+        yield "<div id='tree'>"
+
+        # Then display the tree
+        tree = get_tree(base, exclude, self.coverage)
+        if not tree:
+            yield '<p>No modules covered.</p>'
+        else:
+            for chunk in _show_branch(tree, base, '/', pct,
+                                      showpct == 'checked', exclude,
+                                      coverage=self.coverage):
+                yield chunk
+
+        yield '</div>'
+        yield '</body></html>'
+
+    def annotated_file(self, filename, statements, excluded, missing):
+        source = open(filename, 'r')
+        buffer = []
+        for lineno, line in enumerate(source.readlines()):
+            lineno += 1
+            line = line.strip('\n\r')
+            empty_the_buffer = True
+            if lineno in excluded:
+                template = TEMPLATE_LOC_EXCLUDED
+            elif lineno in missing:
+                template = TEMPLATE_LOC_NOT_COVERED
+            elif lineno in statements:
+                template = TEMPLATE_LOC_COVERED
+            else:
+                empty_the_buffer = False
+                buffer.append((lineno, line))
+            if empty_the_buffer:
+                for lno, pastline in buffer:
+                    yield template % (lno, cgi.escape(pastline))
+                buffer = []
+                yield template % (lineno, cgi.escape(line))
+
+    @cherrypy.expose
+    def report(self, name):
+        filename, statements, excluded, missing, _ = self.coverage.analysis2(
+            name)
+        pc = _percent(statements, missing)
+        yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
+                                       fullpath=name,
+                                       pc=pc)
+        yield '<table>\n'
+        for line in self.annotated_file(filename, statements, excluded,
+                                        missing):
+            yield line
+        yield '</table>'
+        yield '</body>'
+        yield '</html>'
+
+
+def serve(path=localFile, port=8080, root=None):
+    if coverage is None:
+        raise ImportError('The coverage module could not be imported.')
+    from coverage import coverage
+    cov = coverage(data_file=path)
+    cov.load()
+
+    cherrypy.config.update({'server.socket_port': int(port),
+                            'server.thread_pool': 10,
+                            'environment': 'production',
+                            })
+    cherrypy.quickstart(CoverStats(cov, root))
+
+
+if __name__ == '__main__':
+    serve(*tuple(sys.argv[1:]))
diff --git a/libraries/cherrypy/lib/cpstats.py b/libraries/cherrypy/lib/cpstats.py
new file mode 100644
index 00000000..ae9f7475
--- /dev/null
+++ b/libraries/cherrypy/lib/cpstats.py
@@ -0,0 +1,696 @@
+"""CPStats, a package for collecting and reporting on program statistics.
+
+Overview
+========
+
+Statistics about program operation are an invaluable monitoring and debugging
+tool. Unfortunately, the gathering and reporting of these critical values is
+usually ad-hoc. This package aims to add a centralized place for gathering
+statistical performance data, a structure for recording that data which
+provides for extrapolation of that data into more useful information,
+and a method of serving that data to both human investigators and
+monitoring software. Let's examine each of those in more detail.
+
+Data Gathering
+--------------
+
+Just as Python's `logging` module provides a common importable for gathering
+and sending messages, performance statistics would benefit from a similar
+common mechanism, and one that does *not* require each package which wishes
+to collect stats to import a third-party module. Therefore, we choose to
+re-use the `logging` module by adding a `statistics` object to it.
+
+That `logging.statistics` object is a nested dict. It is not a custom class,
+because that would:
+
+ 1. require libraries and applications to import a third-party module in
+    order to participate
+ 2. inhibit innovation in extrapolation approaches and in reporting tools, and
+ 3. be slow.
+
+There are, however, some specifications regarding the structure of the dict.::
+
+   {
+     +----"SQLAlchemy": {
+     |        "Inserts": 4389745,
+     |        "Inserts per Second":
+     |            lambda s: s["Inserts"] / (time() - s["Start"]),
+     |  C +---"Table Statistics": {
+     |  o |        "widgets": {-----------+
+   N |  l |            "Rows": 1.3M,      | Record
+   a |  l |            "Inserts": 400,    |
+   m |  e |        },---------------------+
+   e |  c |        "froobles": {
+   s |  t |            "Rows": 7845,
+   p |  i |            "Inserts": 0,
+   a |  o |        },
+   c |  n +---},
+   e |        "Slow Queries":
+     |            [{"Query": "SELECT * FROM widgets;",
+     |              "Processing Time": 47.840923343,
+     |              },
+     |             ],
+     +----},
+   }
+
+The `logging.statistics` dict has four levels. The topmost level is nothing
+more than a set of names to introduce modularity, usually along the lines of
+package names. If the SQLAlchemy project wanted to participate, for example,
+it might populate the item `logging.statistics['SQLAlchemy']`, whose value
+would be a second-layer dict we call a "namespace". Namespaces help multiple
+packages to avoid collisions over key names, and make reports easier to read,
+to boot. The maintainers of SQLAlchemy should feel free to use more than one
+namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case
+or other syntax constraints on the namespace names; they should be chosen
+to be maximally readable by humans (neither too short nor too long).
+
+Each namespace, then, is a dict of named statistical values, such as
+'Requests/sec' or 'Uptime'. You should choose names which will look
+good on a report: spaces and capitalization are just fine.
+
+In addition to scalars, values in a namespace MAY be a (third-layer)
+dict, or a list, called a "collection". For example, the CherryPy
+:class:`StatsTool` keeps track of what each request is doing (or has most
+recently done) in a 'Requests' collection, where each key is a thread ID; each
+value in the subdict MUST be a fourth dict (whew!) of statistical data about
+each thread. We call each subdict in the collection a "record". Similarly,
+the :class:`StatsTool` also keeps a list of slow queries, where each record
+contains data about each slow query, in order.
+
+Values in a namespace or record may also be functions, which brings us to:
+
+Extrapolation
+-------------
+
+The collection of statistical data needs to be fast, as close to unnoticeable
+as possible to the host program. That requires us to minimize I/O, for example,
+but in Python it also means we need to minimize function calls. So when you
+are designing your namespace and record values, try to insert the most basic
+scalar values you already have on hand.
+
+When it comes time to report on the gathered data, however, we usually have
+much more freedom in what we can calculate. Therefore, whenever reporting
+tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
+of `logging.statistics` for reporting, they first call
+`extrapolate_statistics` (passing the whole `statistics` dict as the only
+argument). This makes a deep copy of the statistics dict so that the
+reporting tool can both iterate over it and even change it without harming
+the original. But it also expands any functions in the dict by calling them.
+For example, you might have a 'Current Time' entry in the namespace with the
+value "lambda scope: time.time()". The "scope" parameter is the current
+namespace dict (or record, if we're currently expanding one of those
+instead), allowing you access to existing static entries. If you're truly
+evil, you can even modify more than one entry at a time.
+
+However, don't try to calculate an entry and then use its value in further
+extrapolations; the order in which the functions are called is not guaranteed.
+This can lead to a certain amount of duplicated work (or a redesign of your
+schema), but that's better than complicating the spec.
+
+After the whole thing has been extrapolated, it's time for:
+
+Reporting
+---------
+
+The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
+it all, and then transforms it to HTML for easy viewing. Each namespace gets
+its own header and attribute table, plus an extra table for each collection.
+This is NOT part of the statistics specification; other tools can format how
+they like.
+
+You can control which columns are output and how they are formatted by updating
+StatsPage.formatting, which is a dict that mirrors the keys and nesting of
+`logging.statistics`. The difference is that, instead of data values, it has
+formatting values. Use None for a given key to indicate to the StatsPage that a
+given column should not be output. Use a string with formatting
+(such as '%.3f') to interpolate the value(s), or use a callable (such as
+lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
+mentioned in the formatting dict is output unchanged.
+
+Monitoring
+----------
+
+Although the HTML output takes pains to assign unique id's to each <td> with
+statistical data, you're probably better off fetching /cpstats/data, which
+outputs the whole (extrapolated) `logging.statistics` dict in JSON format.
+That is probably easier to parse, and doesn't have any formatting controls,
+so you get the "original" data in a consistently-serialized format.
+Note: there's no treatment yet for datetime objects. Try time.time() instead
+for now if you can. Nagios will probably thank you.
+
+Turning Collection Off
+----------------------
+
+It is recommended each namespace have an "Enabled" item which, if False,
+stops collection (but not reporting) of statistical data. Applications
+SHOULD provide controls to pause and resume collection by setting these
+entries to False or True, if present.
+
+
+Usage
+=====
+
+To collect statistics on CherryPy applications::
+
+    from cherrypy.lib import cpstats
+    appconfig['/']['tools.cpstats.on'] = True
+
+To collect statistics on your own code::
+
+    import logging
+    # Initialize the repository
+    if not hasattr(logging, 'statistics'): logging.statistics = {}
+    # Initialize my namespace
+    mystats = logging.statistics.setdefault('My Stuff', {})
+    # Initialize my namespace's scalars and collections
+    mystats.update({
+        'Enabled': True,
+        'Start Time': time.time(),
+        'Important Events': 0,
+        'Events/Second': lambda s: (
+            (s['Important Events'] / (time.time() - s['Start Time']))),
+        })
+    ...
+    for event in events:
+        ...
+        # Collect stats
+        if mystats.get('Enabled', False):
+            mystats['Important Events'] += 1
+
+To report statistics::
+
+    root.cpstats = cpstats.StatsPage()
+
+To format statistics reports::
+
+    See 'Reporting', above.
+
+"""
+
+import logging
+import os
+import sys
+import threading
+import time
+
+import six
+
+import cherrypy
+from cherrypy._cpcompat import json
+
+# ------------------------------- Statistics -------------------------------- #
+
+if not hasattr(logging, 'statistics'):
+    logging.statistics = {}
+
+
+def extrapolate_statistics(scope):
+    """Return an extrapolated copy of the given scope."""
+    c = {}
+    for k, v in list(scope.items()):
+        if isinstance(v, dict):
+            v = extrapolate_statistics(v)
+        elif isinstance(v, (list, tuple)):
+            v = [extrapolate_statistics(record) for record in v]
+        elif hasattr(v, '__call__'):
+            v = v(scope)
+        c[k] = v
+    return c
+
+
+# -------------------- CherryPy Applications Statistics --------------------- #
+
+appstats = logging.statistics.setdefault('CherryPy Applications', {})
+appstats.update({
+    'Enabled': True,
+    'Bytes Read/Request': lambda s: (
+        s['Total Requests'] and
+        (s['Total Bytes Read'] / float(s['Total Requests'])) or
+        0.0
+    ),
+    'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
+    'Bytes Written/Request': lambda s: (
+        s['Total Requests'] and
+        (s['Total Bytes Written'] / float(s['Total Requests'])) or
+        0.0
+    ),
+    'Bytes Written/Second': lambda s: (
+        s['Total Bytes Written'] / s['Uptime'](s)
+    ),
+    'Current Time': lambda s: time.time(),
+    'Current Requests': 0,
+    'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
+    'Server Version': cherrypy.__version__,
+    'Start Time': time.time(),
+    'Total Bytes Read': 0,
+    'Total Bytes Written': 0,
+    'Total Requests': 0,
+    'Total Time': 0,
+    'Uptime': lambda s: time.time() - s['Start Time'],
+    'Requests': {},
+})
+
+
+def proc_time(s):
+    return time.time() - s['Start Time']
+
+
+class ByteCountWrapper(object):
+
+    """Wraps a file-like object, counting the number of bytes read."""
+
+    def __init__(self, rfile):
+        self.rfile = rfile
+        self.bytes_read = 0
+
+    def read(self, size=-1):
+        data = self.rfile.read(size)
+        self.bytes_read += len(data)
+        return data
+
+    def readline(self, size=-1):
+        data = self.rfile.readline(size)
+        self.bytes_read += len(data)
+        return data
+
+    def readlines(self, sizehint=0):
+        # Shamelessly stolen from StringIO
+        total = 0
+        lines = []
+        line = self.readline()
+        while line:
+            lines.append(line)
+            total += len(line)
+            if 0 < sizehint <= total:
+                break
+            line = self.readline()
+        return lines
+
+    def close(self):
+        self.rfile.close()
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        data = self.rfile.next()
+        self.bytes_read += len(data)
+        return data
+
+
+def average_uriset_time(s):
+    return s['Count'] and (s['Sum'] / s['Count']) or 0
+
+
+def _get_threading_ident():
+    if sys.version_info >= (3, 3):
+        return threading.get_ident()
+    return threading._get_ident()
+
+
+class StatsTool(cherrypy.Tool):
+
+    """Record various information about the current request."""
+
+    def __init__(self):
+        cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
+
+    def _setup(self):
+        """Hook this tool into cherrypy.request.
+
+        The standard CherryPy request object will automatically call this
+        method when the tool is "turned on" in config.
+        """
+        if appstats.get('Enabled', False):
+            cherrypy.Tool._setup(self)
+            self.record_start()
+
+    def record_start(self):
+        """Record the beginning of a request."""
+        request = cherrypy.serving.request
+        if not hasattr(request.rfile, 'bytes_read'):
+            request.rfile = ByteCountWrapper(request.rfile)
+            request.body.fp = request.rfile
+
+        r = request.remote
+
+        appstats['Current Requests'] += 1
+        appstats['Total Requests'] += 1
+        appstats['Requests'][_get_threading_ident()] = {
+            'Bytes Read': None,
+            'Bytes Written': None,
+            # Use a lambda so the ip gets updated by tools.proxy later
+            'Client': lambda s: '%s:%s' % (r.ip, r.port),
+            'End Time': None,
+            'Processing Time': proc_time,
+            'Request-Line': request.request_line,
+            'Response Status': None,
+            'Start Time': time.time(),
+        }
+
+    def record_stop(
+            self, uriset=None, slow_queries=1.0, slow_queries_count=100,
+            debug=False, **kwargs):
+        """Record the end of a request."""
+        resp = cherrypy.serving.response
+        w = appstats['Requests'][_get_threading_ident()]
+
+        r = cherrypy.request.rfile.bytes_read
+        w['Bytes Read'] = r
+        appstats['Total Bytes Read'] += r
+
+        if resp.stream:
+            w['Bytes Written'] = 'chunked'
+        else:
+            cl = int(resp.headers.get('Content-Length', 0))
+            w['Bytes Written'] = cl
+            appstats['Total Bytes Written'] += cl
+
+        w['Response Status'] = getattr(
+            resp, 'output_status', None) or resp.status
+
+        w['End Time'] = time.time()
+        p = w['End Time'] - w['Start Time']
+        w['Processing Time'] = p
+        appstats['Total Time'] += p
+
+        appstats['Current Requests'] -= 1
+
+        if debug:
+            cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
+
+        if uriset:
+            rs = appstats.setdefault('URI Set Tracking', {})
+            r = rs.setdefault(uriset, {
+                'Min': None, 'Max': None, 'Count': 0, 'Sum': 0,
+                'Avg': average_uriset_time})
+            if r['Min'] is None or p < r['Min']:
+                r['Min'] = p
+            if r['Max'] is None or p > r['Max']:
+                r['Max'] = p
+            r['Count'] += 1
+            r['Sum'] += p
+
+        if slow_queries and p > slow_queries:
+            sq = appstats.setdefault('Slow Queries', [])
+            sq.append(w.copy())
+            if len(sq) > slow_queries_count:
+                sq.pop(0)
+
+
+cherrypy.tools.cpstats = StatsTool()
+
+
+# ---------------------- CherryPy Statistics Reporting ---------------------- #
+
+thisdir = os.path.abspath(os.path.dirname(__file__))
+
+missing = object()
+
+
+def locale_date(v):
+    return time.strftime('%c', time.gmtime(v))
+
+
+def iso_format(v):
+    return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
+
+
+def pause_resume(ns):
+    def _pause_resume(enabled):
+        pause_disabled = ''
+        resume_disabled = ''
+        if enabled:
+            resume_disabled = 'disabled="disabled" '
+        else:
+            pause_disabled = 'disabled="disabled" '
+        return """
+            <form action="pause" method="POST" style="display:inline">
+            <input type="hidden" name="namespace" value="%s" />
+            <input type="submit" value="Pause" %s/>
+            </form>
+            <form action="resume" method="POST" style="display:inline">
+            <input type="hidden" name="namespace" value="%s" />
+            <input type="submit" value="Resume" %s/>
+            </form>
+            """ % (ns, pause_disabled, ns, resume_disabled)
+    return _pause_resume
+
+
+class StatsPage(object):
+
+    formatting = {
+        'CherryPy Applications': {
+            'Enabled': pause_resume('CherryPy Applications'),
+            'Bytes Read/Request': '%.3f',
+            'Bytes Read/Second': '%.3f',
+            'Bytes Written/Request': '%.3f',
+            'Bytes Written/Second': '%.3f',
+            'Current Time': iso_format,
+            'Requests/Second': '%.3f',
+            'Start Time': iso_format,
+            'Total Time': '%.3f',
+            'Uptime': '%.3f',
+            'Slow Queries': {
+                'End Time': None,
+                'Processing Time': '%.3f',
+                'Start Time': iso_format,
+            },
+            'URI Set Tracking': {
+                'Avg': '%.3f',
+                'Max': '%.3f',
+                'Min': '%.3f',
+                'Sum': '%.3f',
+            },
+            'Requests': {
+                'Bytes Read': '%s',
+                'Bytes Written': '%s',
+                'End Time': None,
+                'Processing Time': '%.3f',
+                'Start Time': None,
+            },
+        },
+        'CherryPy WSGIServer': {
+            'Enabled': pause_resume('CherryPy WSGIServer'),
+            'Connections/second': '%.3f',
+            'Start time': iso_format,
+        },
+    }
+
+    @cherrypy.expose
+    def index(self):
+        # Transform the raw data into pretty output for HTML
+        yield """
+<html>
+<head>
+    <title>Statistics</title>
+<style>
+
+th, td {
+    padding: 0.25em 0.5em;
+    border: 1px solid #666699;
+}
+
+table {
+    border-collapse: collapse;
+}
+
+table.stats1 {
+    width: 100%;
+}
+
+table.stats1 th {
+    font-weight: bold;
+    text-align: right;
+    background-color: #CCD5DD;
+}
+
+table.stats2, h2 {
+    margin-left: 50px;
+}
+
+table.stats2 th {
+    font-weight: bold;
+    text-align: center;
+    background-color: #CCD5DD;
+}
+
+</style>
+</head>
+<body>
+"""
+        for title, scalars, collections in self.get_namespaces():
+            yield """
+<h1>%s</h1>
+
+<table class='stats1'>
+    <tbody>
+""" % title
+            for i, (key, value) in enumerate(scalars):
+                colnum = i % 3
+                if colnum == 0:
+                    yield """
+        <tr>"""
+                yield (
+                    """
+            <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
+                    vars()
+                )
+                if colnum == 2:
+                    yield """
+        </tr>"""
+
+            if colnum == 0:
+                yield """
+            <th></th><td></td>
+            <th></th><td></td>
+        </tr>"""
+            elif colnum == 1:
+                yield """
+            <th></th><td></td>
+        </tr>"""
+            yield """
+    </tbody>
+</table>"""
+
+            for subtitle, headers, subrows in collections:
+                yield """
+<h2>%s</h2>
+<table class='stats2'>
+    <thead>
+        <tr>""" % subtitle
+                for key in headers:
+                    yield """
+            <th>%s</th>""" % key
+                yield """
+        </tr>
+    </thead>
+    <tbody>"""
+                for subrow in subrows:
+                    yield """
+        <tr>"""
+                    for value in subrow:
+                        yield """
+            <td>%s</td>""" % value
+                    yield """
+        </tr>"""
+                yield """
+    </tbody>
+</table>"""
+        yield """
+</body>
+</html>
+"""
+
+    def get_namespaces(self):
+        """Yield (title, scalars, collections) for each namespace."""
+        s = extrapolate_statistics(logging.statistics)
+        for title, ns in sorted(s.items()):
+            scalars = []
+            collections = []
+            ns_fmt = self.formatting.get(title, {})
+            for k, v in sorted(ns.items()):
+                fmt = ns_fmt.get(k, {})
+                if isinstance(v, dict):
+                    headers, subrows = self.get_dict_collection(v, fmt)
+                    collections.append((k, ['ID'] + headers, subrows))
+                elif isinstance(v, (list, tuple)):
+                    headers, subrows = self.get_list_collection(v, fmt)
+                    collections.append((k, headers, subrows))
+                else:
+                    format = ns_fmt.get(k, missing)
+                    if format is None:
+                        # Don't output this column.
+                        continue
+                    if hasattr(format, '__call__'):
+                        v = format(v)
+                    elif format is not missing:
+                        v = format % v
+                    scalars.append((k, v))
+            yield title, scalars, collections
+
+    def get_dict_collection(self, v, formatting):
+        """Return ([headers], [rows]) for the given collection."""
+        # E.g., the 'Requests' dict.
+        headers = []
+        vals = six.itervalues(v)
+        for record in vals:
+            for k3 in record:
+                format = formatting.get(k3, missing)
+                if format is None:
+                    # Don't output this column.
+                    continue
+                if k3 not in headers:
+                    headers.append(k3)
+        headers.sort()
+
+        subrows = []
+        for k2, record in sorted(v.items()):
+            subrow = [k2]
+            for k3 in headers:
+                v3 = record.get(k3, '')
+                format = formatting.get(k3, missing)
+                if format is None:
+                    # Don't output this column.
+                    continue
+                if hasattr(format, '__call__'):
+                    v3 = format(v3)
+                elif format is not missing:
+                    v3 = format % v3
+                subrow.append(v3)
+            subrows.append(subrow)
+
+        return headers, subrows
+
+    def get_list_collection(self, v, formatting):
+        """Return ([headers], [subrows]) for the given collection."""
+        # E.g., the 'Slow Queries' list.
+        headers = []
+        for record in v:
+            for k3 in record:
+                format = formatting.get(k3, missing)
+                if format is None:
+                    # Don't output this column.
+                    continue
+                if k3 not in headers:
+                    headers.append(k3)
+        headers.sort()
+
+        subrows = []
+        for record in v:
+            subrow = []
+            for k3 in headers:
+                v3 = record.get(k3, '')
+                format = formatting.get(k3, missing)
+                if format is None:
+                    # Don't output this column.
+                    continue
+                if hasattr(format, '__call__'):
+                    v3 = format(v3)
+                elif format is not missing:
+                    v3 = format % v3
+                subrow.append(v3)
+            subrows.append(subrow)
+
+        return headers, subrows
+
+    if json is not None:
+        @cherrypy.expose
+        def data(self):
+            s = extrapolate_statistics(logging.statistics)
+            cherrypy.response.headers['Content-Type'] = 'application/json'
+            return json.dumps(s, sort_keys=True, indent=4)
+
+    @cherrypy.expose
+    def pause(self, namespace):
+        logging.statistics.get(namespace, {})['Enabled'] = False
+        raise cherrypy.HTTPRedirect('./')
+    pause.cp_config = {'tools.allow.on': True,
+                       'tools.allow.methods': ['POST']}
+
+    @cherrypy.expose
+    def resume(self, namespace):
+        logging.statistics.get(namespace, {})['Enabled'] = True
+        raise cherrypy.HTTPRedirect('./')
+    resume.cp_config = {'tools.allow.on': True,
+                        'tools.allow.methods': ['POST']}
diff --git a/libraries/cherrypy/lib/cptools.py b/libraries/cherrypy/lib/cptools.py
new file mode 100644
index 00000000..1c079634
--- /dev/null
+++ b/libraries/cherrypy/lib/cptools.py
@@ -0,0 +1,640 @@
+"""Functions for builtin CherryPy tools."""
+
+import logging
+import re
+from hashlib import md5
+
+import six
+from six.moves import urllib
+
+import cherrypy
+from cherrypy._cpcompat import text_or_bytes
+from cherrypy.lib import httputil as _httputil
+from cherrypy.lib import is_iterator
+
+
+#                     Conditional HTTP request support                     #
+
+def validate_etags(autotags=False, debug=False):
+    """Validate the current ETag against If-Match, If-None-Match headers.
+
+    If autotags is True, an ETag response-header value will be provided
+    from an MD5 hash of the response body (unless some other code has
+    already provided an ETag header). If False (the default), the ETag
+    will not be automatic.
+
+    WARNING: the autotags feature is not designed for URL's which allow
+    methods other than GET. For example, if a POST to the same URL returns
+    no content, the automatic ETag will be incorrect, breaking a fundamental
+    use for entity tags in a possibly destructive fashion. Likewise, if you
+    raise 304 Not Modified, the response body will be empty, the ETag hash
+    will be incorrect, and your application will break.
+    See :rfc:`2616` Section 14.24.
+    """
+    response = cherrypy.serving.response
+
+    # Guard against being run twice.
+    if hasattr(response, 'ETag'):
+        return
+
+    status, reason, msg = _httputil.valid_status(response.status)
+
+    etag = response.headers.get('ETag')
+
+    # Automatic ETag generation. See warning in docstring.
+    if etag:
+        if debug:
+            cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS')
+    elif not autotags:
+        if debug:
+            cherrypy.log('Autotags off', 'TOOLS.ETAGS')
+    elif status != 200:
+        if debug:
+            cherrypy.log('Status not 200', 'TOOLS.ETAGS')
+    else:
+        etag = response.collapse_body()
+        etag = '"%s"' % md5(etag).hexdigest()
+        if debug:
+            cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
+        response.headers['ETag'] = etag
+
+    response.ETag = etag
+
+    # "If the request would, without the If-Match header field, result in
+    # anything other than a 2xx or 412 status, then the If-Match header
+    # MUST be ignored."
+    if debug:
+        cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
+    if status >= 200 and status <= 299:
+        request = cherrypy.serving.request
+
+        conditions = request.headers.elements('If-Match') or []
+        conditions = [str(x) for x in conditions]
+        if debug:
+            cherrypy.log('If-Match conditions: %s' % repr(conditions),
+                         'TOOLS.ETAGS')
+        if conditions and not (conditions == ['*'] or etag in conditions):
+            raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did '
+                                     'not match %r' % (etag, conditions))
+
+        conditions = request.headers.elements('If-None-Match') or []
+        conditions = [str(x) for x in conditions]
+        if debug:
+            cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
+                         'TOOLS.ETAGS')
+        if conditions == ['*'] or etag in conditions:
+            if debug:
+                cherrypy.log('request.method: %s' %
+                             request.method, 'TOOLS.ETAGS')
+            if request.method in ('GET', 'HEAD'):
+                raise cherrypy.HTTPRedirect([], 304)
+            else:
+                raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
+                                         'matched %r' % (etag, conditions))
+
+
+def validate_since():
+    """Validate the current Last-Modified against If-Modified-Since headers.
+
+    If no code has set the Last-Modified response header, then no validation
+    will be performed.
+    """
+    response = cherrypy.serving.response
+    lastmod = response.headers.get('Last-Modified')
+    if lastmod:
+        status, reason, msg = _httputil.valid_status(response.status)
+
+        request = cherrypy.serving.request
+
+        since = request.headers.get('If-Unmodified-Since')
+        if since and since != lastmod:
+            if (status >= 200 and status <= 299) or status == 412:
+                raise cherrypy.HTTPError(412)
+
+        since = request.headers.get('If-Modified-Since')
+        if since and since == lastmod:
+            if (status >= 200 and status <= 299) or status == 304:
+                if request.method in ('GET', 'HEAD'):
+                    raise cherrypy.HTTPRedirect([], 304)
+                else:
+                    raise cherrypy.HTTPError(412)
+
+
+#                                Tool code                                #
+
+def allow(methods=None, debug=False):
+    """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
+
+    The given methods are case-insensitive, and may be in any order.
+    If only one method is allowed, you may supply a single string;
+    if more than one, supply a list of strings.
+
+    Regardless of whether the current method is allowed or not, this
+    also emits an 'Allow' response header, containing the given methods.
+    """
+    if not isinstance(methods, (tuple, list)):
+        methods = [methods]
+    methods = [m.upper() for m in methods if m]
+    if not methods:
+        methods = ['GET', 'HEAD']
+    elif 'GET' in methods and 'HEAD' not in methods:
+        methods.append('HEAD')
+
+    cherrypy.response.headers['Allow'] = ', '.join(methods)
+    if cherrypy.request.method not in methods:
+        if debug:
+            cherrypy.log('request.method %r not in methods %r' %
+                         (cherrypy.request.method, methods), 'TOOLS.ALLOW')
+        raise cherrypy.HTTPError(405)
+    else:
+        if debug:
+            cherrypy.log('request.method %r in methods %r' %
+                         (cherrypy.request.method, methods), 'TOOLS.ALLOW')
+
+
+def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
+          scheme='X-Forwarded-Proto', debug=False):
+    """Change the base URL (scheme://host[:port][/path]).
+
+    For running a CP server behind Apache, lighttpd, or other HTTP server.
+
+    For Apache and lighttpd, you should leave the 'local' argument at the
+    default value of 'X-Forwarded-Host'. For Squid, you probably want to set
+    tools.proxy.local = 'Origin'.
+
+    If you want the new request.base to include path info (not just the host),
+    you must explicitly set base to the full base path, and ALSO set 'local'
+    to '', so that the X-Forwarded-Host request header (which never includes
+    path info) does not override it. Regardless, the value for 'base' MUST
+    NOT end in a slash.
+
+    cherrypy.request.remote.ip (the IP address of the client) will be
+    rewritten if the header specified by the 'remote' arg is valid.
+    By default, 'remote' is set to 'X-Forwarded-For'. If you do not
+    want to rewrite remote.ip, set the 'remote' arg to an empty string.
+    """
+
+    request = cherrypy.serving.request
+
+    if scheme:
+        s = request.headers.get(scheme, None)
+        if debug:
+            cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
+        if s == 'on' and 'ssl' in scheme.lower():
+            # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
+            scheme = 'https'
+        else:
+            # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
+            scheme = s
+    if not scheme:
+        scheme = request.base[:request.base.find('://')]
+
+    if local:
+        lbase = request.headers.get(local, None)
+        if debug:
+            cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
+        if lbase is not None:
+            base = lbase.split(',')[0]
+    if not base:
+        default = urllib.parse.urlparse(request.base).netloc
+        base = request.headers.get('Host', default)
+
+    if base.find('://') == -1:
+        # add http:// or https:// if needed
+        base = scheme + '://' + base
+
+    request.base = base
+
+    if remote:
+        xff = request.headers.get(remote)
+        if debug:
+            cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
+        if xff:
+            if remote == 'X-Forwarded-For':
+                # Grab the first IP in a comma-separated list. Ref #1268.
+                xff = next(ip.strip() for ip in xff.split(','))
+            request.remote.ip = xff
+
+
+def ignore_headers(headers=('Range',), debug=False):
+    """Delete request headers whose field names are included in 'headers'.
+
+    This is a useful tool for working behind certain HTTP servers;
+    for example, Apache duplicates the work that CP does for 'Range'
+    headers, and will doubly-truncate the response.
+    """
+    request = cherrypy.serving.request
+    for name in headers:
+        if name in request.headers:
+            if debug:
+                cherrypy.log('Ignoring request header %r' % name,
+                             'TOOLS.IGNORE_HEADERS')
+            del request.headers[name]
+
+
+def response_headers(headers=None, debug=False):
+    """Set headers on the response."""
+    if debug:
+        cherrypy.log('Setting response headers: %s' % repr(headers),
+                     'TOOLS.RESPONSE_HEADERS')
+    for name, value in (headers or []):
+        cherrypy.serving.response.headers[name] = value
+
+
+response_headers.failsafe = True
+
+
+def referer(pattern, accept=True, accept_missing=False, error=403,
+            message='Forbidden Referer header.', debug=False):
+    """Raise HTTPError if Referer header does/does not match the given pattern.
+
+    pattern
+        A regular expression pattern to test against the Referer.
+
+    accept
+        If True, the Referer must match the pattern; if False,
+        the Referer must NOT match the pattern.
+
+    accept_missing
+        If True, permit requests with no Referer header.
+
+    error
+        The HTTP error code to return to the client on failure.
+
+    message
+        A string to include in the response body on failure.
+
+    """
+    try:
+        ref = cherrypy.serving.request.headers['Referer']
+        match = bool(re.match(pattern, ref))
+        if debug:
+            cherrypy.log('Referer %r matches %r' % (ref, pattern),
+                         'TOOLS.REFERER')
+        if accept == match:
+            return
+    except KeyError:
+        if debug:
+            cherrypy.log('No Referer header', 'TOOLS.REFERER')
+        if accept_missing:
+            return
+
+    raise cherrypy.HTTPError(error, message)
+
+
+class SessionAuth(object):
+
+    """Assert that the user is logged in."""
+
+    session_key = 'username'
+    debug = False
+
+    def check_username_and_password(self, username, password):
+        pass
+
+    def anonymous(self):
+        """Provide a temporary user name for anonymous users."""
+        pass
+
+    def on_login(self, username):
+        pass
+
+    def on_logout(self, username):
+        pass
+
+    def on_check(self, username):
+        pass
+
+    def login_screen(self, from_page='..', username='', error_msg='',
+                     **kwargs):
+        return (six.text_type("""<html><body>
+Message: %(error_msg)s
+<form method="post" action="do_login">
+    Login: <input type="text" name="username" value="%(username)s" size="10" />
+    <br />
+    Password: <input type="password" name="password" size="10" />
+    <br />
+    <input type="hidden" name="from_page" value="%(from_page)s" />
+    <br />
+    <input type="submit" />
+</form>
+</body></html>""") % vars()).encode('utf-8')
+
+    def do_login(self, username, password, from_page='..', **kwargs):
+        """Login. May raise redirect, or return True if request handled."""
+        response = cherrypy.serving.response
+        error_msg = self.check_username_and_password(username, password)
+        if error_msg:
+            body = self.login_screen(from_page, username, error_msg)
+            response.body = body
+            if 'Content-Length' in response.headers:
+                # Delete Content-Length header so finalize() recalcs it.
+                del response.headers['Content-Length']
+            return True
+        else:
+            cherrypy.serving.request.login = username
+            cherrypy.session[self.session_key] = username
+            self.on_login(username)
+            raise cherrypy.HTTPRedirect(from_page or '/')
+
+    def do_logout(self, from_page='..', **kwargs):
+        """Logout. May raise redirect, or return True if request handled."""
+        sess = cherrypy.session
+        username = sess.get(self.session_key)
+        sess[self.session_key] = None
+        if username:
+            cherrypy.serving.request.login = None
+            self.on_logout(username)
+        raise cherrypy.HTTPRedirect(from_page)
+
+    def do_check(self):
+        """Assert username. Raise redirect, or return True if request handled.
+        """
+        sess = cherrypy.session
+        request = cherrypy.serving.request
+        response = cherrypy.serving.response
+
+        username = sess.get(self.session_key)
+        if not username:
+            sess[self.session_key] = username = self.anonymous()
+            self._debug_message('No session[username], trying anonymous')
+        if not username:
+            url = cherrypy.url(qs=request.query_string)
+            self._debug_message(
+                'No username, routing to login_screen with from_page %(url)r',
+                locals(),
+            )
+            response.body = self.login_screen(url)
+            if 'Content-Length' in response.headers:
+                # Delete Content-Length header so finalize() recalcs it.
+                del response.headers['Content-Length']
+            return True
+        self._debug_message('Setting request.login to %(username)r', locals())
+        request.login = username
+        self.on_check(username)
+
+    def _debug_message(self, template, context={}):
+        if not self.debug:
+            return
+        cherrypy.log(template % context, 'TOOLS.SESSAUTH')
+
+    def run(self):
+        request = cherrypy.serving.request
+        response = cherrypy.serving.response
+
+        path = request.path_info
+        if path.endswith('login_screen'):
+            self._debug_message('routing %(path)r to login_screen', locals())
+            response.body = self.login_screen()
+            return True
+        elif path.endswith('do_login'):
+            if request.method != 'POST':
+                response.headers['Allow'] = 'POST'
+                self._debug_message('do_login requires POST')
+                raise cherrypy.HTTPError(405)
+            self._debug_message('routing %(path)r to do_login', locals())
+            return self.do_login(**request.params)
+        elif path.endswith('do_logout'):
+            if request.method != 'POST':
+                response.headers['Allow'] = 'POST'
+                raise cherrypy.HTTPError(405)
+            self._debug_message('routing %(path)r to do_logout', locals())
+            return self.do_logout(**request.params)
+        else:
+            self._debug_message('No special path, running do_check')
+            return self.do_check()
+
+
+def session_auth(**kwargs):
+    sa = SessionAuth()
+    for k, v in kwargs.items():
+        setattr(sa, k, v)
+    return sa.run()
+
+
+session_auth.__doc__ = (
+    """Session authentication hook.
+
+    Any attribute of the SessionAuth class may be overridden via a keyword arg
+    to this function:
+
+    """ + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
+                     for k in dir(SessionAuth) if not k.startswith('__')])
+)
+
+
+def log_traceback(severity=logging.ERROR, debug=False):
+    """Write the last error's traceback to the cherrypy error log."""
+    cherrypy.log('', 'HTTP', severity=severity, traceback=True)
+
+
+def log_request_headers(debug=False):
+    """Write request headers to the cherrypy error log."""
+    h = ['  %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list]
+    cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP')
+
+
+def log_hooks(debug=False):
+    """Write request.hooks to the cherrypy error log."""
+    request = cherrypy.serving.request
+
+    msg = []
+    # Sort by the standard points if possible.
+    from cherrypy import _cprequest
+    points = _cprequest.hookpoints
+    for k in request.hooks.keys():
+        if k not in points:
+            points.append(k)
+
+    for k in points:
+        msg.append('    %s:' % k)
+        v = request.hooks.get(k, [])
+        v.sort()
+        for h in v:
+            msg.append('        %r' % h)
+    cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
+                 ':\n' + '\n'.join(msg), 'HTTP')
+
+
+def redirect(url='', internal=True, debug=False):
+    """Raise InternalRedirect or HTTPRedirect to the given url."""
+    if debug:
+        cherrypy.log('Redirecting %sto: %s' %
+                     ({True: 'internal ', False: ''}[internal], url),
+                     'TOOLS.REDIRECT')
+    if internal:
+        raise cherrypy.InternalRedirect(url)
+    else:
+        raise cherrypy.HTTPRedirect(url)
+
+
+def trailing_slash(missing=True, extra=False, status=None, debug=False):
+    """Redirect if path_info has (missing|extra) trailing slash."""
+    request = cherrypy.serving.request
+    pi = request.path_info
+
+    if debug:
+        cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
+                     (request.is_index, missing, extra, pi),
+                     'TOOLS.TRAILING_SLASH')
+    if request.is_index is True:
+        if missing:
+            if not pi.endswith('/'):
+                new_url = cherrypy.url(pi + '/', request.query_string)
+                raise cherrypy.HTTPRedirect(new_url, status=status or 301)
+    elif request.is_index is False:
+        if extra:
+            # If pi == '/', don't redirect to ''!
+            if pi.endswith('/') and pi != '/':
+                new_url = cherrypy.url(pi[:-1], request.query_string)
+                raise cherrypy.HTTPRedirect(new_url, status=status or 301)
+
+
+def flatten(debug=False):
+    """Wrap response.body in a generator that recursively iterates over body.
+
+    This allows cherrypy.response.body to consist of 'nested generators';
+    that is, a set of generators that yield generators.
+    """
+    def flattener(input):
+        numchunks = 0
+        for x in input:
+            if not is_iterator(x):
+                numchunks += 1
+                yield x
+            else:
+                for y in flattener(x):
+                    numchunks += 1
+                    yield y
+        if debug:
+            cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
+    response = cherrypy.serving.response
+    response.body = flattener(response.body)
+
+
+def accept(media=None, debug=False):
+    """Return the client's preferred media-type (from the given Content-Types).
+
+    If 'media' is None (the default), no test will be performed.
+
+    If 'media' is provided, it should be the Content-Type value (as a string)
+    or values (as a list or tuple of strings) which the current resource
+    can emit. The client's acceptable media ranges (as declared in the
+    Accept request header) will be matched in order to these Content-Type
+    values; the first such string is returned. That is, the return value
+    will always be one of the strings provided in the 'media' arg (or None
+    if 'media' is None).
+
+    If no match is found, then HTTPError 406 (Not Acceptable) is raised.
+    Note that most web browsers send */* as a (low-quality) acceptable
+    media range, which should match any Content-Type. In addition, "...if
+    no Accept header field is present, then it is assumed that the client
+    accepts all media types."
+
+    Matching types are checked in order of client preference first,
+    and then in the order of the given 'media' values.
+
+    Note that this function does not honor accept-params (other than "q").
+    """
+    if not media:
+        return
+    if isinstance(media, text_or_bytes):
+        media = [media]
+    request = cherrypy.serving.request
+
+    # Parse the Accept request header, and try to match one
+    # of the requested media-ranges (in order of preference).
+    ranges = request.headers.elements('Accept')
+    if not ranges:
+        # Any media type is acceptable.
+        if debug:
+            cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
+        return media[0]
+    else:
+        # Note that 'ranges' is sorted in order of preference
+        for element in ranges:
+            if element.qvalue > 0:
+                if element.value == '*/*':
+                    # Matches any type or subtype
+                    if debug:
+                        cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
+                    return media[0]
+                elif element.value.endswith('/*'):
+                    # Matches any subtype
+                    mtype = element.value[:-1]  # Keep the slash
+                    for m in media:
+                        if m.startswith(mtype):
+                            if debug:
+                                cherrypy.log('Match due to %s' % element.value,
+                                             'TOOLS.ACCEPT')
+                            return m
+                else:
+                    # Matches exact value
+                    if element.value in media:
+                        if debug:
+                            cherrypy.log('Match due to %s' % element.value,
+                                         'TOOLS.ACCEPT')
+                        return element.value
+
+    # No suitable media-range found.
+    ah = request.headers.get('Accept')
+    if ah is None:
+        msg = 'Your client did not send an Accept header.'
+    else:
+        msg = 'Your client sent this Accept header: %s.' % ah
+    msg += (' But this resource only emits these media types: %s.' %
+            ', '.join(media))
+    raise cherrypy.HTTPError(406, msg)
+
+
+class MonitoredHeaderMap(_httputil.HeaderMap):
+
+    def transform_key(self, key):
+        self.accessed_headers.add(key)
+        return super(MonitoredHeaderMap, self).transform_key(key)
+
+    def __init__(self):
+        self.accessed_headers = set()
+        super(MonitoredHeaderMap, self).__init__()
+
+
+def autovary(ignore=None, debug=False):
+    """Auto-populate the Vary response header based on request.header access.
+    """
+    request = cherrypy.serving.request
+
+    req_h = request.headers
+    request.headers = MonitoredHeaderMap()
+    request.headers.update(req_h)
+    if ignore is None:
+        ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
+
+    def set_response_header():
+        resp_h = cherrypy.serving.response.headers
+        v = set([e.value for e in resp_h.elements('Vary')])
+        if debug:
+            cherrypy.log(
+                'Accessed headers: %s' % request.headers.accessed_headers,
+                'TOOLS.AUTOVARY')
+        v = v.union(request.headers.accessed_headers)
+        v = v.difference(ignore)
+        v = list(v)
+        v.sort()
+        resp_h['Vary'] = ', '.join(v)
+    request.hooks.attach('before_finalize', set_response_header, 95)
+
+
+def convert_params(exception=ValueError, error=400):
+    """Convert request params based on function annotations, with error handling.
+
+    exception
+        Exception class to catch.
+
+    status
+        The HTTP error code to return to the client on failure.
+    """
+    request = cherrypy.serving.request
+    types = request.handler.callable.__annotations__
+    with cherrypy.HTTPError.handle(exception, error):
+        for key in set(types).intersection(request.params):
+            request.params[key] = types[key](request.params[key])
diff --git a/libraries/cherrypy/lib/encoding.py b/libraries/cherrypy/lib/encoding.py
new file mode 100644
index 00000000..3d001ca6
--- /dev/null
+++ b/libraries/cherrypy/lib/encoding.py
@@ -0,0 +1,436 @@
+import struct
+import time
+import io
+
+import six
+
+import cherrypy
+from cherrypy._cpcompat import text_or_bytes
+from cherrypy.lib import file_generator
+from cherrypy.lib import is_closable_iterator
+from cherrypy.lib import set_vary_header
+
+
+def decode(encoding=None, default_encoding='utf-8'):
+    """Replace or extend the list of charsets used to decode a request entity.
+
+    Either argument may be a single string or a list of strings.
+
+    encoding
+        If not None, restricts the set of charsets attempted while decoding
+        a request entity to the given set (even if a different charset is
+        given in the Content-Type request header).
+
+    default_encoding
+        Only in effect if the 'encoding' argument is not given.
+        If given, the set of charsets attempted while decoding a request
+        entity is *extended* with the given value(s).
+
+    """
+    body = cherrypy.request.body
+    if encoding is not None:
+        if not isinstance(encoding, list):
+            encoding = [encoding]
+        body.attempt_charsets = encoding
+    elif default_encoding:
+        if not isinstance(default_encoding, list):
+            default_encoding = [default_encoding]
+        body.attempt_charsets = body.attempt_charsets + default_encoding
+
+
+class UTF8StreamEncoder:
+    def __init__(self, iterator):
+        self._iterator = iterator
+
+    def __iter__(self):
+        return self
+
+    def next(self):
+        return self.__next__()
+
+    def __next__(self):
+        res = next(self._iterator)
+        if isinstance(res, six.text_type):
+            res = res.encode('utf-8')
+        return res
+
+    def close(self):
+        if is_closable_iterator(self._iterator):
+            self._iterator.close()
+
+    def __getattr__(self, attr):
+        if attr.startswith('__'):
+            raise AttributeError(self, attr)
+        return getattr(self._iterator, attr)
+
+
+class ResponseEncoder:
+
+    default_encoding = 'utf-8'
+    failmsg = 'Response body could not be encoded with %r.'
+    encoding = None
+    errors = 'strict'
+    text_only = True
+    add_charset = True
+    debug = False
+
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+        self.attempted_charsets = set()
+        request = cherrypy.serving.request
+        if request.handler is not None:
+            # Replace request.handler with self
+            if self.debug:
+                cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
+            self.oldhandler = request.handler
+            request.handler = self
+
+    def encode_stream(self, encoding):
+        """Encode a streaming response body.
+
+        Use a generator wrapper, and just pray it works as the stream is
+        being written out.
+        """
+        if encoding in self.attempted_charsets:
+            return False
+        self.attempted_charsets.add(encoding)
+
+        def encoder(body):
+            for chunk in body:
+                if isinstance(chunk, six.text_type):
+                    chunk = chunk.encode(encoding, self.errors)
+                yield chunk
+        self.body = encoder(self.body)
+        return True
+
+    def encode_string(self, encoding):
+        """Encode a buffered response body."""
+        if encoding in self.attempted_charsets:
+            return False
+        self.attempted_charsets.add(encoding)
+        body = []
+        for chunk in self.body:
+            if isinstance(chunk, six.text_type):
+                try:
+                    chunk = chunk.encode(encoding, self.errors)
+                except (LookupError, UnicodeError):
+                    return False
+            body.append(chunk)
+        self.body = body
+        return True
+
+    def find_acceptable_charset(self):
+        request = cherrypy.serving.request
+        response = cherrypy.serving.response
+
+        if self.debug:
+            cherrypy.log('response.stream %r' %
+                         response.stream, 'TOOLS.ENCODE')
+        if response.stream:
+            encoder = self.encode_stream
+        else:
+            encoder = self.encode_string
+            if 'Content-Length' in response.headers:
+                # Delete Content-Length header so finalize() recalcs it.
+                # Encoded strings may be of different lengths from their
+                # unicode equivalents, and even from each other. For example:
+                # >>> t = u"\u7007\u3040"
+                # >>> len(t)
+                # 2
+                # >>> len(t.encode("UTF-8"))
+                # 6
+                # >>> len(t.encode("utf7"))
+                # 8
+                del response.headers['Content-Length']
+
+        # Parse the Accept-Charset request header, and try to provide one
+        # of the requested charsets (in order of user preference).
+        encs = request.headers.elements('Accept-Charset')
+        charsets = [enc.value.lower() for enc in encs]
+        if self.debug:
+            cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
+
+        if self.encoding is not None:
+            # If specified, force this encoding to be used, or fail.
+            encoding = self.encoding.lower()
+            if self.debug:
+                cherrypy.log('Specified encoding %r' %
+                             encoding, 'TOOLS.ENCODE')
+            if (not charsets) or '*' in charsets or encoding in charsets:
+                if self.debug:
+                    cherrypy.log('Attempting encoding %r' %
+                                 encoding, 'TOOLS.ENCODE')
+                if encoder(encoding):
+                    return encoding
+        else:
+            if not encs:
+                if self.debug:
+                    cherrypy.log('Attempting default encoding %r' %
+                                 self.default_encoding, 'TOOLS.ENCODE')
+                # Any character-set is acceptable.
+                if encoder(self.default_encoding):
+                    return self.default_encoding
+                else:
+                    raise cherrypy.HTTPError(500, self.failmsg %
+                                             self.default_encoding)
+            else:
+                for element in encs:
+                    if element.qvalue > 0:
+                        if element.value == '*':
+                            # Matches any charset. Try our default.
+                            if self.debug:
+                                cherrypy.log('Attempting default encoding due '
+                                             'to %r' % element, 'TOOLS.ENCODE')
+                            if encoder(self.default_encoding):
+                                return self.default_encoding
+                        else:
+                            encoding = element.value
+                            if self.debug:
+                                cherrypy.log('Attempting encoding %s (qvalue >'
+                                             '0)' % element, 'TOOLS.ENCODE')
+                            if encoder(encoding):
+                                return encoding
+
+                if '*' not in charsets:
+                    # If no "*" is present in an Accept-Charset field, then all
+                    # character sets not explicitly mentioned get a quality
+                    # value of 0, except for ISO-8859-1, which gets a quality
+                    # value of 1 if not explicitly mentioned.
+                    iso = 'iso-8859-1'
+                    if iso not in charsets:
+                        if self.debug:
+                            cherrypy.log('Attempting ISO-8859-1 encoding',
+                                         'TOOLS.ENCODE')
+                        if encoder(iso):
+                            return iso
+
+        # No suitable encoding found.
+        ac = request.headers.get('Accept-Charset')
+        if ac is None:
+            msg = 'Your client did not send an Accept-Charset header.'
+        else:
+            msg = 'Your client sent this Accept-Charset header: %s.' % ac
+        _charsets = ', '.join(sorted(self.attempted_charsets))
+        msg += ' We tried these charsets: %s.' % (_charsets,)
+        raise cherrypy.HTTPError(406, msg)
+
+    def __call__(self, *args, **kwargs):
+        response = cherrypy.serving.response
+        self.body = self.oldhandler(*args, **kwargs)
+
+        self.body = prepare_iter(self.body)
+
+        ct = response.headers.elements('Content-Type')
+        if self.debug:
+            cherrypy.log('Content-Type: %r' % [str(h)
+                         for h in ct], 'TOOLS.ENCODE')
+        if ct and self.add_charset:
+            ct = ct[0]
+            if self.text_only:
+                if ct.value.lower().startswith('text/'):
+                    if self.debug:
+                        cherrypy.log(
+                            'Content-Type %s starts with "text/"' % ct,
+                            'TOOLS.ENCODE')
+                    do_find = True
+                else:
+                    if self.debug:
+                        cherrypy.log('Not finding because Content-Type %s '
+                                     'does not start with "text/"' % ct,
+                                     'TOOLS.ENCODE')
+                    do_find = False
+            else:
+                if self.debug:
+                    cherrypy.log('Finding because not text_only',
+                                 'TOOLS.ENCODE')
+                do_find = True
+
+            if do_find:
+                # Set "charset=..." param on response Content-Type header
+                ct.params['charset'] = self.find_acceptable_charset()
+                if self.debug:
+                    cherrypy.log('Setting Content-Type %s' % ct,
+                                 'TOOLS.ENCODE')
+                response.headers['Content-Type'] = str(ct)
+
+        return self.body
+
+
+def prepare_iter(value):
+    """
+    Ensure response body is iterable and resolves to False when empty.
+    """
+    if isinstance(value, text_or_bytes):
+        # strings get wrapped in a list because iterating over a single
+        # item list is much faster than iterating over every character
+        # in a long string.
+        if value:
+            value = [value]
+        else:
+            # [''] doesn't evaluate to False, so replace it with [].
+            value = []
+    # Don't use isinstance here; io.IOBase which has an ABC takes
+    # 1000 times as long as, say, isinstance(value, str)
+    elif hasattr(value, 'read'):
+        value = file_generator(value)
+    elif value is None:
+        value = []
+    return value
+
+
+# GZIP
+
+
+def compress(body, compress_level):
+    """Compress 'body' at the given compress_level."""
+    import zlib
+
+    # See http://www.gzip.org/zlib/rfc-gzip.html
+    yield b'\x1f\x8b'       # ID1 and ID2: gzip marker
+    yield b'\x08'           # CM: compression method
+    yield b'\x00'           # FLG: none set
+    # MTIME: 4 bytes
+    yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
+    yield b'\x02'           # XFL: max compression, slowest algo
+    yield b'\xff'           # OS: unknown
+
+    crc = zlib.crc32(b'')
+    size = 0
+    zobj = zlib.compressobj(compress_level,
+                            zlib.DEFLATED, -zlib.MAX_WBITS,
+                            zlib.DEF_MEM_LEVEL, 0)
+    for line in body:
+        size += len(line)
+        crc = zlib.crc32(line, crc)
+        yield zobj.compress(line)
+    yield zobj.flush()
+
+    # CRC32: 4 bytes
+    yield struct.pack('<L', crc & int('FFFFFFFF', 16))
+    # ISIZE: 4 bytes
+    yield struct.pack('<L', size & int('FFFFFFFF', 16))
+
+
+def decompress(body):
+    import gzip
+
+    zbuf = io.BytesIO()
+    zbuf.write(body)
+    zbuf.seek(0)
+    zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
+    data = zfile.read()
+    zfile.close()
+    return data
+
+
+def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
+         debug=False):
+    """Try to gzip the response body if Content-Type in mime_types.
+
+    cherrypy.response.headers['Content-Type'] must be set to one of the
+    values in the mime_types arg before calling this function.
+
+    The provided list of mime-types must be of one of the following form:
+        * `type/subtype`
+        * `type/*`
+        * `type/*+subtype`
+
+    No compression is performed if any of the following hold:
+        * The client sends no Accept-Encoding request header
+        * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
+        * No 'gzip' or 'x-gzip' with a qvalue > 0 is present
+        * The 'identity' value is given with a qvalue > 0.
+
+    """
+    request = cherrypy.serving.request
+    response = cherrypy.serving.response
+
+    set_vary_header(response, 'Accept-Encoding')
+
+    if not response.body:
+        # Response body is empty (might be a 304 for instance)
+        if debug:
+            cherrypy.log('No response body', context='TOOLS.GZIP')
+        return
+
+    # If returning cached content (which should already have been gzipped),
+    # don't re-zip.
+    if getattr(request, 'cached', False):
+        if debug:
+            cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
+        return
+
+    acceptable = request.headers.elements('Accept-Encoding')
+    if not acceptable:
+        # If no Accept-Encoding field is present in a request,
+        # the server MAY assume that the client will accept any
+        # content coding. In this case, if "identity" is one of
+        # the available content-codings, then the server SHOULD use
+        # the "identity" content-coding, unless it has additional
+        # information that a different content-coding is meaningful
+        # to the client.
+        if debug:
+            cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
+        return
+
+    ct = response.headers.get('Content-Type', '').split(';')[0]
+    for coding in acceptable:
+        if coding.value == 'identity' and coding.qvalue != 0:
+            if debug:
+                cherrypy.log('Non-zero identity qvalue: %s' % coding,
+                             context='TOOLS.GZIP')
+            return
+        if coding.value in ('gzip', 'x-gzip'):
+            if coding.qvalue == 0:
+                if debug:
+                    cherrypy.log('Zero gzip qvalue: %s' % coding,
+                                 context='TOOLS.GZIP')
+                return
+
+            if ct not in mime_types:
+                # If the list of provided mime-types contains tokens
+                # such as 'text/*' or 'application/*+xml',
+                # we go through them and find the most appropriate one
+                # based on the given content-type.
+                # The pattern matching is only caring about the most
+                # common cases, as stated above, and doesn't support
+                # for extra parameters.
+                found = False
+                if '/' in ct:
+                    ct_media_type, ct_sub_type = ct.split('/')
+                    for mime_type in mime_types:
+                        if '/' in mime_type:
+                            media_type, sub_type = mime_type.split('/')
+                            if ct_media_type == media_type:
+                                if sub_type == '*':
+                                    found = True
+                                    break
+                                elif '+' in sub_type and '+' in ct_sub_type:
+                                    ct_left, ct_right = ct_sub_type.split('+')
+                                    left, right = sub_type.split('+')
+                                    if left == '*' and ct_right == right:
+                                        found = True
+                                        break
+
+                if not found:
+                    if debug:
+                        cherrypy.log('Content-Type %s not in mime_types %r' %
+                                     (ct, mime_types), context='TOOLS.GZIP')
+                    return
+
+            if debug:
+                cherrypy.log('Gzipping', context='TOOLS.GZIP')
+            # Return a generator that compresses the page
+            response.headers['Content-Encoding'] = 'gzip'
+            response.body = compress(response.body, compress_level)
+            if 'Content-Length' in response.headers:
+                # Delete Content-Length header so finalize() recalcs it.
+                del response.headers['Content-Length']
+
+            return
+
+    if debug:
+        cherrypy.log('No acceptable encoding found.', context='GZIP')
+    cherrypy.HTTPError(406, 'identity, gzip').set_response()
diff --git a/libraries/cherrypy/lib/gctools.py b/libraries/cherrypy/lib/gctools.py
new file mode 100644
index 00000000..26746d78
--- /dev/null
+++ b/libraries/cherrypy/lib/gctools.py
@@ -0,0 +1,218 @@
+import gc
+import inspect
+import sys
+import time
+
+try:
+    import objgraph
+except ImportError:
+    objgraph = None
+
+import cherrypy
+from cherrypy import _cprequest, _cpwsgi
+from cherrypy.process.plugins import SimplePlugin
+
+
+class ReferrerTree(object):
+
+    """An object which gathers all referrers of an object to a given depth."""
+
+    peek_length = 40
+
+    def __init__(self, ignore=None, maxdepth=2, maxparents=10):
+        self.ignore = ignore or []
+        self.ignore.append(inspect.currentframe().f_back)
+        self.maxdepth = maxdepth
+        self.maxparents = maxparents
+
+    def ascend(self, obj, depth=1):
+        """Return a nested list containing referrers of the given object."""
+        depth += 1
+        parents = []
+
+        # Gather all referrers in one step to minimize
+        # cascading references due to repr() logic.
+        refs = gc.get_referrers(obj)
+        self.ignore.append(refs)
+        if len(refs) > self.maxparents:
+            return [('[%s referrers]' % len(refs), [])]
+
+        try:
+            ascendcode = self.ascend.__code__
+        except AttributeError:
+            ascendcode = self.ascend.im_func.func_code
+        for parent in refs:
+            if inspect.isframe(parent) and parent.f_code is ascendcode:
+                continue
+            if parent in self.ignore:
+                continue
+            if depth <= self.maxdepth:
+                parents.append((parent, self.ascend(parent, depth)))
+            else:
+                parents.append((parent, []))
+
+        return parents
+
+    def peek(self, s):
+        """Return s, restricted to a sane length."""
+        if len(s) > (self.peek_length + 3):
+            half = self.peek_length // 2
+            return s[:half] + '...' + s[-half:]
+        else:
+            return s
+
+    def _format(self, obj, descend=True):
+        """Return a string representation of a single object."""
+        if inspect.isframe(obj):
+            filename, lineno, func, context, index = inspect.getframeinfo(obj)
+            return "<frame of function '%s'>" % func
+
+        if not descend:
+            return self.peek(repr(obj))
+
+        if isinstance(obj, dict):
+            return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
+                                                self._format(v, descend=False))
+                                    for k, v in obj.items()]) + '}'
+        elif isinstance(obj, list):
+            return '[' + ', '.join([self._format(item, descend=False)
+                                    for item in obj]) + ']'
+        elif isinstance(obj, tuple):
+            return '(' + ', '.join([self._format(item, descend=False)
+                                    for item in obj]) + ')'
+
+        r = self.peek(repr(obj))
+        if isinstance(obj, (str, int, float)):
+            return r
+        return '%s: %s' % (type(obj), r)
+
+    def format(self, tree):
+        """Return a list of string reprs from a nested list of referrers."""
+        output = []
+
+        def ascend(branch, depth=1):
+            for parent, grandparents in branch:
+                output.append(('    ' * depth) + self._format(parent))
+                if grandparents:
+                    ascend(grandparents, depth + 1)
+        ascend(tree)
+        return output
+
+
+def get_instances(cls):
+    return [x for x in gc.get_objects() if isinstance(x, cls)]
+
+
+class RequestCounter(SimplePlugin):
+
+    def start(self):
+        self.count = 0
+
+    def before_request(self):
+        self.count += 1
+
+    def after_request(self):
+        self.count -= 1
+
+
+request_counter = RequestCounter(cherrypy.engine)
+request_counter.subscribe()
+
+
+def get_context(obj):
+    if isinstance(obj, _cprequest.Request):
+        return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
+    elif isinstance(obj, _cprequest.Response):
+        return 'status=%s' % obj.status
+    elif isinstance(obj, _cpwsgi.AppResponse):
+        return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
+    elif hasattr(obj, 'tb_lineno'):
+        return 'tb_lineno=%s' % obj.tb_lineno
+    return ''
+
+
+class GCRoot(object):
+
+    """A CherryPy page handler for testing reference leaks."""
+
+    classes = [
+        (_cprequest.Request, 2, 2,
+         'Should be 1 in this request thread and 1 in the main thread.'),
+        (_cprequest.Response, 2, 2,
+         'Should be 1 in this request thread and 1 in the main thread.'),
+        (_cpwsgi.AppResponse, 1, 1,
+         'Should be 1 in this request thread only.'),
+    ]
+
+    @cherrypy.expose
+    def index(self):
+        return 'Hello, world!'
+
+    @cherrypy.expose
+    def stats(self):
+        output = ['Statistics:']
+
+        for trial in range(10):
+            if request_counter.count > 0:
+                break
+            time.sleep(0.5)
+        else:
+            output.append('\nNot all requests closed properly.')
+
+        # gc_collect isn't perfectly synchronous, because it may
+        # break reference cycles that then take time to fully
+        # finalize. Call it thrice and hope for the best.
+        gc.collect()
+        gc.collect()
+        unreachable = gc.collect()
+        if unreachable:
+            if objgraph is not None:
+                final = objgraph.by_type('Nondestructible')
+                if final:
+                    objgraph.show_backrefs(final, filename='finalizers.png')
+
+            trash = {}
+            for x in gc.garbage:
+                trash[type(x)] = trash.get(type(x), 0) + 1
+            if trash:
+                output.insert(0, '\n%s unreachable objects:' % unreachable)
+                trash = [(v, k) for k, v in trash.items()]
+                trash.sort()
+                for pair in trash:
+                    output.append('    ' + repr(pair))
+
+        # Check declared classes to verify uncollected instances.
+        # These don't have to be part of a cycle; they can be
+        # any objects that have unanticipated referrers that keep
+        # them from being collected.
+        allobjs = {}
+        for cls, minobj, maxobj, msg in self.classes:
+            allobjs[cls] = get_instances(cls)
+
+        for cls, minobj, maxobj, msg in self.classes:
+            objs = allobjs[cls]
+            lenobj = len(objs)
+            if lenobj < minobj or lenobj > maxobj:
+                if minobj == maxobj:
+                    output.append(
+                        '\nExpected %s %r references, got %s.' %
+                        (minobj, cls, lenobj))
+                else:
+                    output.append(
+                        '\nExpected %s to %s %r references, got %s.' %
+                        (minobj, maxobj, cls, lenobj))
+
+                for obj in objs:
+                    if objgraph is not None:
+                        ig = [id(objs), id(inspect.currentframe())]
+                        fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
+                        objgraph.show_backrefs(
+                            obj, extra_ignore=ig, max_depth=4, too_many=20,
+                            filename=fname, extra_info=get_context)
+                    output.append('\nReferrers for %s (refcount=%s):' %
+                                  (repr(obj), sys.getrefcount(obj)))
+                    t = ReferrerTree(ignore=[objs], maxdepth=3)
+                    tree = t.ascend(obj)
+                    output.extend(t.format(tree))
+
+        return '\n'.join(output)
diff --git a/libraries/cherrypy/lib/httputil.py b/libraries/cherrypy/lib/httputil.py
new file mode 100644
index 00000000..b68d8dd5
--- /dev/null
+++ b/libraries/cherrypy/lib/httputil.py
@@ -0,0 +1,581 @@
+"""HTTP library functions.
+
+This module contains functions for building an HTTP application
+framework: any one, not just one whose name starts with "Ch". ;) If you
+reference any modules from some popular framework inside *this* module,
+FuManChu will personally hang you up by your thumbs and submit you
+to a public caning.
+"""
+
+import functools
+import email.utils
+import re
+from binascii import b2a_base64
+from cgi import parse_header
+from email.header import decode_header
+
+import six
+from six.moves import range, builtins, map
+from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
+
+import cherrypy
+from cherrypy._cpcompat import ntob, ntou
+from cherrypy._cpcompat import unquote_plus
+
+response_codes = BaseHTTPRequestHandler.responses.copy()
+
+# From https://github.com/cherrypy/cherrypy/issues/361
+response_codes[500] = ('Internal Server Error',
+                       'The server encountered an unexpected condition '
+                       'which prevented it from fulfilling the request.')
+response_codes[503] = ('Service Unavailable',
+                       'The server is currently unable to handle the '
+                       'request due to a temporary overloading or '
+                       'maintenance of the server.')
+
+
+HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
+
+
+def urljoin(*atoms):
+    r"""Return the given path \*atoms, joined into a single URL.
+
+    This will correctly join a SCRIPT_NAME and PATH_INFO into the
+    original URL, even if either atom is blank.
+    """
+    url = '/'.join([x for x in atoms if x])
+    while '//' in url:
+        url = url.replace('//', '/')
+    # Special-case the final url of "", and return "/" instead.
+    return url or '/'
+
+
+def urljoin_bytes(*atoms):
+    """Return the given path `*atoms`, joined into a single URL.
+
+    This will correctly join a SCRIPT_NAME and PATH_INFO into the
+    original URL, even if either atom is blank.
+    """
+    url = b'/'.join([x for x in atoms if x])
+    while b'//' in url:
+        url = url.replace(b'//', b'/')
+    # Special-case the final url of "", and return "/" instead.
+    return url or b'/'
+
+
+def protocol_from_http(protocol_str):
+    """Return a protocol tuple from the given 'HTTP/x.y' string."""
+    return int(protocol_str[5]), int(protocol_str[7])
+
+
+def get_ranges(headervalue, content_length):
+    """Return a list of (start, stop) indices from a Range header, or None.
+
+    Each (start, stop) tuple will be composed of two ints, which are suitable
+    for use in a slicing operation. That is, the header "Range: bytes=3-6",
+    if applied against a Python string, is requesting resource[3:7]. This
+    function will return the list [(3, 7)].
+
+    If this function returns an empty list, you should return HTTP 416.
+    """
+
+    if not headervalue:
+        return None
+
+    result = []
+    bytesunit, byteranges = headervalue.split('=', 1)
+    for brange in byteranges.split(','):
+        start, stop = [x.strip() for x in brange.split('-', 1)]
+        if start:
+            if not stop:
+                stop = content_length - 1
+            start, stop = int(start), int(stop)
+            if start >= content_length:
+                # From rfc 2616 sec 14.16:
+                # "If the server receives a request (other than one
+                # including an If-Range request-header field) with an
+                # unsatisfiable Range request-header field (that is,
+                # all of whose byte-range-spec values have a first-byte-pos
+                # value greater than the current length of the selected
+                # resource), it SHOULD return a response code of 416
+                # (Requested range not satisfiable)."
+                continue
+            if stop < start:
+                # From rfc 2616 sec 14.16:
+                # "If the server ignores a byte-range-spec because it
+                # is syntactically invalid, the server SHOULD treat
+                # the request as if the invalid Range header field
+                # did not exist. (Normally, this means return a 200
+                # response containing the full entity)."
+                return None
+            result.append((start, stop + 1))
+        else:
+            if not stop:
+                # See rfc quote above.
+                return None
+            # Negative subscript (last N bytes)
+            #
+            # RFC 2616 Section 14.35.1:
+            #   If the entity is shorter than the specified suffix-length,
+            #   the entire entity-body is used.
+            if int(stop) > content_length:
+                result.append((0, content_length))
+            else:
+                result.append((content_length - int(stop), content_length))
+
+    return result
+
+
+class HeaderElement(object):
+
+    """An element (with parameters) from an HTTP header's element list."""
+
+    def __init__(self, value, params=None):
+        self.value = value
+        if params is None:
+            params = {}
+        self.params = params
+
+    def __cmp__(self, other):
+        return builtins.cmp(self.value, other.value)
+
+    def __lt__(self, other):
+        return self.value < other.value
+
+    def __str__(self):
+        p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)]
+        return str('%s%s' % (self.value, ''.join(p)))
+
+    def __bytes__(self):
+        return ntob(self.__str__())
+
+    def __unicode__(self):
+        return ntou(self.__str__())
+
+    @staticmethod
+    def parse(elementstr):
+        """Transform 'token;key=val' to ('token', {'key': 'val'})."""
+        initial_value, params = parse_header(elementstr)
+        return initial_value, params
+
+    @classmethod
+    def from_str(cls, elementstr):
+        """Construct an instance from a string of the form 'token;key=val'."""
+        ival, params = cls.parse(elementstr)
+        return cls(ival, params)
+
+
+q_separator = re.compile(r'; *q *=')
+
+
+class AcceptElement(HeaderElement):
+
+    """An element (with parameters) from an Accept* header's element list.
+
+    AcceptElement objects are comparable; the more-preferred object will be
+    "less than" the less-preferred object. They are also therefore sortable;
+    if you sort a list of AcceptElement objects, they will be listed in
+    priority order; the most preferred value will be first. Yes, it should
+    have been the other way around, but it's too late to fix now.
+    """
+
+    @classmethod
+    def from_str(cls, elementstr):
+        qvalue = None
+        # The first "q" parameter (if any) separates the initial
+        # media-range parameter(s) (if any) from the accept-params.
+        atoms = q_separator.split(elementstr, 1)
+        media_range = atoms.pop(0).strip()
+        if atoms:
+            # The qvalue for an Accept header can have extensions. The other
+            # headers cannot, but it's easier to parse them as if they did.
+            qvalue = HeaderElement.from_str(atoms[0].strip())
+
+        media_type, params = cls.parse(media_range)
+        if qvalue is not None:
+            params['q'] = qvalue
+        return cls(media_type, params)
+
+    @property
+    def qvalue(self):
+        'The qvalue, or priority, of this value.'
+        val = self.params.get('q', '1')
+        if isinstance(val, HeaderElement):
+            val = val.value
+        try:
+            return float(val)
+        except ValueError as val_err:
+            """Fail client requests with invalid quality value.
+
+            Ref: https://github.com/cherrypy/cherrypy/issues/1370
+            """
+            six.raise_from(
+                cherrypy.HTTPError(
+                    400,
+                    'Malformed HTTP header: `{}`'.
+                    format(str(self)),
+                ),
+                val_err,
+            )
+
+    def __cmp__(self, other):
+        diff = builtins.cmp(self.qvalue, other.qvalue)
+        if diff == 0:
+            diff = builtins.cmp(str(self), str(other))
+        return diff
+
+    def __lt__(self, other):
+        if self.qvalue == other.qvalue:
+            return str(self) < str(other)
+        else:
+            return self.qvalue < other.qvalue
+
+
+RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
+
+
+def header_elements(fieldname, fieldvalue):
+    """Return a sorted HeaderElement list from a comma-separated header string.
+    """
+    if not fieldvalue:
+        return []
+
+    result = []
+    for element in RE_HEADER_SPLIT.split(fieldvalue):
+        if fieldname.startswith('Accept') or fieldname == 'TE':
+            hv = AcceptElement.from_str(element)
+        else:
+            hv = HeaderElement.from_str(element)
+        result.append(hv)
+
+    return list(reversed(sorted(result)))
+
+
+def decode_TEXT(value):
+    r"""
+    Decode :rfc:`2047` TEXT
+
+    >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1')
+    True
+    """
+    atoms = decode_header(value)
+    decodedvalue = ''
+    for atom, charset in atoms:
+        if charset is not None:
+            atom = atom.decode(charset)
+        decodedvalue += atom
+    return decodedvalue
+
+
+def decode_TEXT_maybe(value):
+    """
+    Decode the text but only if '=?' appears in it.
+    """
+    return decode_TEXT(value) if '=?' in value else value
+
+
+def valid_status(status):
+    """Return legal HTTP status Code, Reason-phrase and Message.
+
+    The status arg must be an int, a str that begins with an int
+    or the constant from ``http.client`` stdlib module.
+
+    If status has no reason-phrase is supplied, a default reason-
+    phrase will be provided.
+
+    >>> from six.moves import http_client
+    >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
+    >>> valid_status(http_client.ACCEPTED) == (
+    ...     int(http_client.ACCEPTED),
+    ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED]
+    True
+    """
+
+    if not status:
+        status = 200
+
+    code, reason = status, None
+    if isinstance(status, six.string_types):
+        code, _, reason = status.partition(' ')
+        reason = reason.strip() or None
+
+    try:
+        code = int(code)
+    except (TypeError, ValueError):
+        raise ValueError('Illegal response status from server '
+                         '(%s is non-numeric).' % repr(code))
+
+    if code < 100 or code > 599:
+        raise ValueError('Illegal response status from server '
+                         '(%s is out of range).' % repr(code))
+
+    if code not in response_codes:
+        # code is unknown but not illegal
+        default_reason, message = '', ''
+    else:
+        default_reason, message = response_codes[code]
+
+    if reason is None:
+        reason = default_reason
+
+    return code, reason, message
+
+
+# NOTE: the parse_qs functions that follow are modified version of those
+# in the python3.0 source - we need to pass through an encoding to the unquote
+# method, but the default parse_qs function doesn't allow us to.  These do.
+
+def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
+    """Parse a query given as a string argument.
+
+    Arguments:
+
+    qs: URL-encoded query string to be parsed
+
+    keep_blank_values: flag indicating whether blank values in
+        URL encoded queries should be treated as blank strings.  A
+        true value indicates that blanks should be retained as blank
+        strings.  The default false value indicates that blank values
+        are to be ignored and treated as if they were  not included.
+
+    strict_parsing: flag indicating what to do with parsing errors. If
+        false (the default), errors are silently ignored. If true,
+        errors raise a ValueError exception.
+
+    Returns a dict, as G-d intended.
+    """
+    pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
+    d = {}
+    for name_value in pairs:
+        if not name_value and not strict_parsing:
+            continue
+        nv = name_value.split('=', 1)
+        if len(nv) != 2:
+            if strict_parsing:
+                raise ValueError('bad query field: %r' % (name_value,))
+            # Handle case of a control-name with no equal sign
+            if keep_blank_values:
+                nv.append('')
+            else:
+                continue
+        if len(nv[1]) or keep_blank_values:
+            name = unquote_plus(nv[0], encoding, errors='strict')
+            value = unquote_plus(nv[1], encoding, errors='strict')
+            if name in d:
+                if not isinstance(d[name], list):
+                    d[name] = [d[name]]
+                d[name].append(value)
+            else:
+                d[name] = value
+    return d
+
+
+image_map_pattern = re.compile(r'[0-9]+,[0-9]+')
+
+
+def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
+    """Build a params dictionary from a query_string.
+
+    Duplicate key/value pairs in the provided query_string will be
+    returned as {'key': [val1, val2, ...]}. Single key/values will
+    be returned as strings: {'key': 'value'}.
+    """
+    if image_map_pattern.match(query_string):
+        # Server-side image map. Map the coords to 'x' and 'y'
+        # (like CGI::Request does).
+        pm = query_string.split(',')
+        pm = {'x': int(pm[0]), 'y': int(pm[1])}
+    else:
+        pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
+    return pm
+
+
+####
+# Inlined from jaraco.collections 1.5.2
+# Ref #1673
+class KeyTransformingDict(dict):
+    """
+    A dict subclass that transforms the keys before they're used.
+    Subclasses may override the default transform_key to customize behavior.
+    """
+    @staticmethod
+    def transform_key(key):
+        return key
+
+    def __init__(self, *args, **kargs):
+        super(KeyTransformingDict, self).__init__()
+        # build a dictionary using the default constructs
+        d = dict(*args, **kargs)
+        # build this dictionary using transformed keys.
+        for item in d.items():
+            self.__setitem__(*item)
+
+    def __setitem__(self, key, val):
+        key = self.transform_key(key)
+        super(KeyTransformingDict, self).__setitem__(key, val)
+
+    def __getitem__(self, key):
+        key = self.transform_key(key)
+        return super(KeyTransformingDict, self).__getitem__(key)
+
+    def __contains__(self, key):
+        key = self.transform_key(key)
+        return super(KeyTransformingDict, self).__contains__(key)
+
+    def __delitem__(self, key):
+        key = self.transform_key(key)
+        return super(KeyTransformingDict, self).__delitem__(key)
+
+    def get(self, key, *args, **kwargs):
+        key = self.transform_key(key)
+        return super(KeyTransformingDict, self).get(key, *args, **kwargs)
+
+    def setdefault(self, key, *args, **kwargs):
+        key = self.transform_key(key)
+        return super(KeyTransformingDict, self).setdefault(
+            key, *args, **kwargs)
+
+    def pop(self, key, *args, **kwargs):
+        key = self.transform_key(key)
+        return super(KeyTransformingDict, self).pop(key, *args, **kwargs)
+
+    def matching_key_for(self, key):
+        """
+        Given a key, return the actual key stored in self that matches.
+        Raise KeyError if the key isn't found.
+        """
+        try:
+            return next(e_key for e_key in self.keys() if e_key == key)
+        except StopIteration:
+            raise KeyError(key)
+####
+
+
+class CaseInsensitiveDict(KeyTransformingDict):
+
+    """A case-insensitive dict subclass.
+
+    Each key is changed on entry to str(key).title().
+    """
+
+    @staticmethod
+    def transform_key(key):
+        return str(key).title()
+
+
+#   TEXT = <any OCTET except CTLs, but including LWS>
+#
+# A CRLF is allowed in the definition of TEXT only as part of a header
+# field continuation. It is expected that the folding LWS will be
+# replaced with a single SP before interpretation of the TEXT value."
+if str == bytes:
+    header_translate_table = ''.join([chr(i) for i in range(256)])
+    header_translate_deletechars = ''.join(
+        [chr(i) for i in range(32)]) + chr(127)
+else:
+    header_translate_table = None
+    header_translate_deletechars = bytes(range(32)) + bytes([127])
+
+
+class HeaderMap(CaseInsensitiveDict):
+
+    """A dict subclass for HTTP request and response headers.
+
+    Each key is changed on entry to str(key).title(). This allows headers
+    to be case-insensitive and avoid duplicates.
+
+    Values are header values (decoded according to :rfc:`2047` if necessary).
+    """
+
+    protocol = (1, 1)
+    encodings = ['ISO-8859-1']
+
+    # Someday, when http-bis is done, this will probably get dropped
+    # since few servers, clients, or intermediaries do it. But until then,
+    # we're going to obey the spec as is.
+    # "Words of *TEXT MAY contain characters from character sets other than
+    # ISO-8859-1 only when encoded according to the rules of RFC 2047."
+    use_rfc_2047 = True
+
+    def elements(self, key):
+        """Return a sorted list of HeaderElements for the given header."""
+        key = str(key).title()
+        value = self.get(key)
+        return header_elements(key, value)
+
+    def values(self, key):
+        """Return a sorted list of HeaderElement.value for the given header."""
+        return [e.value for e in self.elements(key)]
+
+    def output(self):
+        """Transform self into a list of (name, value) tuples."""
+        return list(self.encode_header_items(self.items()))
+
+    @classmethod
+    def encode_header_items(cls, header_items):
+        """
+        Prepare the sequence of name, value tuples into a form suitable for
+        transmitting on the wire for HTTP.
+        """
+        for k, v in header_items:
+            if not isinstance(v, six.string_types):
+                v = six.text_type(v)
+
+            yield tuple(map(cls.encode_header_item, (k, v)))
+
+    @classmethod
+    def encode_header_item(cls, item):
+        if isinstance(item, six.text_type):
+            item = cls.encode(item)
+
+        # See header_translate_* constants above.
+        # Replace only if you really know what you're doing.
+        return item.translate(
+            header_translate_table, header_translate_deletechars)
+
+    @classmethod
+    def encode(cls, v):
+        """Return the given header name or value, encoded for HTTP output."""
+        for enc in cls.encodings:
+            try:
+                return v.encode(enc)
+            except UnicodeEncodeError:
+                continue
+
+        if cls.protocol == (1, 1) and cls.use_rfc_2047:
+            # Encode RFC-2047 TEXT
+            # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
+            # We do our own here instead of using the email module
+            # because we never want to fold lines--folding has
+            # been deprecated by the HTTP working group.
+            v = b2a_base64(v.encode('utf-8'))
+            return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=')
+
+        raise ValueError('Could not encode header part %r using '
+                         'any of the encodings %r.' %
+                         (v, cls.encodings))
+
+
+class Host(object):
+
+    """An internet address.
+
+    name
+        Should be the client's host name. If not available (because no DNS
+        lookup is performed), the IP address should be used instead.
+
+    """
+
+    ip = '0.0.0.0'
+    port = 80
+    name = 'unknown.tld'
+
+    def __init__(self, ip, port, name=None):
+        self.ip = ip
+        self.port = port
+        if name is None:
+            name = ip
+        self.name = name
+
+    def __repr__(self):
+        return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)
diff --git a/libraries/cherrypy/lib/jsontools.py b/libraries/cherrypy/lib/jsontools.py
new file mode 100644
index 00000000..48683097
--- /dev/null
+++ b/libraries/cherrypy/lib/jsontools.py
@@ -0,0 +1,88 @@
+import cherrypy
+from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode
+
+
+def json_processor(entity):
+    """Read application/json data into request.json."""
+    if not entity.headers.get(ntou('Content-Length'), ntou('')):
+        raise cherrypy.HTTPError(411)
+
+    body = entity.fp.read()
+    with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
+        cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
+
+
+def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
+            force=True, debug=False, processor=json_processor):
+    """Add a processor to parse JSON request entities:
+    The default processor places the parsed data into request.json.
+
+    Incoming request entities which match the given content_type(s) will
+    be deserialized from JSON to the Python equivalent, and the result
+    stored at cherrypy.request.json. The 'content_type' argument may
+    be a Content-Type string or a list of allowable Content-Type strings.
+
+    If the 'force' argument is True (the default), then entities of other
+    content types will not be allowed; "415 Unsupported Media Type" is
+    raised instead.
+
+    Supply your own processor to use a custom decoder, or to handle the parsed
+    data differently.  The processor can be configured via
+    tools.json_in.processor or via the decorator method.
+
+    Note that the deserializer requires the client send a Content-Length
+    request header, or it will raise "411 Length Required". If for any
+    other reason the request entity cannot be deserialized from JSON,
+    it will raise "400 Bad Request: Invalid JSON document".
+    """
+    request = cherrypy.serving.request
+    if isinstance(content_type, text_or_bytes):
+        content_type = [content_type]
+
+    if force:
+        if debug:
+            cherrypy.log('Removing body processors %s' %
+                         repr(request.body.processors.keys()), 'TOOLS.JSON_IN')
+        request.body.processors.clear()
+        request.body.default_proc = cherrypy.HTTPError(
+            415, 'Expected an entity of content type %s' %
+            ', '.join(content_type))
+
+    for ct in content_type:
+        if debug:
+            cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
+        request.body.processors[ct] = processor
+
+
+def json_handler(*args, **kwargs):
+    value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
+    return json_encode(value)
+
+
+def json_out(content_type='application/json', debug=False,
+             handler=json_handler):
+    """Wrap request.handler to serialize its output to JSON. Sets Content-Type.
+
+    If the given content_type is None, the Content-Type response header
+    is not set.
+
+    Provide your own handler to use a custom encoder.  For example
+    cherrypy.config['tools.json_out.handler'] = <function>, or
+    @json_out(handler=function).
+    """
+    request = cherrypy.serving.request
+    # request.handler may be set to None by e.g. the caching tool
+    # to signal to all components that a response body has already
+    # been attached, in which case we don't need to wrap anything.
+    if request.handler is None:
+        return
+    if debug:
+        cherrypy.log('Replacing %s with JSON handler' % request.handler,
+                     'TOOLS.JSON_OUT')
+    request._json_inner_handler = request.handler
+    request.handler = handler
+    if content_type is not None:
+        if debug:
+            cherrypy.log('Setting Content-Type to %s' %
+                         content_type, 'TOOLS.JSON_OUT')
+        cherrypy.serving.response.headers['Content-Type'] = content_type
diff --git a/libraries/cherrypy/lib/locking.py b/libraries/cherrypy/lib/locking.py
new file mode 100644
index 00000000..317fb58c
--- /dev/null
+++ b/libraries/cherrypy/lib/locking.py
@@ -0,0 +1,47 @@
+import datetime
+
+
+class NeverExpires(object):
+    def expired(self):
+        return False
+
+
+class Timer(object):
+    """
+    A simple timer that will indicate when an expiration time has passed.
+    """
+    def __init__(self, expiration):
+        'Create a timer that expires at `expiration` (UTC datetime)'
+        self.expiration = expiration
+
+    @classmethod
+    def after(cls, elapsed):
+        """
+        Return a timer that will expire after `elapsed` passes.
+        """
+        return cls(datetime.datetime.utcnow() + elapsed)
+
+    def expired(self):
+        return datetime.datetime.utcnow() >= self.expiration
+
+
+class LockTimeout(Exception):
+    'An exception when a lock could not be acquired before a timeout period'
+
+
+class LockChecker(object):
+    """
+    Keep track of the time and detect if a timeout has expired
+    """
+    def __init__(self, session_id, timeout):
+        self.session_id = session_id
+        if timeout:
+            self.timer = Timer.after(timeout)
+        else:
+            self.timer = NeverExpires()
+
+    def expired(self):
+        if self.timer.expired():
+            raise LockTimeout(
+                'Timeout acquiring lock for %(session_id)s' % vars(self))
+        return False
diff --git a/libraries/cherrypy/lib/profiler.py b/libraries/cherrypy/lib/profiler.py
new file mode 100644
index 00000000..fccf2eb8
--- /dev/null
+++ b/libraries/cherrypy/lib/profiler.py
@@ -0,0 +1,221 @@
+"""Profiler tools for CherryPy.
+
+CherryPy users
+==============
+
+You can profile any of your pages as follows::
+
+    from cherrypy.lib import profiler
+
+    class Root:
+        p = profiler.Profiler("/path/to/profile/dir")
+
+        @cherrypy.expose
+        def index(self):
+            self.p.run(self._index)
+
+        def _index(self):
+            return "Hello, world!"
+
+    cherrypy.tree.mount(Root())
+
+You can also turn on profiling for all requests
+using the ``make_app`` function as WSGI middleware.
+
+CherryPy developers
+===================
+
+This module can be used whenever you make changes to CherryPy,
+to get a quick sanity-check on overall CP performance. Use the
+``--profile`` flag when running the test suite. Then, use the ``serve()``
+function to browse the results in a web browser. If you run this
+module from the command line, it will call ``serve()`` for you.
+
+"""
+
+import io
+import os
+import os.path
+import sys
+import warnings
+
+import cherrypy
+
+
+try:
+    import profile
+    import pstats
+
+    def new_func_strip_path(func_name):
+        """Make profiler output more readable by adding `__init__` modules' parents
+        """
+        filename, line, name = func_name
+        if filename.endswith('__init__.py'):
+            return (
+                os.path.basename(filename[:-12]) + filename[-12:],
+                line,
+                name,
+            )
+        return os.path.basename(filename), line, name
+
+    pstats.func_strip_path = new_func_strip_path
+except ImportError:
+    profile = None
+    pstats = None
+
+
+_count = 0
+
+
+class Profiler(object):
+
+    def __init__(self, path=None):
+        if not path:
+            path = os.path.join(os.path.dirname(__file__), 'profile')
+        self.path = path
+        if not os.path.exists(path):
+            os.makedirs(path)
+
+    def run(self, func, *args, **params):
+        """Dump profile data into self.path."""
+        global _count
+        c = _count = _count + 1
+        path = os.path.join(self.path, 'cp_%04d.prof' % c)
+        prof = profile.Profile()
+        result = prof.runcall(func, *args, **params)
+        prof.dump_stats(path)
+        return result
+
+    def statfiles(self):
+        """:rtype: list of available profiles.
+        """
+        return [f for f in os.listdir(self.path)
+                if f.startswith('cp_') and f.endswith('.prof')]
+
+    def stats(self, filename, sortby='cumulative'):
+        """:rtype stats(index): output of print_stats() for the given profile.
+        """
+        sio = io.StringIO()
+        if sys.version_info >= (2, 5):
+            s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
+            s.strip_dirs()
+            s.sort_stats(sortby)
+            s.print_stats()
+        else:
+            # pstats.Stats before Python 2.5 didn't take a 'stream' arg,
+            # but just printed to stdout. So re-route stdout.
+            s = pstats.Stats(os.path.join(self.path, filename))
+            s.strip_dirs()
+            s.sort_stats(sortby)
+            oldout = sys.stdout
+            try:
+                sys.stdout = sio
+                s.print_stats()
+            finally:
+                sys.stdout = oldout
+        response = sio.getvalue()
+        sio.close()
+        return response
+
+    @cherrypy.expose
+    def index(self):
+        return """<html>
+        <head><title>CherryPy profile data</title></head>
+        <frameset cols='200, 1*'>
+            <frame src='menu' />
+            <frame name='main' src='' />
+        </frameset>
+        </html>
+        """
+
+    @cherrypy.expose
+    def menu(self):
+        yield '<h2>Profiling runs</h2>'
+        yield '<p>Click on one of the runs below to see profiling data.</p>'
+        runs = self.statfiles()
+        runs.sort()
+        for i in runs:
+            yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
+                i, i)
+
+    @cherrypy.expose
+    def report(self, filename):
+        cherrypy.response.headers['Content-Type'] = 'text/plain'
+        return self.stats(filename)
+
+
+class ProfileAggregator(Profiler):
+
+    def __init__(self, path=None):
+        Profiler.__init__(self, path)
+        global _count
+        self.count = _count = _count + 1
+        self.profiler = profile.Profile()
+
+    def run(self, func, *args, **params):
+        path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
+        result = self.profiler.runcall(func, *args, **params)
+        self.profiler.dump_stats(path)
+        return result
+
+
+class make_app:
+
+    def __init__(self, nextapp, path=None, aggregate=False):
+        """Make a WSGI middleware app which wraps 'nextapp' with profiling.
+
+        nextapp
+            the WSGI application to wrap, usually an instance of
+            cherrypy.Application.
+
+        path
+            where to dump the profiling output.
+
+        aggregate
+            if True, profile data for all HTTP requests will go in
+            a single file. If False (the default), each HTTP request will
+            dump its profile data into a separate file.
+
+        """
+        if profile is None or pstats is None:
+            msg = ('Your installation of Python does not have a profile '
+                   "module. If you're on Debian, try "
+                   '`sudo apt-get install python-profiler`. '
+                   'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
+                   'for details.')
+            warnings.warn(msg)
+
+        self.nextapp = nextapp
+        self.aggregate = aggregate
+        if aggregate:
+            self.profiler = ProfileAggregator(path)
+        else:
+            self.profiler = Profiler(path)
+
+    def __call__(self, environ, start_response):
+        def gather():
+            result = []
+            for line in self.nextapp(environ, start_response):
+                result.append(line)
+            return result
+        return self.profiler.run(gather)
+
+
+def serve(path=None, port=8080):
+    if profile is None or pstats is None:
+        msg = ('Your installation of Python does not have a profile module. '
+               "If you're on Debian, try "
+               '`sudo apt-get install python-profiler`. '
+               'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
+               'for details.')
+        warnings.warn(msg)
+
+    cherrypy.config.update({'server.socket_port': int(port),
+                            'server.thread_pool': 10,
+                            'environment': 'production',
+                            })
+    cherrypy.quickstart(Profiler(path))
+
+
+if __name__ == '__main__':
+    serve(*tuple(sys.argv[1:]))
diff --git a/libraries/cherrypy/lib/reprconf.py b/libraries/cherrypy/lib/reprconf.py
new file mode 100644
index 00000000..291ab663
--- /dev/null
+++ b/libraries/cherrypy/lib/reprconf.py
@@ -0,0 +1,514 @@
+"""Generic configuration system using unrepr.
+
+Configuration data may be supplied as a Python dictionary, as a filename,
+or as an open file object. When you supply a filename or file, Python's
+builtin ConfigParser is used (with some extensions).
+
+Namespaces
+----------
+
+Configuration keys are separated into namespaces by the first "." in the key.
+
+The only key that cannot exist in a namespace is the "environment" entry.
+This special entry 'imports' other config entries from a template stored in
+the Config.environments dict.
+
+You can define your own namespaces to be called when new config is merged
+by adding a named handler to Config.namespaces. The name can be any string,
+and the handler must be either a callable or a context manager.
+"""
+
+from cherrypy._cpcompat import text_or_bytes
+from six.moves import configparser
+from six.moves import builtins
+
+import operator
+import sys
+
+
+class NamespaceSet(dict):
+
+    """A dict of config namespace names and handlers.
+
+    Each config entry should begin with a namespace name; the corresponding
+    namespace handler will be called once for each config entry in that
+    namespace, and will be passed two arguments: the config key (with the
+    namespace removed) and the config value.
+
+    Namespace handlers may be any Python callable; they may also be
+    Python 2.5-style 'context managers', in which case their __enter__
+    method should return a callable to be used as the handler.
+    See cherrypy.tools (the Toolbox class) for an example.
+    """
+
+    def __call__(self, config):
+        """Iterate through config and pass it to each namespace handler.
+
+        config
+            A flat dict, where keys use dots to separate
+            namespaces, and values are arbitrary.
+
+        The first name in each config key is used to look up the corresponding
+        namespace handler. For example, a config entry of {'tools.gzip.on': v}
+        will call the 'tools' namespace handler with the args: ('gzip.on', v)
+        """
+        # Separate the given config into namespaces
+        ns_confs = {}
+        for k in config:
+            if '.' in k:
+                ns, name = k.split('.', 1)
+                bucket = ns_confs.setdefault(ns, {})
+                bucket[name] = config[k]
+
+        # I chose __enter__ and __exit__ so someday this could be
+        # rewritten using Python 2.5's 'with' statement:
+        # for ns, handler in six.iteritems(self):
+        #     with handler as callable:
+        #         for k, v in six.iteritems(ns_confs.get(ns, {})):
+        #             callable(k, v)
+        for ns, handler in self.items():
+            exit = getattr(handler, '__exit__', None)
+            if exit:
+                callable = handler.__enter__()
+                no_exc = True
+                try:
+                    try:
+                        for k, v in ns_confs.get(ns, {}).items():
+                            callable(k, v)
+                    except Exception:
+                        # The exceptional case is handled here
+                        no_exc = False
+                        if exit is None:
+                            raise
+                        if not exit(*sys.exc_info()):
+                            raise
+                        # The exception is swallowed if exit() returns true
+                finally:
+                    # The normal and non-local-goto cases are handled here
+                    if no_exc and exit:
+                        exit(None, None, None)
+            else:
+                for k, v in ns_confs.get(ns, {}).items():
+                    handler(k, v)
+
+    def __repr__(self):
+        return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
+                              dict.__repr__(self))
+
+    def __copy__(self):
+        newobj = self.__class__()
+        newobj.update(self)
+        return newobj
+    copy = __copy__
+
+
+class Config(dict):
+
+    """A dict-like set of configuration data, with defaults and namespaces.
+
+    May take a file, filename, or dict.
+    """
+
+    defaults = {}
+    environments = {}
+    namespaces = NamespaceSet()
+
+    def __init__(self, file=None, **kwargs):
+        self.reset()
+        if file is not None:
+            self.update(file)
+        if kwargs:
+            self.update(kwargs)
+
+    def reset(self):
+        """Reset self to default values."""
+        self.clear()
+        dict.update(self, self.defaults)
+
+    def update(self, config):
+        """Update self from a dict, file, or filename."""
+        self._apply(Parser.load(config))
+
+    def _apply(self, config):
+        """Update self from a dict."""
+        which_env = config.get('environment')
+        if which_env:
+            env = self.environments[which_env]
+            for k in env:
+                if k not in config:
+                    config[k] = env[k]
+
+        dict.update(self, config)
+        self.namespaces(config)
+
+    def __setitem__(self, k, v):
+        dict.__setitem__(self, k, v)
+        self.namespaces({k: v})
+
+
+class Parser(configparser.ConfigParser):
+
+    """Sub-class of ConfigParser that keeps the case of options and that
+    raises an exception if the file cannot be read.
+    """
+
+    def optionxform(self, optionstr):
+        return optionstr
+
+    def read(self, filenames):
+        if isinstance(filenames, text_or_bytes):
+            filenames = [filenames]
+        for filename in filenames:
+            # try:
+            #     fp = open(filename)
+            # except IOError:
+            #     continue
+            fp = open(filename)
+            try:
+                self._read(fp, filename)
+            finally:
+                fp.close()
+
+    def as_dict(self, raw=False, vars=None):
+        """Convert an INI file to a dictionary"""
+        # Load INI file into a dict
+        result = {}
+        for section in self.sections():
+            if section not in result:
+                result[section] = {}
+            for option in self.options(section):
+                value = self.get(section, option, raw=raw, vars=vars)
+                try:
+                    value = unrepr(value)
+                except Exception:
+                    x = sys.exc_info()[1]
+                    msg = ('Config error in section: %r, option: %r, '
+                           'value: %r. Config values must be valid Python.' %
+                           (section, option, value))
+                    raise ValueError(msg, x.__class__.__name__, x.args)
+                result[section][option] = value
+        return result
+
+    def dict_from_file(self, file):
+        if hasattr(file, 'read'):
+            self.readfp(file)
+        else:
+            self.read(file)
+        return self.as_dict()
+
+    @classmethod
+    def load(self, input):
+        """Resolve 'input' to dict from a dict, file, or filename."""
+        is_file = (
+            # Filename
+            isinstance(input, text_or_bytes)
+            # Open file object
+            or hasattr(input, 'read')
+        )
+        return Parser().dict_from_file(input) if is_file else input.copy()
+
+
+# public domain "unrepr" implementation, found on the web and then improved.
+
+
+class _Builder2:
+
+    def build(self, o):
+        m = getattr(self, 'build_' + o.__class__.__name__, None)
+        if m is None:
+            raise TypeError('unrepr does not recognize %s' %
+                            repr(o.__class__.__name__))
+        return m(o)
+
+    def astnode(self, s):
+        """Return a Python2 ast Node compiled from a string."""
+        try:
+            import compiler
+        except ImportError:
+            # Fallback to eval when compiler package is not available,
+            # e.g. IronPython 1.0.
+            return eval(s)
+
+        p = compiler.parse('__tempvalue__ = ' + s)
+        return p.getChildren()[1].getChildren()[0].getChildren()[1]
+
+    def build_Subscript(self, o):
+        expr, flags, subs = o.getChildren()
+        expr = self.build(expr)
+        subs = self.build(subs)
+        return expr[subs]
+
+    def build_CallFunc(self, o):
+        children = o.getChildren()
+        # Build callee from first child
+        callee = self.build(children[0])
+        # Build args and kwargs from remaining children
+        args = []
+        kwargs = {}
+        for child in children[1:]:
+            class_name = child.__class__.__name__
+            # None is ignored
+            if class_name == 'NoneType':
+                continue
+            # Keywords become kwargs
+            if class_name == 'Keyword':
+                kwargs.update(self.build(child))
+            # Everything else becomes args
+            else:
+                args.append(self.build(child))
+
+        return callee(*args, **kwargs)
+
+    def build_Keyword(self, o):
+        key, value_obj = o.getChildren()
+        value = self.build(value_obj)
+        kw_dict = {key: value}
+        return kw_dict
+
+    def build_List(self, o):
+        return map(self.build, o.getChildren())
+
+    def build_Const(self, o):
+        return o.value
+
+    def build_Dict(self, o):
+        d = {}
+        i = iter(map(self.build, o.getChildren()))
+        for el in i:
+            d[el] = i.next()
+        return d
+
+    def build_Tuple(self, o):
+        return tuple(self.build_List(o))
+
+    def build_Name(self, o):
+        name = o.name
+        if name == 'None':
+            return None
+        if name == 'True':
+            return True
+        if name == 'False':
+            return False
+
+        # See if the Name is a package or module. If it is, import it.
+        try:
+            return modules(name)
+        except ImportError:
+            pass
+
+        # See if the Name is in builtins.
+        try:
+            return getattr(builtins, name)
+        except AttributeError:
+            pass
+
+        raise TypeError('unrepr could not resolve the name %s' % repr(name))
+
+    def build_Add(self, o):
+        left, right = map(self.build, o.getChildren())
+        return left + right
+
+    def build_Mul(self, o):
+        left, right = map(self.build, o.getChildren())
+        return left * right
+
+    def build_Getattr(self, o):
+        parent = self.build(o.expr)
+        return getattr(parent, o.attrname)
+
+    def build_NoneType(self, o):
+        return None
+
+    def build_UnarySub(self, o):
+        return -self.build(o.getChildren()[0])
+
+    def build_UnaryAdd(self, o):
+        return self.build(o.getChildren()[0])
+
+
+class _Builder3:
+
+    def build(self, o):
+        m = getattr(self, 'build_' + o.__class__.__name__, None)
+        if m is None:
+            raise TypeError('unrepr does not recognize %s' %
+                            repr(o.__class__.__name__))
+        return m(o)
+
+    def astnode(self, s):
+        """Return a Python3 ast Node compiled from a string."""
+        try:
+            import ast
+        except ImportError:
+            # Fallback to eval when ast package is not available,
+            # e.g. IronPython 1.0.
+            return eval(s)
+
+        p = ast.parse('__tempvalue__ = ' + s)
+        return p.body[0].value
+
+    def build_Subscript(self, o):
+        return self.build(o.value)[self.build(o.slice)]
+
+    def build_Index(self, o):
+        return self.build(o.value)
+
+    def _build_call35(self, o):
+        """
+        Workaround for python 3.5 _ast.Call signature, docs found here
+        https://greentreesnakes.readthedocs.org/en/latest/nodes.html
+        """
+        import ast
+        callee = self.build(o.func)
+        args = []
+        if o.args is not None:
+            for a in o.args:
+                if isinstance(a, ast.Starred):
+                    args.append(self.build(a.value))
+                else:
+                    args.append(self.build(a))
+        kwargs = {}
+        for kw in o.keywords:
+            if kw.arg is None:  # double asterix `**`
+                rst = self.build(kw.value)
+                if not isinstance(rst, dict):
+                    raise TypeError('Invalid argument for call.'
+                                    'Must be a mapping object.')
+                # give preference to the keys set directly from arg=value
+                for k, v in rst.items():
+                    if k not in kwargs:
+                        kwargs[k] = v
+            else:  # defined on the call as: arg=value
+                kwargs[kw.arg] = self.build(kw.value)
+        return callee(*args, **kwargs)
+
+    def build_Call(self, o):
+        if sys.version_info >= (3, 5):
+            return self._build_call35(o)
+
+        callee = self.build(o.func)
+
+        if o.args is None:
+            args = ()
+        else:
+            args = tuple([self.build(a) for a in o.args])
+
+        if o.starargs is None:
+            starargs = ()
+        else:
+            starargs = tuple(self.build(o.starargs))
+
+        if o.kwargs is None:
+            kwargs = {}
+        else:
+            kwargs = self.build(o.kwargs)
+        if o.keywords is not None:  # direct a=b keywords
+            for kw in o.keywords:
+                # preference because is a direct keyword against **kwargs
+                kwargs[kw.arg] = self.build(kw.value)
+        return callee(*(args + starargs), **kwargs)
+
+    def build_List(self, o):
+        return list(map(self.build, o.elts))
+
+    def build_Str(self, o):
+        return o.s
+
+    def build_Num(self, o):
+        return o.n
+
+    def build_Dict(self, o):
+        return dict([(self.build(k), self.build(v))
+                     for k, v in zip(o.keys, o.values)])
+
+    def build_Tuple(self, o):
+        return tuple(self.build_List(o))
+
+    def build_Name(self, o):
+        name = o.id
+        if name == 'None':
+            return None
+        if name == 'True':
+            return True
+        if name == 'False':
+            return False
+
+        # See if the Name is a package or module. If it is, import it.
+        try:
+            return modules(name)
+        except ImportError:
+            pass
+
+        # See if the Name is in builtins.
+        try:
+            import builtins
+            return getattr(builtins, name)
+        except AttributeError:
+            pass
+
+        raise TypeError('unrepr could not resolve the name %s' % repr(name))
+
+    def build_NameConstant(self, o):
+        return o.value
+
+    def build_UnaryOp(self, o):
+        op, operand = map(self.build, [o.op, o.operand])
+        return op(operand)
+
+    def build_BinOp(self, o):
+        left, op, right = map(self.build, [o.left, o.op, o.right])
+        return op(left, right)
+
+    def build_Add(self, o):
+        return operator.add
+
+    def build_Mult(self, o):
+        return operator.mul
+
+    def build_USub(self, o):
+        return operator.neg
+
+    def build_Attribute(self, o):
+        parent = self.build(o.value)
+        return getattr(parent, o.attr)
+
+    def build_NoneType(self, o):
+        return None
+
+
+def unrepr(s):
+    """Return a Python object compiled from a string."""
+    if not s:
+        return s
+    if sys.version_info < (3, 0):
+        b = _Builder2()
+    else:
+        b = _Builder3()
+    obj = b.astnode(s)
+    return b.build(obj)
+
+
+def modules(modulePath):
+    """Load a module and retrieve a reference to that module."""
+    __import__(modulePath)
+    return sys.modules[modulePath]
+
+
+def attributes(full_attribute_name):
+    """Load a module and retrieve an attribute of that module."""
+
+    # Parse out the path, module, and attribute
+    last_dot = full_attribute_name.rfind('.')
+    attr_name = full_attribute_name[last_dot + 1:]
+    mod_path = full_attribute_name[:last_dot]
+
+    mod = modules(mod_path)
+    # Let an AttributeError propagate outward.
+    try:
+        attr = getattr(mod, attr_name)
+    except AttributeError:
+        raise AttributeError("'%s' object has no attribute '%s'"
+                             % (mod_path, attr_name))
+
+    # Return a reference to the attribute.
+    return attr
diff --git a/libraries/cherrypy/lib/sessions.py b/libraries/cherrypy/lib/sessions.py
new file mode 100644
index 00000000..5b49ee13
--- /dev/null
+++ b/libraries/cherrypy/lib/sessions.py
@@ -0,0 +1,919 @@
+"""Session implementation for CherryPy.
+
+You need to edit your config file to use sessions. Here's an example::
+
+    [/]
+    tools.sessions.on = True
+    tools.sessions.storage_class = cherrypy.lib.sessions.FileSession
+    tools.sessions.storage_path = "/home/site/sessions"
+    tools.sessions.timeout = 60
+
+This sets the session to be stored in files in the directory
+/home/site/sessions, and the session timeout to 60 minutes. If you omit
+``storage_class``, the sessions will be saved in RAM.
+``tools.sessions.on`` is the only required line for working sessions,
+the rest are optional.
+
+By default, the session ID is passed in a cookie, so the client's browser must
+have cookies enabled for your site.
+
+To set data for the current session, use
+``cherrypy.session['fieldname'] = 'fieldvalue'``;
+to get data use ``cherrypy.session.get('fieldname')``.
+
+================
+Locking sessions
+================
+
+By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
+the session is locked early and unlocked late. Be mindful of this default mode
+for any requests that take a long time to process (streaming responses,
+expensive calculations, database lookups, API calls, etc), as other concurrent
+requests that also utilize sessions will hang until the session is unlocked.
+
+If you want to control when the session data is locked and unlocked,
+set ``tools.sessions.locking = 'explicit'``. Then call
+``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
+Regardless of which mode you use, the session is guaranteed to be unlocked when
+the request is complete.
+
+=================
+Expiring Sessions
+=================
+
+You can force a session to expire with :func:`cherrypy.lib.sessions.expire`.
+Simply call that function at the point you want the session to expire, and it
+will cause the session cookie to expire client-side.
+
+===========================
+Session Fixation Protection
+===========================
+
+If CherryPy receives, via a request cookie, a session id that it does not
+recognize, it will reject that id and create a new one to return in the
+response cookie. This `helps prevent session fixation attacks
+<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_.
+However, CherryPy "recognizes" a session id by looking up the saved session
+data for that id. Therefore, if you never save any session data,
+**you will get a new session id for every request**.
+
+A side effect of CherryPy overwriting unrecognised session ids is that if you
+have multiple, separate CherryPy applications running on a single domain (e.g.
+on different ports), each app will overwrite the other's session id because by
+default they use the same cookie name (``"session_id"``) but do not recognise
+each others sessions. It is therefore a good idea to use a different name for
+each, for example::
+
+    [/]
+    ...
+    tools.sessions.name = "my_app_session_id"
+
+================
+Sharing Sessions
+================
+
+If you run multiple instances of CherryPy (for example via mod_python behind
+Apache prefork), you most likely cannot use the RAM session backend, since each
+instance of CherryPy will have its own memory space. Use a different backend
+instead, and verify that all instances are pointing at the same file or db
+location. Alternately, you might try a load balancer which makes sessions
+"sticky". Google is your friend, there.
+
+================
+Expiration Dates
+================
+
+The response cookie will possess an expiration date to inform the client at
+which point to stop sending the cookie back in requests. If the server time
+and client time differ, expect sessions to be unreliable. **Make sure the
+system time of your server is accurate**.
+
+CherryPy defaults to a 60-minute session timeout, which also applies to the
+cookie which is sent to the client. Unfortunately, some versions of Safari
+("4 public beta" on Windows XP at least) appear to have a bug in their parsing
+of the GMT expiration date--they appear to interpret the date as one hour in
+the past. Sixty minutes minus one hour is pretty close to zero, so you may
+experience this bug as a new session id for every request, unless the requests
+are less than one second apart. To fix, try increasing the session.timeout.
+
+On the other extreme, some users report Firefox sending cookies after their
+expiration date, although this was on a system with an inaccurate system time.
+Maybe FF doesn't trust system time.
+"""
+import sys
+import datetime
+import os
+import time
+import threading
+import binascii
+
+import six
+from six.moves import cPickle as pickle
+import contextlib2
+
+import zc.lockfile
+
+import cherrypy
+from cherrypy.lib import httputil
+from cherrypy.lib import locking
+from cherrypy.lib import is_iterator
+
+
+if six.PY2:
+    FileNotFoundError = OSError
+
+
+missing = object()
+
+
+class Session(object):
+
+    """A CherryPy dict-like Session object (one per request)."""
+
+    _id = None
+
+    id_observers = None
+    "A list of callbacks to which to pass new id's."
+
+    @property
+    def id(self):
+        """Return the current session id."""
+        return self._id
+
+    @id.setter
+    def id(self, value):
+        self._id = value
+        for o in self.id_observers:
+            o(value)
+
+    timeout = 60
+    'Number of minutes after which to delete session data.'
+
+    locked = False
+    """
+    If True, this session instance has exclusive read/write access
+    to session data."""
+
+    loaded = False
+    """
+    If True, data has been retrieved from storage. This should happen
+    automatically on the first attempt to access session data."""
+
+    clean_thread = None
+    'Class-level Monitor which calls self.clean_up.'
+
+    clean_freq = 5
+    'The poll rate for expired session cleanup in minutes.'
+
+    originalid = None
+    'The session id passed by the client. May be missing or unsafe.'
+
+    missing = False
+    'True if the session requested by the client did not exist.'
+
+    regenerated = False
+    """
+    True if the application called session.regenerate(). This is not set by
+    internal calls to regenerate the session id."""
+
+    debug = False
+    'If True, log debug information.'
+
+    # --------------------- Session management methods --------------------- #
+
+    def __init__(self, id=None, **kwargs):
+        self.id_observers = []
+        self._data = {}
+
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+        self.originalid = id
+        self.missing = False
+        if id is None:
+            if self.debug:
+                cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
+            self._regenerate()
+        else:
+            self.id = id
+            if self._exists():
+                if self.debug:
+                    cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS')
+            else:
+                if self.debug:
+                    cherrypy.log('Expired or malicious session %r; '
+                                 'making a new one' % id, 'TOOLS.SESSIONS')
+                # Expired or malicious session. Make a new one.
+                # See https://github.com/cherrypy/cherrypy/issues/709.
+                self.id = None
+                self.missing = True
+                self._regenerate()
+
+    def now(self):
+        """Generate the session specific concept of 'now'.
+
+        Other session providers can override this to use alternative,
+        possibly timezone aware, versions of 'now'.
+        """
+        return datetime.datetime.now()
+
+    def regenerate(self):
+        """Replace the current session (with a new id)."""
+        self.regenerated = True
+        self._regenerate()
+
+    def _regenerate(self):
+        if self.id is not None:
+            if self.debug:
+                cherrypy.log(
+                    'Deleting the existing session %r before '
+                    'regeneration.' % self.id,
+                    'TOOLS.SESSIONS')
+            self.delete()
+
+        old_session_was_locked = self.locked
+        if old_session_was_locked:
+            self.release_lock()
+            if self.debug:
+                cherrypy.log('Old lock released.', 'TOOLS.SESSIONS')
+
+        self.id = None
+        while self.id is None:
+            self.id = self.generate_id()
+            # Assert that the generated id is not already stored.
+            if self._exists():
+                self.id = None
+        if self.debug:
+            cherrypy.log('Set id to generated %s.' % self.id,
+                         'TOOLS.SESSIONS')
+
+        if old_session_was_locked:
+            self.acquire_lock()
+            if self.debug:
+                cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS')
+
+    def clean_up(self):
+        """Clean up expired sessions."""
+        pass
+
+    def generate_id(self):
+        """Return a new session id."""
+        return binascii.hexlify(os.urandom(20)).decode('ascii')
+
+    def save(self):
+        """Save session data."""
+        try:
+            # If session data has never been loaded then it's never been
+            #   accessed: no need to save it
+            if self.loaded:
+                t = datetime.timedelta(seconds=self.timeout * 60)
+                expiration_time = self.now() + t
+                if self.debug:
+                    cherrypy.log('Saving session %r with expiry %s' %
+                                 (self.id, expiration_time),
+                                 'TOOLS.SESSIONS')
+                self._save(expiration_time)
+            else:
+                if self.debug:
+                    cherrypy.log(
+                        'Skipping save of session %r (no session loaded).' %
+                        self.id, 'TOOLS.SESSIONS')
+        finally:
+            if self.locked:
+                # Always release the lock if the user didn't release it
+                self.release_lock()
+                if self.debug:
+                    cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS')
+
+    def load(self):
+        """Copy stored session data into this session instance."""
+        data = self._load()
+        # data is either None or a tuple (session_data, expiration_time)
+        if data is None or data[1] < self.now():
+            if self.debug:
+                cherrypy.log('Expired session %r, flushing data.' % self.id,
+                             'TOOLS.SESSIONS')
+            self._data = {}
+        else:
+            if self.debug:
+                cherrypy.log('Data loaded for session %r.' % self.id,
+                             'TOOLS.SESSIONS')
+            self._data = data[0]
+        self.loaded = True
+
+        # Stick the clean_thread in the class, not the instance.
+        # The instances are created and destroyed per-request.
+        cls = self.__class__
+        if self.clean_freq and not cls.clean_thread:
+            # clean_up is an instancemethod and not a classmethod,
+            # so that tool config can be accessed inside the method.
+            t = cherrypy.process.plugins.Monitor(
+                cherrypy.engine, self.clean_up, self.clean_freq * 60,
+                name='Session cleanup')
+            t.subscribe()
+            cls.clean_thread = t
+            t.start()
+            if self.debug:
+                cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS')
+
+    def delete(self):
+        """Delete stored session data."""
+        self._delete()
+        if self.debug:
+            cherrypy.log('Deleted session %s.' % self.id,
+                         'TOOLS.SESSIONS')
+
+    # -------------------- Application accessor methods -------------------- #
+
+    def __getitem__(self, key):
+        if not self.loaded:
+            self.load()
+        return self._data[key]
+
+    def __setitem__(self, key, value):
+        if not self.loaded:
+            self.load()
+        self._data[key] = value
+
+    def __delitem__(self, key):
+        if not self.loaded:
+            self.load()
+        del self._data[key]
+
+    def pop(self, key, default=missing):
+        """Remove the specified key and return the corresponding value.
+        If key is not found, default is returned if given,
+        otherwise KeyError is raised.
+        """
+        if not self.loaded:
+            self.load()
+        if default is missing:
+            return self._data.pop(key)
+        else:
+            return self._data.pop(key, default)
+
+    def __contains__(self, key):
+        if not self.loaded:
+            self.load()
+        return key in self._data
+
+    def get(self, key, default=None):
+        """D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None."""
+        if not self.loaded:
+            self.load()
+        return self._data.get(key, default)
+
+    def update(self, d):
+        """D.update(E) -> None.  Update D from E: for k in E: D[k] = E[k]."""
+        if not self.loaded:
+            self.load()
+        self._data.update(d)
+
+    def setdefault(self, key, default=None):
+        """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
+        if not self.loaded:
+            self.load()
+        return self._data.setdefault(key, default)
+
+    def clear(self):
+        """D.clear() -> None.  Remove all items from D."""
+        if not self.loaded:
+            self.load()
+        self._data.clear()
+
+    def keys(self):
+        """D.keys() -> list of D's keys."""
+        if not self.loaded:
+            self.load()
+        return self._data.keys()
+
+    def items(self):
+        """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
+        if not self.loaded:
+            self.load()
+        return self._data.items()
+
+    def values(self):
+        """D.values() -> list of D's values."""
+        if not self.loaded:
+            self.load()
+        return self._data.values()
+
+
+class RamSession(Session):
+
+    # Class-level objects. Don't rebind these!
+    cache = {}
+    locks = {}
+
+    def clean_up(self):
+        """Clean up expired sessions."""
+
+        now = self.now()
+        for _id, (data, expiration_time) in list(six.iteritems(self.cache)):
+            if expiration_time <= now:
+                try:
+                    del self.cache[_id]
+                except KeyError:
+                    pass
+                try:
+                    if self.locks[_id].acquire(blocking=False):
+                        lock = self.locks.pop(_id)
+                        lock.release()
+                except KeyError:
+                    pass
+
+        # added to remove obsolete lock objects
+        for _id in list(self.locks):
+            locked = (
+                _id not in self.cache
+                and self.locks[_id].acquire(blocking=False)
+            )
+            if locked:
+                lock = self.locks.pop(_id)
+                lock.release()
+
+    def _exists(self):
+        return self.id in self.cache
+
+    def _load(self):
+        return self.cache.get(self.id)
+
+    def _save(self, expiration_time):
+        self.cache[self.id] = (self._data, expiration_time)
+
+    def _delete(self):
+        self.cache.pop(self.id, None)
+
+    def acquire_lock(self):
+        """Acquire an exclusive lock on the currently-loaded session data."""
+        self.locked = True
+        self.locks.setdefault(self.id, threading.RLock()).acquire()
+
+    def release_lock(self):
+        """Release the lock on the currently-loaded session data."""
+        self.locks[self.id].release()
+        self.locked = False
+
+    def __len__(self):
+        """Return the number of active sessions."""
+        return len(self.cache)
+
+
+class FileSession(Session):
+
+    """Implementation of the File backend for sessions
+
+    storage_path
+        The folder where session data will be saved. Each session
+        will be saved as pickle.dump(data, expiration_time) in its own file;
+        the filename will be self.SESSION_PREFIX + self.id.
+
+    lock_timeout
+        A timedelta or numeric seconds indicating how long
+        to block acquiring a lock. If None (default), acquiring a lock
+        will block indefinitely.
+    """
+
+    SESSION_PREFIX = 'session-'
+    LOCK_SUFFIX = '.lock'
+    pickle_protocol = pickle.HIGHEST_PROTOCOL
+
+    def __init__(self, id=None, **kwargs):
+        # The 'storage_path' arg is required for file-based sessions.
+        kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+        kwargs.setdefault('lock_timeout', None)
+
+        Session.__init__(self, id=id, **kwargs)
+
+        # validate self.lock_timeout
+        if isinstance(self.lock_timeout, (int, float)):
+            self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
+        if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
+            raise ValueError(
+                'Lock timeout must be numeric seconds or a timedelta instance.'
+            )
+
+    @classmethod
+    def setup(cls, **kwargs):
+        """Set up the storage system for file-based sessions.
+
+        This should only be called once per process; this will be done
+        automatically when using sessions.init (as the built-in Tool does).
+        """
+        # The 'storage_path' arg is required for file-based sessions.
+        kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
+
+        for k, v in kwargs.items():
+            setattr(cls, k, v)
+
+    def _get_file_path(self):
+        f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
+        if not os.path.abspath(f).startswith(self.storage_path):
+            raise cherrypy.HTTPError(400, 'Invalid session id in cookie.')
+        return f
+
+    def _exists(self):
+        path = self._get_file_path()
+        return os.path.exists(path)
+
+    def _load(self, path=None):
+        assert self.locked, ('The session load without being locked.  '
+                             "Check your tools' priority levels.")
+        if path is None:
+            path = self._get_file_path()
+        try:
+            f = open(path, 'rb')
+            try:
+                return pickle.load(f)
+            finally:
+                f.close()
+        except (IOError, EOFError):
+            e = sys.exc_info()[1]
+            if self.debug:
+                cherrypy.log('Error loading the session pickle: %s' %
+                             e, 'TOOLS.SESSIONS')
+            return None
+
+    def _save(self, expiration_time):
+        assert self.locked, ('The session was saved without being locked.  '
+                             "Check your tools' priority levels.")
+        f = open(self._get_file_path(), 'wb')
+        try:
+            pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
+        finally:
+            f.close()
+
+    def _delete(self):
+        assert self.locked, ('The session deletion without being locked.  '
+                             "Check your tools' priority levels.")
+        try:
+            os.unlink(self._get_file_path())
+        except OSError:
+            pass
+
+    def acquire_lock(self, path=None):
+        """Acquire an exclusive lock on the currently-loaded session data."""
+        if path is None:
+            path = self._get_file_path()
+        path += self.LOCK_SUFFIX
+        checker = locking.LockChecker(self.id, self.lock_timeout)
+        while not checker.expired():
+            try:
+                self.lock = zc.lockfile.LockFile(path)
+            except zc.lockfile.LockError:
+                time.sleep(0.1)
+            else:
+                break
+        self.locked = True
+        if self.debug:
+            cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
+
+    def release_lock(self, path=None):
+        """Release the lock on the currently-loaded session data."""
+        self.lock.close()
+        with contextlib2.suppress(FileNotFoundError):
+            os.remove(self.lock._path)
+        self.locked = False
+
+    def clean_up(self):
+        """Clean up expired sessions."""
+        now = self.now()
+        # Iterate over all session files in self.storage_path
+        for fname in os.listdir(self.storage_path):
+            have_session = (
+                fname.startswith(self.SESSION_PREFIX)
+                and not fname.endswith(self.LOCK_SUFFIX)
+            )
+            if have_session:
+                # We have a session file: lock and load it and check
+                #   if it's expired. If it fails, nevermind.
+                path = os.path.join(self.storage_path, fname)
+                self.acquire_lock(path)
+                if self.debug:
+                    # This is a bit of a hack, since we're calling clean_up
+                    # on the first instance rather than the entire class,
+                    # so depending on whether you have "debug" set on the
+                    # path of the first session called, this may not run.
+                    cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS')
+
+                try:
+                    contents = self._load(path)
+                    # _load returns None on IOError
+                    if contents is not None:
+                        data, expiration_time = contents
+                        if expiration_time < now:
+                            # Session expired: deleting it
+                            os.unlink(path)
+                finally:
+                    self.release_lock(path)
+
+    def __len__(self):
+        """Return the number of active sessions."""
+        return len([fname for fname in os.listdir(self.storage_path)
+                    if (fname.startswith(self.SESSION_PREFIX) and
+                        not fname.endswith(self.LOCK_SUFFIX))])
+
+
+class MemcachedSession(Session):
+
+    # The most popular memcached client for Python isn't thread-safe.
+    # Wrap all .get and .set operations in a single lock.
+    mc_lock = threading.RLock()
+
+    # This is a separate set of locks per session id.
+    locks = {}
+
+    servers = ['127.0.0.1:11211']
+
+    @classmethod
+    def setup(cls, **kwargs):
+        """Set up the storage system for memcached-based sessions.
+
+        This should only be called once per process; this will be done
+        automatically when using sessions.init (as the built-in Tool does).
+        """
+        for k, v in kwargs.items():
+            setattr(cls, k, v)
+
+        import memcache
+        cls.cache = memcache.Client(cls.servers)
+
+    def _exists(self):
+        self.mc_lock.acquire()
+        try:
+            return bool(self.cache.get(self.id))
+        finally:
+            self.mc_lock.release()
+
+    def _load(self):
+        self.mc_lock.acquire()
+        try:
+            return self.cache.get(self.id)
+        finally:
+            self.mc_lock.release()
+
+    def _save(self, expiration_time):
+        # Send the expiration time as "Unix time" (seconds since 1/1/1970)
+        td = int(time.mktime(expiration_time.timetuple()))
+        self.mc_lock.acquire()
+        try:
+            if not self.cache.set(self.id, (self._data, expiration_time), td):
+                raise AssertionError(
+                    'Session data for id %r not set.' % self.id)
+        finally:
+            self.mc_lock.release()
+
+    def _delete(self):
+        self.cache.delete(self.id)
+
+    def acquire_lock(self):
+        """Acquire an exclusive lock on the currently-loaded session data."""
+        self.locked = True
+        self.locks.setdefault(self.id, threading.RLock()).acquire()
+        if self.debug:
+            cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
+
+    def release_lock(self):
+        """Release the lock on the currently-loaded session data."""
+        self.locks[self.id].release()
+        self.locked = False
+
+    def __len__(self):
+        """Return the number of active sessions."""
+        raise NotImplementedError
+
+
+# Hook functions (for CherryPy tools)
+
+def save():
+    """Save any changed session data."""
+
+    if not hasattr(cherrypy.serving, 'session'):
+        return
+    request = cherrypy.serving.request
+    response = cherrypy.serving.response
+
+    # Guard against running twice
+    if hasattr(request, '_sessionsaved'):
+        return
+    request._sessionsaved = True
+
+    if response.stream:
+        # If the body is being streamed, we have to save the data
+        #   *after* the response has been written out
+        request.hooks.attach('on_end_request', cherrypy.session.save)
+    else:
+        # If the body is not being streamed, we save the data now
+        # (so we can release the lock).
+        if is_iterator(response.body):
+            response.collapse_body()
+        cherrypy.session.save()
+
+
+save.failsafe = True
+
+
+def close():
+    """Close the session object for this request."""
+    sess = getattr(cherrypy.serving, 'session', None)
+    if getattr(sess, 'locked', False):
+        # If the session is still locked we release the lock
+        sess.release_lock()
+        if sess.debug:
+            cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
+
+
+close.failsafe = True
+close.priority = 90
+
+
+def init(storage_type=None, path=None, path_header=None, name='session_id',
+         timeout=60, domain=None, secure=False, clean_freq=5,
+         persistent=True, httponly=False, debug=False,
+         # Py27 compat
+         # *, storage_class=RamSession,
+         **kwargs):
+    """Initialize session object (using cookies).
+
+    storage_class
+        The Session subclass to use. Defaults to RamSession.
+
+    storage_type
+        (deprecated)
+        One of 'ram', 'file', memcached'. This will be
+        used to look up the corresponding class in cherrypy.lib.sessions
+        globals. For example, 'file' will use the FileSession class.
+
+    path
+        The 'path' value to stick in the response cookie metadata.
+
+    path_header
+        If 'path' is None (the default), then the response
+        cookie 'path' will be pulled from request.headers[path_header].
+
+    name
+        The name of the cookie.
+
+    timeout
+        The expiration timeout (in minutes) for the stored session data.
+        If 'persistent' is True (the default), this is also the timeout
+        for the cookie.
+
+    domain
+        The cookie domain.
+
+    secure
+        If False (the default) the cookie 'secure' value will not
+        be set. If True, the cookie 'secure' value will be set (to 1).
+
+    clean_freq (minutes)
+        The poll rate for expired session cleanup.
+
+    persistent
+        If True (the default), the 'timeout' argument will be used
+        to expire the cookie. If False, the cookie will not have an expiry,
+        and the cookie will be a "session cookie" which expires when the
+        browser is closed.
+
+    httponly
+        If False (the default) the cookie 'httponly' value will not be set.
+        If True, the cookie 'httponly' value will be set (to 1).
+
+    Any additional kwargs will be bound to the new Session instance,
+    and may be specific to the storage type. See the subclass of Session
+    you're using for more information.
+    """
+
+    # Py27 compat
+    storage_class = kwargs.pop('storage_class', RamSession)
+
+    request = cherrypy.serving.request
+
+    # Guard against running twice
+    if hasattr(request, '_session_init_flag'):
+        return
+    request._session_init_flag = True
+
+    # Check if request came with a session ID
+    id = None
+    if name in request.cookie:
+        id = request.cookie[name].value
+        if debug:
+            cherrypy.log('ID obtained from request.cookie: %r' % id,
+                         'TOOLS.SESSIONS')
+
+    first_time = not hasattr(cherrypy, 'session')
+
+    if storage_type:
+        if first_time:
+            msg = 'storage_type is deprecated. Supply storage_class instead'
+            cherrypy.log(msg)
+        storage_class = storage_type.title() + 'Session'
+        storage_class = globals()[storage_class]
+
+    # call setup first time only
+    if first_time:
+        if hasattr(storage_class, 'setup'):
+            storage_class.setup(**kwargs)
+
+    # Create and attach a new Session instance to cherrypy.serving.
+    # It will possess a reference to (and lock, and lazily load)
+    # the requested session data.
+    kwargs['timeout'] = timeout
+    kwargs['clean_freq'] = clean_freq
+    cherrypy.serving.session = sess = storage_class(id, **kwargs)
+    sess.debug = debug
+
+    def update_cookie(id):
+        """Update the cookie every time the session id changes."""
+        cherrypy.serving.response.cookie[name] = id
+    sess.id_observers.append(update_cookie)
+
+    # Create cherrypy.session which will proxy to cherrypy.serving.session
+    if not hasattr(cherrypy, 'session'):
+        cherrypy.session = cherrypy._ThreadLocalProxy('session')
+
+    if persistent:
+        cookie_timeout = timeout
+    else:
+        # See http://support.microsoft.com/kb/223799/EN-US/
+        # and http://support.mozilla.com/en-US/kb/Cookies
+        cookie_timeout = None
+    set_response_cookie(path=path, path_header=path_header, name=name,
+                        timeout=cookie_timeout, domain=domain, secure=secure,
+                        httponly=httponly)
+
+
+def set_response_cookie(path=None, path_header=None, name='session_id',
+                        timeout=60, domain=None, secure=False, httponly=False):
+    """Set a response cookie for the client.
+
+    path
+        the 'path' value to stick in the response cookie metadata.
+
+    path_header
+        if 'path' is None (the default), then the response
+        cookie 'path' will be pulled from request.headers[path_header].
+
+    name
+        the name of the cookie.
+
+    timeout
+        the expiration timeout for the cookie. If 0 or other boolean
+        False, no 'expires' param will be set, and the cookie will be a
+        "session cookie" which expires when the browser is closed.
+
+    domain
+        the cookie domain.
+
+    secure
+        if False (the default) the cookie 'secure' value will not
+        be set. If True, the cookie 'secure' value will be set (to 1).
+
+    httponly
+        If False (the default) the cookie 'httponly' value will not be set.
+        If True, the cookie 'httponly' value will be set (to 1).
+
+    """
+    # Set response cookie
+    cookie = cherrypy.serving.response.cookie
+    cookie[name] = cherrypy.serving.session.id
+    cookie[name]['path'] = (
+        path or
+        cherrypy.serving.request.headers.get(path_header) or
+        '/'
+    )
+
+    if timeout:
+        cookie[name]['max-age'] = timeout * 60
+        _add_MSIE_max_age_workaround(cookie[name], timeout)
+    if domain is not None:
+        cookie[name]['domain'] = domain
+    if secure:
+        cookie[name]['secure'] = 1
+    if httponly:
+        if not cookie[name].isReservedKey('httponly'):
+            raise ValueError('The httponly cookie token is not supported.')
+        cookie[name]['httponly'] = 1
+
+
+def _add_MSIE_max_age_workaround(cookie, timeout):
+    """
+    We'd like to use the "max-age" param as indicated in
+    http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
+    save it to disk and the session is lost if people close
+    the browser. So we have to use the old "expires" ... sigh ...
+    """
+    expires = time.time() + timeout * 60
+    cookie['expires'] = httputil.HTTPDate(expires)
+
+
+def expire():
+    """Expire the current session cookie."""
+    name = cherrypy.serving.request.config.get(
+        'tools.sessions.name', 'session_id')
+    one_year = 60 * 60 * 24 * 365
+    e = time.time() - one_year
+    cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
+    cherrypy.serving.response.cookie[name].pop('max-age', None)
diff --git a/libraries/cherrypy/lib/static.py b/libraries/cherrypy/lib/static.py
new file mode 100644
index 00000000..da9d9373
--- /dev/null
+++ b/libraries/cherrypy/lib/static.py
@@ -0,0 +1,390 @@
+"""Module with helpers for serving static files."""
+
+import os
+import platform
+import re
+import stat
+import mimetypes
+
+from email.generator import _make_boundary as make_boundary
+from io import UnsupportedOperation
+
+from six.moves import urllib
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy.lib import cptools, httputil, file_generator_limited
+
+
+def _setup_mimetypes():
+    """Pre-initialize global mimetype map."""
+    if not mimetypes.inited:
+        mimetypes.init()
+    mimetypes.types_map['.dwg'] = 'image/x-dwg'
+    mimetypes.types_map['.ico'] = 'image/x-icon'
+    mimetypes.types_map['.bz2'] = 'application/x-bzip2'
+    mimetypes.types_map['.gz'] = 'application/x-gzip'
+
+
+_setup_mimetypes()
+
+
+def serve_file(path, content_type=None, disposition=None, name=None,
+               debug=False):
+    """Set status, headers, and body in order to serve the given path.
+
+    The Content-Type header will be set to the content_type arg, if provided.
+    If not provided, the Content-Type will be guessed by the file extension
+    of the 'path' argument.
+
+    If disposition is not None, the Content-Disposition header will be set
+    to "<disposition>; filename=<name>". If name is None, it will be set
+    to the basename of path. If disposition is None, no Content-Disposition
+    header will be written.
+    """
+    response = cherrypy.serving.response
+
+    # If path is relative, users should fix it by making path absolute.
+    # That is, CherryPy should not guess where the application root is.
+    # It certainly should *not* use cwd (since CP may be invoked from a
+    # variety of paths). If using tools.staticdir, you can make your relative
+    # paths become absolute by supplying a value for "tools.staticdir.root".
+    if not os.path.isabs(path):
+        msg = "'%s' is not an absolute path." % path
+        if debug:
+            cherrypy.log(msg, 'TOOLS.STATICFILE')
+        raise ValueError(msg)
+
+    try:
+        st = os.stat(path)
+    except (OSError, TypeError, ValueError):
+        # OSError when file fails to stat
+        # TypeError on Python 2 when there's a null byte
+        # ValueError on Python 3 when there's a null byte
+        if debug:
+            cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
+        raise cherrypy.NotFound()
+
+    # Check if path is a directory.
+    if stat.S_ISDIR(st.st_mode):
+        # Let the caller deal with it as they like.
+        if debug:
+            cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
+        raise cherrypy.NotFound()
+
+    # Set the Last-Modified response header, so that
+    # modified-since validation code can work.
+    response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
+    cptools.validate_since()
+
+    if content_type is None:
+        # Set content-type based on filename extension
+        ext = ''
+        i = path.rfind('.')
+        if i != -1:
+            ext = path[i:].lower()
+        content_type = mimetypes.types_map.get(ext, None)
+    if content_type is not None:
+        response.headers['Content-Type'] = content_type
+    if debug:
+        cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
+
+    cd = None
+    if disposition is not None:
+        if name is None:
+            name = os.path.basename(path)
+        cd = '%s; filename="%s"' % (disposition, name)
+        response.headers['Content-Disposition'] = cd
+    if debug:
+        cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
+
+    # Set Content-Length and use an iterable (file object)
+    #   this way CP won't load the whole file in memory
+    content_length = st.st_size
+    fileobj = open(path, 'rb')
+    return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
+
+
+def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
+                  debug=False):
+    """Set status, headers, and body in order to serve the given file object.
+
+    The Content-Type header will be set to the content_type arg, if provided.
+
+    If disposition is not None, the Content-Disposition header will be set
+    to "<disposition>; filename=<name>". If name is None, 'filename' will
+    not be set. If disposition is None, no Content-Disposition header will
+    be written.
+
+    CAUTION: If the request contains a 'Range' header, one or more seek()s will
+    be performed on the file object.  This may cause undesired behavior if
+    the file object is not seekable.  It could also produce undesired results
+    if the caller set the read position of the file object prior to calling
+    serve_fileobj(), expecting that the data would be served starting from that
+    position.
+    """
+    response = cherrypy.serving.response
+
+    try:
+        st = os.fstat(fileobj.fileno())
+    except AttributeError:
+        if debug:
+            cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
+        content_length = None
+    except UnsupportedOperation:
+        content_length = None
+    else:
+        # Set the Last-Modified response header, so that
+        # modified-since validation code can work.
+        response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
+        cptools.validate_since()
+        content_length = st.st_size
+
+    if content_type is not None:
+        response.headers['Content-Type'] = content_type
+    if debug:
+        cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
+
+    cd = None
+    if disposition is not None:
+        if name is None:
+            cd = disposition
+        else:
+            cd = '%s; filename="%s"' % (disposition, name)
+        response.headers['Content-Disposition'] = cd
+    if debug:
+        cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
+
+    return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
+
+
+def _serve_fileobj(fileobj, content_type, content_length, debug=False):
+    """Internal. Set response.body to the given file object, perhaps ranged."""
+    response = cherrypy.serving.response
+
+    # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
+    request = cherrypy.serving.request
+    if request.protocol >= (1, 1):
+        response.headers['Accept-Ranges'] = 'bytes'
+        r = httputil.get_ranges(request.headers.get('Range'), content_length)
+        if r == []:
+            response.headers['Content-Range'] = 'bytes */%s' % content_length
+            message = ('Invalid Range (first-byte-pos greater than '
+                       'Content-Length)')
+            if debug:
+                cherrypy.log(message, 'TOOLS.STATIC')
+            raise cherrypy.HTTPError(416, message)
+
+        if r:
+            if len(r) == 1:
+                # Return a single-part response.
+                start, stop = r[0]
+                if stop > content_length:
+                    stop = content_length
+                r_len = stop - start
+                if debug:
+                    cherrypy.log(
+                        'Single part; start: %r, stop: %r' % (start, stop),
+                        'TOOLS.STATIC')
+                response.status = '206 Partial Content'
+                response.headers['Content-Range'] = (
+                    'bytes %s-%s/%s' % (start, stop - 1, content_length))
+                response.headers['Content-Length'] = r_len
+                fileobj.seek(start)
+                response.body = file_generator_limited(fileobj, r_len)
+            else:
+                # Return a multipart/byteranges response.
+                response.status = '206 Partial Content'
+                boundary = make_boundary()
+                ct = 'multipart/byteranges; boundary=%s' % boundary
+                response.headers['Content-Type'] = ct
+                if 'Content-Length' in response.headers:
+                    # Delete Content-Length header so finalize() recalcs it.
+                    del response.headers['Content-Length']
+
+                def file_ranges():
+                    # Apache compatibility:
+                    yield b'\r\n'
+
+                    for start, stop in r:
+                        if debug:
+                            cherrypy.log(
+                                'Multipart; start: %r, stop: %r' % (
+                                    start, stop),
+                                'TOOLS.STATIC')
+                        yield ntob('--' + boundary, 'ascii')
+                        yield ntob('\r\nContent-type: %s' % content_type,
+                                   'ascii')
+                        yield ntob(
+                            '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % (
+                                start, stop - 1, content_length),
+                            'ascii')
+                        fileobj.seek(start)
+                        gen = file_generator_limited(fileobj, stop - start)
+                        for chunk in gen:
+                            yield chunk
+                        yield b'\r\n'
+                    # Final boundary
+                    yield ntob('--' + boundary + '--', 'ascii')
+
+                    # Apache compatibility:
+                    yield b'\r\n'
+                response.body = file_ranges()
+            return response.body
+        else:
+            if debug:
+                cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
+
+    # Set Content-Length and use an iterable (file object)
+    #   this way CP won't load the whole file in memory
+    response.headers['Content-Length'] = content_length
+    response.body = fileobj
+    return response.body
+
+
+def serve_download(path, name=None):
+    """Serve 'path' as an application/x-download attachment."""
+    # This is such a common idiom I felt it deserved its own wrapper.
+    return serve_file(path, 'application/x-download', 'attachment', name)
+
+
+def _attempt(filename, content_types, debug=False):
+    if debug:
+        cherrypy.log('Attempting %r (content_types %r)' %
+                     (filename, content_types), 'TOOLS.STATICDIR')
+    try:
+        # you can set the content types for a
+        # complete directory per extension
+        content_type = None
+        if content_types:
+            r, ext = os.path.splitext(filename)
+            content_type = content_types.get(ext[1:], None)
+        serve_file(filename, content_type=content_type, debug=debug)
+        return True
+    except cherrypy.NotFound:
+        # If we didn't find the static file, continue handling the
+        # request. We might find a dynamic handler instead.
+        if debug:
+            cherrypy.log('NotFound', 'TOOLS.STATICFILE')
+        return False
+
+
+def staticdir(section, dir, root='', match='', content_types=None, index='',
+              debug=False):
+    """Serve a static resource from the given (root +) dir.
+
+    match
+        If given, request.path_info will be searched for the given
+        regular expression before attempting to serve static content.
+
+    content_types
+        If given, it should be a Python dictionary of
+        {file-extension: content-type} pairs, where 'file-extension' is
+        a string (e.g. "gif") and 'content-type' is the value to write
+        out in the Content-Type response header (e.g. "image/gif").
+
+    index
+        If provided, it should be the (relative) name of a file to
+        serve for directory requests. For example, if the dir argument is
+        '/home/me', the Request-URI is 'myapp', and the index arg is
+        'index.html', the file '/home/me/myapp/index.html' will be sought.
+    """
+    request = cherrypy.serving.request
+    if request.method not in ('GET', 'HEAD'):
+        if debug:
+            cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
+        return False
+
+    if match and not re.search(match, request.path_info):
+        if debug:
+            cherrypy.log('request.path_info %r does not match pattern %r' %
+                         (request.path_info, match), 'TOOLS.STATICDIR')
+        return False
+
+    # Allow the use of '~' to refer to a user's home directory.
+    dir = os.path.expanduser(dir)
+
+    # If dir is relative, make absolute using "root".
+    if not os.path.isabs(dir):
+        if not root:
+            msg = 'Static dir requires an absolute dir (or root).'
+            if debug:
+                cherrypy.log(msg, 'TOOLS.STATICDIR')
+            raise ValueError(msg)
+        dir = os.path.join(root, dir)
+
+    # Determine where we are in the object tree relative to 'section'
+    # (where the static tool was defined).
+    if section == 'global':
+        section = '/'
+    section = section.rstrip(r'\/')
+    branch = request.path_info[len(section) + 1:]
+    branch = urllib.parse.unquote(branch.lstrip(r'\/'))
+
+    # Requesting a file in sub-dir of the staticdir results
+    # in mixing of delimiter styles, e.g. C:\static\js/script.js.
+    # Windows accepts this form except not when the path is
+    # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js.
+    # http://bit.ly/1vdioCX
+    if platform.system() == 'Windows':
+        branch = branch.replace('/', '\\')
+
+    # If branch is "", filename will end in a slash
+    filename = os.path.join(dir, branch)
+    if debug:
+        cherrypy.log('Checking file %r to fulfill %r' %
+                     (filename, request.path_info), 'TOOLS.STATICDIR')
+
+    # There's a chance that the branch pulled from the URL might
+    # have ".." or similar uplevel attacks in it. Check that the final
+    # filename is a child of dir.
+    if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
+        raise cherrypy.HTTPError(403)  # Forbidden
+
+    handled = _attempt(filename, content_types)
+    if not handled:
+        # Check for an index file if a folder was requested.
+        if index:
+            handled = _attempt(os.path.join(filename, index), content_types)
+            if handled:
+                request.is_index = filename[-1] in (r'\/')
+    return handled
+
+
+def staticfile(filename, root=None, match='', content_types=None, debug=False):
+    """Serve a static resource from the given (root +) filename.
+
+    match
+        If given, request.path_info will be searched for the given
+        regular expression before attempting to serve static content.
+
+    content_types
+        If given, it should be a Python dictionary of
+        {file-extension: content-type} pairs, where 'file-extension' is
+        a string (e.g. "gif") and 'content-type' is the value to write
+        out in the Content-Type response header (e.g. "image/gif").
+
+    """
+    request = cherrypy.serving.request
+    if request.method not in ('GET', 'HEAD'):
+        if debug:
+            cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
+        return False
+
+    if match and not re.search(match, request.path_info):
+        if debug:
+            cherrypy.log('request.path_info %r does not match pattern %r' %
+                         (request.path_info, match), 'TOOLS.STATICFILE')
+        return False
+
+    # If filename is relative, make absolute using "root".
+    if not os.path.isabs(filename):
+        if not root:
+            msg = "Static tool requires an absolute filename (got '%s')." % (
+                filename,)
+            if debug:
+                cherrypy.log(msg, 'TOOLS.STATICFILE')
+            raise ValueError(msg)
+        filename = os.path.join(root, filename)
+
+    return _attempt(filename, content_types, debug=debug)
diff --git a/libraries/cherrypy/lib/xmlrpcutil.py b/libraries/cherrypy/lib/xmlrpcutil.py
new file mode 100644
index 00000000..ddaac86a
--- /dev/null
+++ b/libraries/cherrypy/lib/xmlrpcutil.py
@@ -0,0 +1,61 @@
+"""XML-RPC tool helpers."""
+import sys
+
+from six.moves.xmlrpc_client import (
+    loads as xmlrpc_loads, dumps as xmlrpc_dumps,
+    Fault as XMLRPCFault
+)
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+
+
+def process_body():
+    """Return (params, method) from request body."""
+    try:
+        return xmlrpc_loads(cherrypy.request.body.read())
+    except Exception:
+        return ('ERROR PARAMS', ), 'ERRORMETHOD'
+
+
+def patched_path(path):
+    """Return 'path', doctored for RPC."""
+    if not path.endswith('/'):
+        path += '/'
+    if path.startswith('/RPC2/'):
+        # strip the first /rpc2
+        path = path[5:]
+    return path
+
+
+def _set_response(body):
+    """Set up HTTP status, headers and body within CherryPy."""
+    # The XML-RPC spec (http://www.xmlrpc.com/spec) says:
+    # "Unless there's a lower-level error, always return 200 OK."
+    # Since Python's xmlrpc_client interprets a non-200 response
+    # as a "Protocol Error", we'll just return 200 every time.
+    response = cherrypy.response
+    response.status = '200 OK'
+    response.body = ntob(body, 'utf-8')
+    response.headers['Content-Type'] = 'text/xml'
+    response.headers['Content-Length'] = len(body)
+
+
+def respond(body, encoding='utf-8', allow_none=0):
+    """Construct HTTP response body."""
+    if not isinstance(body, XMLRPCFault):
+        body = (body,)
+
+    _set_response(
+        xmlrpc_dumps(
+            body, methodresponse=1,
+            encoding=encoding,
+            allow_none=allow_none
+        )
+    )
+
+
+def on_error(*args, **kwargs):
+    """Construct HTTP response body for an error response."""
+    body = str(sys.exc_info()[1])
+    _set_response(xmlrpc_dumps(XMLRPCFault(1, body)))
diff --git a/libraries/cherrypy/process/__init__.py b/libraries/cherrypy/process/__init__.py
new file mode 100644
index 00000000..f242d226
--- /dev/null
+++ b/libraries/cherrypy/process/__init__.py
@@ -0,0 +1,17 @@
+"""Site container for an HTTP server.
+
+A Web Site Process Bus object is used to connect applications, servers,
+and frameworks with site-wide services such as daemonization, process
+reload, signal handling, drop privileges, PID file management, logging
+for all of these, and many more.
+
+The 'plugins' module defines a few abstract and concrete services for
+use with the bus. Some use tool-specific channels; see the documentation
+for each class.
+"""
+
+from .wspbus import bus
+from . import plugins, servers
+
+
+__all__ = ('bus', 'plugins', 'servers')
diff --git a/libraries/cherrypy/process/plugins.py b/libraries/cherrypy/process/plugins.py
new file mode 100644
index 00000000..8c246c81
--- /dev/null
+++ b/libraries/cherrypy/process/plugins.py
@@ -0,0 +1,752 @@
+"""Site services for use with a Web Site Process Bus."""
+
+import os
+import re
+import signal as _signal
+import sys
+import time
+import threading
+
+from six.moves import _thread
+
+from cherrypy._cpcompat import text_or_bytes
+from cherrypy._cpcompat import ntob, Timer
+
+# _module__file__base is used by Autoreload to make
+# absolute any filenames retrieved from sys.modules which are not
+# already absolute paths.  This is to work around Python's quirk
+# of importing the startup script and using a relative filename
+# for it in sys.modules.
+#
+# Autoreload examines sys.modules afresh every time it runs. If an application
+# changes the current directory by executing os.chdir(), then the next time
+# Autoreload runs, it will not be able to find any filenames which are
+# not absolute paths, because the current directory is not the same as when the
+# module was first imported.  Autoreload will then wrongly conclude the file
+# has "changed", and initiate the shutdown/re-exec sequence.
+# See ticket #917.
+# For this workaround to have a decent probability of success, this module
+# needs to be imported as early as possible, before the app has much chance
+# to change the working directory.
+_module__file__base = os.getcwd()
+
+
+class SimplePlugin(object):
+
+    """Plugin base class which auto-subscribes methods for known channels."""
+
+    bus = None
+    """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.
+    """
+
+    def __init__(self, bus):
+        self.bus = bus
+
+    def subscribe(self):
+        """Register this object as a (multi-channel) listener on the bus."""
+        for channel in self.bus.listeners:
+            # Subscribe self.start, self.exit, etc. if present.
+            method = getattr(self, channel, None)
+            if method is not None:
+                self.bus.subscribe(channel, method)
+
+    def unsubscribe(self):
+        """Unregister this object as a listener on the bus."""
+        for channel in self.bus.listeners:
+            # Unsubscribe self.start, self.exit, etc. if present.
+            method = getattr(self, channel, None)
+            if method is not None:
+                self.bus.unsubscribe(channel, method)
+
+
+class SignalHandler(object):
+
+    """Register bus channels (and listeners) for system signals.
+
+    You can modify what signals your application listens for, and what it does
+    when it receives signals, by modifying :attr:`SignalHandler.handlers`,
+    a dict of {signal name: callback} pairs. The default set is::
+
+        handlers = {'SIGTERM': self.bus.exit,
+                    'SIGHUP': self.handle_SIGHUP,
+                    'SIGUSR1': self.bus.graceful,
+                   }
+
+    The :func:`SignalHandler.handle_SIGHUP`` method calls
+    :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
+    if the process is daemonized, but
+    :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
+    if the process is attached to a TTY. This is because Unix window
+    managers tend to send SIGHUP to terminal windows when the user closes them.
+
+    Feel free to add signals which are not available on every platform.
+    The :class:`SignalHandler` will ignore errors raised from attempting
+    to register handlers for unknown signals.
+    """
+
+    handlers = {}
+    """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
+
+    signals = {}
+    """A map from signal numbers to names."""
+
+    for k, v in vars(_signal).items():
+        if k.startswith('SIG') and not k.startswith('SIG_'):
+            signals[v] = k
+    del k, v
+
+    def __init__(self, bus):
+        self.bus = bus
+        # Set default handlers
+        self.handlers = {'SIGTERM': self.bus.exit,
+                         'SIGHUP': self.handle_SIGHUP,
+                         'SIGUSR1': self.bus.graceful,
+                         }
+
+        if sys.platform[:4] == 'java':
+            del self.handlers['SIGUSR1']
+            self.handlers['SIGUSR2'] = self.bus.graceful
+            self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
+                         'Using SIGUSR2 instead.')
+            self.handlers['SIGINT'] = self._jython_SIGINT_handler
+
+        self._previous_handlers = {}
+        # used to determine is the process is a daemon in `self._is_daemonized`
+        self._original_pid = os.getpid()
+
+    def _jython_SIGINT_handler(self, signum=None, frame=None):
+        # See http://bugs.jython.org/issue1313
+        self.bus.log('Keyboard Interrupt: shutting down bus')
+        self.bus.exit()
+
+    def _is_daemonized(self):
+        """Return boolean indicating if the current process is
+        running as a daemon.
+
+        The criteria to determine the `daemon` condition is to verify
+        if the current pid is not the same as the one that got used on
+        the initial construction of the plugin *and* the stdin is not
+        connected to a terminal.
+
+        The sole validation of the tty is not enough when the plugin
+        is executing inside other process like in a CI tool
+        (Buildbot, Jenkins).
+        """
+        return (
+            self._original_pid != os.getpid() and
+            not os.isatty(sys.stdin.fileno())
+        )
+
+    def subscribe(self):
+        """Subscribe self.handlers to signals."""
+        for sig, func in self.handlers.items():
+            try:
+                self.set_handler(sig, func)
+            except ValueError:
+                pass
+
+    def unsubscribe(self):
+        """Unsubscribe self.handlers from signals."""
+        for signum, handler in self._previous_handlers.items():
+            signame = self.signals[signum]
+
+            if handler is None:
+                self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
+                handler = _signal.SIG_DFL
+            else:
+                self.bus.log('Restoring %s handler %r.' % (signame, handler))
+
+            try:
+                our_handler = _signal.signal(signum, handler)
+                if our_handler is None:
+                    self.bus.log('Restored old %s handler %r, but our '
+                                 'handler was not registered.' %
+                                 (signame, handler), level=30)
+            except ValueError:
+                self.bus.log('Unable to restore %s handler %r.' %
+                             (signame, handler), level=40, traceback=True)
+
+    def set_handler(self, signal, listener=None):
+        """Subscribe a handler for the given signal (number or name).
+
+        If the optional 'listener' argument is provided, it will be
+        subscribed as a listener for the given signal's channel.
+
+        If the given signal name or number is not available on the current
+        platform, ValueError is raised.
+        """
+        if isinstance(signal, text_or_bytes):
+            signum = getattr(_signal, signal, None)
+            if signum is None:
+                raise ValueError('No such signal: %r' % signal)
+            signame = signal
+        else:
+            try:
+                signame = self.signals[signal]
+            except KeyError:
+                raise ValueError('No such signal: %r' % signal)
+            signum = signal
+
+        prev = _signal.signal(signum, self._handle_signal)
+        self._previous_handlers[signum] = prev
+
+        if listener is not None:
+            self.bus.log('Listening for %s.' % signame)
+            self.bus.subscribe(signame, listener)
+
+    def _handle_signal(self, signum=None, frame=None):
+        """Python signal handler (self.set_handler subscribes it for you)."""
+        signame = self.signals[signum]
+        self.bus.log('Caught signal %s.' % signame)
+        self.bus.publish(signame)
+
+    def handle_SIGHUP(self):
+        """Restart if daemonized, else exit."""
+        if self._is_daemonized():
+            self.bus.log('SIGHUP caught while daemonized. Restarting.')
+            self.bus.restart()
+        else:
+            # not daemonized (may be foreground or background)
+            self.bus.log('SIGHUP caught but not daemonized. Exiting.')
+            self.bus.exit()
+
+
+try:
+    import pwd
+    import grp
+except ImportError:
+    pwd, grp = None, None
+
+
+class DropPrivileges(SimplePlugin):
+
+    """Drop privileges. uid/gid arguments not available on Windows.
+
+    Special thanks to `Gavin Baker
+    <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
+    """
+
+    def __init__(self, bus, umask=None, uid=None, gid=None):
+        SimplePlugin.__init__(self, bus)
+        self.finalized = False
+        self.uid = uid
+        self.gid = gid
+        self.umask = umask
+
+    @property
+    def uid(self):
+        """The uid under which to run. Availability: Unix."""
+        return self._uid
+
+    @uid.setter
+    def uid(self, val):
+        if val is not None:
+            if pwd is None:
+                self.bus.log('pwd module not available; ignoring uid.',
+                             level=30)
+                val = None
+            elif isinstance(val, text_or_bytes):
+                val = pwd.getpwnam(val)[2]
+        self._uid = val
+
+    @property
+    def gid(self):
+        """The gid under which to run. Availability: Unix."""
+        return self._gid
+
+    @gid.setter
+    def gid(self, val):
+        if val is not None:
+            if grp is None:
+                self.bus.log('grp module not available; ignoring gid.',
+                             level=30)
+                val = None
+            elif isinstance(val, text_or_bytes):
+                val = grp.getgrnam(val)[2]
+        self._gid = val
+
+    @property
+    def umask(self):
+        """The default permission mode for newly created files and directories.
+
+        Usually expressed in octal format, for example, ``0644``.
+        Availability: Unix, Windows.
+        """
+        return self._umask
+
+    @umask.setter
+    def umask(self, val):
+        if val is not None:
+            try:
+                os.umask
+            except AttributeError:
+                self.bus.log('umask function not available; ignoring umask.',
+                             level=30)
+                val = None
+        self._umask = val
+
+    def start(self):
+        # uid/gid
+        def current_ids():
+            """Return the current (uid, gid) if available."""
+            name, group = None, None
+            if pwd:
+                name = pwd.getpwuid(os.getuid())[0]
+            if grp:
+                group = grp.getgrgid(os.getgid())[0]
+            return name, group
+
+        if self.finalized:
+            if not (self.uid is None and self.gid is None):
+                self.bus.log('Already running as uid: %r gid: %r' %
+                             current_ids())
+        else:
+            if self.uid is None and self.gid is None:
+                if pwd or grp:
+                    self.bus.log('uid/gid not set', level=30)
+            else:
+                self.bus.log('Started as uid: %r gid: %r' % current_ids())
+                if self.gid is not None:
+                    os.setgid(self.gid)
+                    os.setgroups([])
+                if self.uid is not None:
+                    os.setuid(self.uid)
+                self.bus.log('Running as uid: %r gid: %r' % current_ids())
+
+        # umask
+        if self.finalized:
+            if self.umask is not None:
+                self.bus.log('umask already set to: %03o' % self.umask)
+        else:
+            if self.umask is None:
+                self.bus.log('umask not set', level=30)
+            else:
+                old_umask = os.umask(self.umask)
+                self.bus.log('umask old: %03o, new: %03o' %
+                             (old_umask, self.umask))
+
+        self.finalized = True
+    # This is slightly higher than the priority for server.start
+    # in order to facilitate the most common use: starting on a low
+    # port (which requires root) and then dropping to another user.
+    start.priority = 77
+
+
+class Daemonizer(SimplePlugin):
+
+    """Daemonize the running script.
+
+    Use this with a Web Site Process Bus via::
+
+        Daemonizer(bus).subscribe()
+
+    When this component finishes, the process is completely decoupled from
+    the parent environment. Please note that when this component is used,
+    the return code from the parent process will still be 0 if a startup
+    error occurs in the forked children. Errors in the initial daemonizing
+    process still return proper exit codes. Therefore, if you use this
+    plugin to daemonize, don't use the return code as an accurate indicator
+    of whether the process fully started. In fact, that return code only
+    indicates if the process successfully finished the first fork.
+    """
+
+    def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
+                 stderr='/dev/null'):
+        SimplePlugin.__init__(self, bus)
+        self.stdin = stdin
+        self.stdout = stdout
+        self.stderr = stderr
+        self.finalized = False
+
+    def start(self):
+        if self.finalized:
+            self.bus.log('Already deamonized.')
+
+        # forking has issues with threads:
+        # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
+        # "The general problem with making fork() work in a multi-threaded
+        #  world is what to do with all of the threads..."
+        # So we check for active threads:
+        if threading.activeCount() != 1:
+            self.bus.log('There are %r active threads. '
+                         'Daemonizing now may cause strange failures.' %
+                         threading.enumerate(), level=30)
+
+        self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log)
+
+        self.finalized = True
+    start.priority = 65
+
+    @staticmethod
+    def daemonize(
+            stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
+            logger=lambda msg: None):
+        # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
+        # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
+        # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
+
+        # Finish up with the current stdout/stderr
+        sys.stdout.flush()
+        sys.stderr.flush()
+
+        error_tmpl = (
+            '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n'
+        )
+
+        for fork in range(2):
+            msg = ['Forking once.', 'Forking twice.'][fork]
+            try:
+                pid = os.fork()
+                if pid > 0:
+                    # This is the parent; exit.
+                    logger(msg)
+                    os._exit(0)
+            except OSError as exc:
+                # Python raises OSError rather than returning negative numbers.
+                sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1))
+            if fork == 0:
+                os.setsid()
+
+        os.umask(0)
+
+        si = open(stdin, 'r')
+        so = open(stdout, 'a+')
+        se = open(stderr, 'a+')
+
+        # os.dup2(fd, fd2) will close fd2 if necessary,
+        # so we don't explicitly close stdin/out/err.
+        # See http://docs.python.org/lib/os-fd-ops.html
+        os.dup2(si.fileno(), sys.stdin.fileno())
+        os.dup2(so.fileno(), sys.stdout.fileno())
+        os.dup2(se.fileno(), sys.stderr.fileno())
+
+        logger('Daemonized to PID: %s' % os.getpid())
+
+
+class PIDFile(SimplePlugin):
+
+    """Maintain a PID file via a WSPBus."""
+
+    def __init__(self, bus, pidfile):
+        SimplePlugin.__init__(self, bus)
+        self.pidfile = pidfile
+        self.finalized = False
+
+    def start(self):
+        pid = os.getpid()
+        if self.finalized:
+            self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
+        else:
+            open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
+            self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
+            self.finalized = True
+    start.priority = 70
+
+    def exit(self):
+        try:
+            os.remove(self.pidfile)
+            self.bus.log('PID file removed: %r.' % self.pidfile)
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            pass
+
+
+class PerpetualTimer(Timer):
+
+    """A responsive subclass of threading.Timer whose run() method repeats.
+
+    Use this timer only when you really need a very interruptible timer;
+    this checks its 'finished' condition up to 20 times a second, which can
+    results in pretty high CPU usage
+    """
+
+    def __init__(self, *args, **kwargs):
+        "Override parent constructor to allow 'bus' to be provided."
+        self.bus = kwargs.pop('bus', None)
+        super(PerpetualTimer, self).__init__(*args, **kwargs)
+
+    def run(self):
+        while True:
+            self.finished.wait(self.interval)
+            if self.finished.isSet():
+                return
+            try:
+                self.function(*self.args, **self.kwargs)
+            except Exception:
+                if self.bus:
+                    self.bus.log(
+                        'Error in perpetual timer thread function %r.' %
+                        self.function, level=40, traceback=True)
+                # Quit on first error to avoid massive logs.
+                raise
+
+
+class BackgroundTask(threading.Thread):
+
+    """A subclass of threading.Thread whose run() method repeats.
+
+    Use this class for most repeating tasks. It uses time.sleep() to wait
+    for each interval, which isn't very responsive; that is, even if you call
+    self.cancel(), you'll have to wait until the sleep() call finishes before
+    the thread stops. To compensate, it defaults to being daemonic, which means
+    it won't delay stopping the whole process.
+    """
+
+    def __init__(self, interval, function, args=[], kwargs={}, bus=None):
+        super(BackgroundTask, self).__init__()
+        self.interval = interval
+        self.function = function
+        self.args = args
+        self.kwargs = kwargs
+        self.running = False
+        self.bus = bus
+
+        # default to daemonic
+        self.daemon = True
+
+    def cancel(self):
+        self.running = False
+
+    def run(self):
+        self.running = True
+        while self.running:
+            time.sleep(self.interval)
+            if not self.running:
+                return
+            try:
+                self.function(*self.args, **self.kwargs)
+            except Exception:
+                if self.bus:
+                    self.bus.log('Error in background task thread function %r.'
+                                 % self.function, level=40, traceback=True)
+                # Quit on first error to avoid massive logs.
+                raise
+
+
+class Monitor(SimplePlugin):
+
+    """WSPBus listener to periodically run a callback in its own thread."""
+
+    callback = None
+    """The function to call at intervals."""
+
+    frequency = 60
+    """The time in seconds between callback runs."""
+
+    thread = None
+    """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>`
+    thread.
+    """
+
+    def __init__(self, bus, callback, frequency=60, name=None):
+        SimplePlugin.__init__(self, bus)
+        self.callback = callback
+        self.frequency = frequency
+        self.thread = None
+        self.name = name
+
+    def start(self):
+        """Start our callback in its own background thread."""
+        if self.frequency > 0:
+            threadname = self.name or self.__class__.__name__
+            if self.thread is None:
+                self.thread = BackgroundTask(self.frequency, self.callback,
+                                             bus=self.bus)
+                self.thread.setName(threadname)
+                self.thread.start()
+                self.bus.log('Started monitor thread %r.' % threadname)
+            else:
+                self.bus.log('Monitor thread %r already started.' % threadname)
+    start.priority = 70
+
+    def stop(self):
+        """Stop our callback's background task thread."""
+        if self.thread is None:
+            self.bus.log('No thread running for %s.' %
+                         self.name or self.__class__.__name__)
+        else:
+            if self.thread is not threading.currentThread():
+                name = self.thread.getName()
+                self.thread.cancel()
+                if not self.thread.daemon:
+                    self.bus.log('Joining %r' % name)
+                    self.thread.join()
+                self.bus.log('Stopped thread %r.' % name)
+            self.thread = None
+
+    def graceful(self):
+        """Stop the callback's background task thread and restart it."""
+        self.stop()
+        self.start()
+
+
+class Autoreloader(Monitor):
+
+    """Monitor which re-executes the process when files change.
+
+    This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
+    if any of the files it monitors change (or is deleted). By default, the
+    autoreloader monitors all imported modules; you can add to the
+    set by adding to ``autoreload.files``::
+
+        cherrypy.engine.autoreload.files.add(myFile)
+
+    If there are imported files you do *not* wish to monitor, you can
+    adjust the ``match`` attribute, a regular expression. For example,
+    to stop monitoring cherrypy itself::
+
+        cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
+
+    Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
+    the autoreload plugin takes a ``frequency`` argument. The default is
+    1 second; that is, the autoreloader will examine files once each second.
+    """
+
+    files = None
+    """The set of files to poll for modifications."""
+
+    frequency = 1
+    """The interval in seconds at which to poll for modified files."""
+
+    match = '.*'
+    """A regular expression by which to match filenames."""
+
+    def __init__(self, bus, frequency=1, match='.*'):
+        self.mtimes = {}
+        self.files = set()
+        self.match = match
+        Monitor.__init__(self, bus, self.run, frequency)
+
+    def start(self):
+        """Start our own background task thread for self.run."""
+        if self.thread is None:
+            self.mtimes = {}
+        Monitor.start(self)
+    start.priority = 70
+
+    def sysfiles(self):
+        """Return a Set of sys.modules filenames to monitor."""
+        search_mod_names = filter(re.compile(self.match).match, sys.modules)
+        mods = map(sys.modules.get, search_mod_names)
+        return set(filter(None, map(self._file_for_module, mods)))
+
+    @classmethod
+    def _file_for_module(cls, module):
+        """Return the relevant file for the module."""
+        return (
+            cls._archive_for_zip_module(module)
+            or cls._file_for_file_module(module)
+        )
+
+    @staticmethod
+    def _archive_for_zip_module(module):
+        """Return the archive filename for the module if relevant."""
+        try:
+            return module.__loader__.archive
+        except AttributeError:
+            pass
+
+    @classmethod
+    def _file_for_file_module(cls, module):
+        """Return the file for the module."""
+        try:
+            return module.__file__ and cls._make_absolute(module.__file__)
+        except AttributeError:
+            pass
+
+    @staticmethod
+    def _make_absolute(filename):
+        """Ensure filename is absolute to avoid effect of os.chdir."""
+        return filename if os.path.isabs(filename) else (
+            os.path.normpath(os.path.join(_module__file__base, filename))
+        )
+
+    def run(self):
+        """Reload the process if registered files have been modified."""
+        for filename in self.sysfiles() | self.files:
+            if filename:
+                if filename.endswith('.pyc'):
+                    filename = filename[:-1]
+
+                oldtime = self.mtimes.get(filename, 0)
+                if oldtime is None:
+                    # Module with no .py file. Skip it.
+                    continue
+
+                try:
+                    mtime = os.stat(filename).st_mtime
+                except OSError:
+                    # Either a module with no .py file, or it's been deleted.
+                    mtime = None
+
+                if filename not in self.mtimes:
+                    # If a module has no .py file, this will be None.
+                    self.mtimes[filename] = mtime
+                else:
+                    if mtime is None or mtime > oldtime:
+                        # The file has been deleted or modified.
+                        self.bus.log('Restarting because %s changed.' %
+                                     filename)
+                        self.thread.cancel()
+                        self.bus.log('Stopped thread %r.' %
+                                     self.thread.getName())
+                        self.bus.restart()
+                        return
+
+
+class ThreadManager(SimplePlugin):
+
+    """Manager for HTTP request threads.
+
+    If you have control over thread creation and destruction, publish to
+    the 'acquire_thread' and 'release_thread' channels (for each thread).
+    This will register/unregister the current thread and publish to
+    'start_thread' and 'stop_thread' listeners in the bus as needed.
+
+    If threads are created and destroyed by code you do not control
+    (e.g., Apache), then, at the beginning of every HTTP request,
+    publish to 'acquire_thread' only. You should not publish to
+    'release_thread' in this case, since you do not know whether
+    the thread will be re-used or not. The bus will call
+    'stop_thread' listeners for you when it stops.
+    """
+
+    threads = None
+    """A map of {thread ident: index number} pairs."""
+
+    def __init__(self, bus):
+        self.threads = {}
+        SimplePlugin.__init__(self, bus)
+        self.bus.listeners.setdefault('acquire_thread', set())
+        self.bus.listeners.setdefault('start_thread', set())
+        self.bus.listeners.setdefault('release_thread', set())
+        self.bus.listeners.setdefault('stop_thread', set())
+
+    def acquire_thread(self):
+        """Run 'start_thread' listeners for the current thread.
+
+        If the current thread has already been seen, any 'start_thread'
+        listeners will not be run again.
+        """
+        thread_ident = _thread.get_ident()
+        if thread_ident not in self.threads:
+            # We can't just use get_ident as the thread ID
+            # because some platforms reuse thread ID's.
+            i = len(self.threads) + 1
+            self.threads[thread_ident] = i
+            self.bus.publish('start_thread', i)
+
+    def release_thread(self):
+        """Release the current thread and run 'stop_thread' listeners."""
+        thread_ident = _thread.get_ident()
+        i = self.threads.pop(thread_ident, None)
+        if i is not None:
+            self.bus.publish('stop_thread', i)
+
+    def stop(self):
+        """Release all threads and run all 'stop_thread' listeners."""
+        for thread_ident, i in self.threads.items():
+            self.bus.publish('stop_thread', i)
+        self.threads.clear()
+    graceful = stop
diff --git a/libraries/cherrypy/process/servers.py b/libraries/cherrypy/process/servers.py
new file mode 100644
index 00000000..dcb34de6
--- /dev/null
+++ b/libraries/cherrypy/process/servers.py
@@ -0,0 +1,416 @@
+r"""
+Starting in CherryPy 3.1, cherrypy.server is implemented as an
+:ref:`Engine Plugin<plugins>`. It's an instance of
+:class:`cherrypy._cpserver.Server`, which is a subclass of
+:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class
+is designed to control other servers, as well.
+
+Multiple servers/ports
+======================
+
+If you need to start more than one HTTP server (to serve on multiple ports, or
+protocols, etc.), you can manually register each one and then start them all
+with engine.start::
+
+    s1 = ServerAdapter(
+        cherrypy.engine,
+        MyWSGIServer(host='0.0.0.0', port=80)
+    )
+    s2 = ServerAdapter(
+        cherrypy.engine,
+        another.HTTPServer(host='127.0.0.1', SSL=True)
+    )
+    s1.subscribe()
+    s2.subscribe()
+    cherrypy.engine.start()
+
+.. index:: SCGI
+
+FastCGI/SCGI
+============
+
+There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in
+:mod:`cherrypy.process.servers`. To start an fcgi server, for example,
+wrap an instance of it in a ServerAdapter::
+
+    addr = ('0.0.0.0', 4000)
+    f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr)
+    s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr)
+    s.subscribe()
+
+The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for
+you via its `-f` flag.
+Note that you need to download and install `flup <http://trac.saddi.com/flup>`_
+yourself, whether you use ``cherryd`` or not.
+
+.. _fastcgi:
+.. index:: FastCGI
+
+FastCGI
+-------
+
+A very simple setup lets your cherry run with FastCGI.
+You just need the flup library,
+plus a running Apache server (with ``mod_fastcgi``) or lighttpd server.
+
+CherryPy code
+^^^^^^^^^^^^^
+
+hello.py::
+
+    #!/usr/bin/python
+    import cherrypy
+
+    class HelloWorld:
+        '''Sample request handler class.'''
+        @cherrypy.expose
+        def index(self):
+            return "Hello world!"
+
+    cherrypy.tree.mount(HelloWorld())
+    # CherryPy autoreload must be disabled for the flup server to work
+    cherrypy.config.update({'engine.autoreload.on':False})
+
+Then run :doc:`/deployguide/cherryd` with the '-f' arg::
+
+    cherryd -c <myconfig> -d -f -i hello.py
+
+Apache
+^^^^^^
+
+At the top level in httpd.conf::
+
+    FastCgiIpcDir /tmp
+    FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4
+
+And inside the relevant VirtualHost section::
+
+    # FastCGI config
+    AddHandler fastcgi-script .fcgi
+    ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1
+
+Lighttpd
+^^^^^^^^
+
+For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these
+instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is
+active within ``server.modules``. Then, within your ``$HTTP["host"]``
+directive, configure your fastcgi script like the following::
+
+    $HTTP["url"] =~ "" {
+      fastcgi.server = (
+        "/" => (
+          "script.fcgi" => (
+            "bin-path" => "/path/to/your/script.fcgi",
+            "socket"          => "/tmp/script.sock",
+            "check-local"     => "disable",
+            "disable-time"    => 1,
+            "min-procs"       => 1,
+            "max-procs"       => 1, # adjust as needed
+          ),
+        ),
+      )
+    } # end of $HTTP["url"] =~ "^/"
+
+Please see `Lighttpd FastCGI Docs
+<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
+an explanation of the possible configuration options.
+"""
+
+import os
+import sys
+import time
+import warnings
+import contextlib
+
+import portend
+
+
+class Timeouts:
+    occupied = 5
+    free = 1
+
+
+class ServerAdapter(object):
+
+    """Adapter for an HTTP server.
+
+    If you need to start more than one HTTP server (to serve on multiple
+    ports, or protocols, etc.), you can manually register each one and then
+    start them all with bus.start::
+
+        s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
+        s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
+        s1.subscribe()
+        s2.subscribe()
+        bus.start()
+    """
+
+    def __init__(self, bus, httpserver=None, bind_addr=None):
+        self.bus = bus
+        self.httpserver = httpserver
+        self.bind_addr = bind_addr
+        self.interrupt = None
+        self.running = False
+
+    def subscribe(self):
+        self.bus.subscribe('start', self.start)
+        self.bus.subscribe('stop', self.stop)
+
+    def unsubscribe(self):
+        self.bus.unsubscribe('start', self.start)
+        self.bus.unsubscribe('stop', self.stop)
+
+    def start(self):
+        """Start the HTTP server."""
+        if self.running:
+            self.bus.log('Already serving on %s' % self.description)
+            return
+
+        self.interrupt = None
+        if not self.httpserver:
+            raise ValueError('No HTTP server has been created.')
+
+        if not os.environ.get('LISTEN_PID', None):
+            # Start the httpserver in a new thread.
+            if isinstance(self.bind_addr, tuple):
+                portend.free(*self.bind_addr, timeout=Timeouts.free)
+
+        import threading
+        t = threading.Thread(target=self._start_http_thread)
+        t.setName('HTTPServer ' + t.getName())
+        t.start()
+
+        self.wait()
+        self.running = True
+        self.bus.log('Serving on %s' % self.description)
+    start.priority = 75
+
+    @property
+    def description(self):
+        """
+        A description about where this server is bound.
+        """
+        if self.bind_addr is None:
+            on_what = 'unknown interface (dynamic?)'
+        elif isinstance(self.bind_addr, tuple):
+            on_what = self._get_base()
+        else:
+            on_what = 'socket file: %s' % self.bind_addr
+        return on_what
+
+    def _get_base(self):
+        if not self.httpserver:
+            return ''
+        host, port = self.bound_addr
+        if getattr(self.httpserver, 'ssl_adapter', None):
+            scheme = 'https'
+            if port != 443:
+                host += ':%s' % port
+        else:
+            scheme = 'http'
+            if port != 80:
+                host += ':%s' % port
+
+        return '%s://%s' % (scheme, host)
+
+    def _start_http_thread(self):
+        """HTTP servers MUST be running in new threads, so that the
+        main thread persists to receive KeyboardInterrupt's. If an
+        exception is raised in the httpserver's thread then it's
+        trapped here, and the bus (and therefore our httpserver)
+        are shut down.
+        """
+        try:
+            self.httpserver.start()
+        except KeyboardInterrupt:
+            self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
+            self.interrupt = sys.exc_info()[1]
+            self.bus.exit()
+        except SystemExit:
+            self.bus.log('SystemExit raised: shutting down HTTP server')
+            self.interrupt = sys.exc_info()[1]
+            self.bus.exit()
+            raise
+        except Exception:
+            self.interrupt = sys.exc_info()[1]
+            self.bus.log('Error in HTTP server: shutting down',
+                         traceback=True, level=40)
+            self.bus.exit()
+            raise
+
+    def wait(self):
+        """Wait until the HTTP server is ready to receive requests."""
+        while not getattr(self.httpserver, 'ready', False):
+            if self.interrupt:
+                raise self.interrupt
+            time.sleep(.1)
+
+        # bypass check when LISTEN_PID is set
+        if os.environ.get('LISTEN_PID', None):
+            return
+
+        # bypass check when running via socket-activation
+        # (for socket-activation the port will be managed by systemd)
+        if not isinstance(self.bind_addr, tuple):
+            return
+
+        # wait for port to be occupied
+        with _safe_wait(*self.bound_addr):
+            portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
+
+    @property
+    def bound_addr(self):
+        """
+        The bind address, or if it's an ephemeral port and the
+        socket has been bound, return the actual port bound.
+        """
+        host, port = self.bind_addr
+        if port == 0 and self.httpserver.socket:
+            # Bound to ephemeral port. Get the actual port allocated.
+            port = self.httpserver.socket.getsockname()[1]
+        return host, port
+
+    def stop(self):
+        """Stop the HTTP server."""
+        if self.running:
+            # stop() MUST block until the server is *truly* stopped.
+            self.httpserver.stop()
+            # Wait for the socket to be truly freed.
+            if isinstance(self.bind_addr, tuple):
+                portend.free(*self.bound_addr, timeout=Timeouts.free)
+            self.running = False
+            self.bus.log('HTTP Server %s shut down' % self.httpserver)
+        else:
+            self.bus.log('HTTP Server %s already shut down' % self.httpserver)
+    stop.priority = 25
+
+    def restart(self):
+        """Restart the HTTP server."""
+        self.stop()
+        self.start()
+
+
+class FlupCGIServer(object):
+
+    """Adapter for a flup.server.cgi.WSGIServer."""
+
+    def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        self.ready = False
+
+    def start(self):
+        """Start the CGI server."""
+        # We have to instantiate the server class here because its __init__
+        # starts a threadpool. If we do it too early, daemonize won't work.
+        from flup.server.cgi import WSGIServer
+
+        self.cgiserver = WSGIServer(*self.args, **self.kwargs)
+        self.ready = True
+        self.cgiserver.run()
+
+    def stop(self):
+        """Stop the HTTP server."""
+        self.ready = False
+
+
+class FlupFCGIServer(object):
+
+    """Adapter for a flup.server.fcgi.WSGIServer."""
+
+    def __init__(self, *args, **kwargs):
+        if kwargs.get('bindAddress', None) is None:
+            import socket
+            if not hasattr(socket, 'fromfd'):
+                raise ValueError(
+                    'Dynamic FCGI server not available on this platform. '
+                    'You must use a static or external one by providing a '
+                    'legal bindAddress.')
+        self.args = args
+        self.kwargs = kwargs
+        self.ready = False
+
+    def start(self):
+        """Start the FCGI server."""
+        # We have to instantiate the server class here because its __init__
+        # starts a threadpool. If we do it too early, daemonize won't work.
+        from flup.server.fcgi import WSGIServer
+        self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
+        # TODO: report this bug upstream to flup.
+        # If we don't set _oldSIGs on Windows, we get:
+        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+        #   line 108, in run
+        #     self._restoreSignalHandlers()
+        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+        #   line 156, in _restoreSignalHandlers
+        #     for signum,handler in self._oldSIGs:
+        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
+        self.fcgiserver._installSignalHandlers = lambda: None
+        self.fcgiserver._oldSIGs = []
+        self.ready = True
+        self.fcgiserver.run()
+
+    def stop(self):
+        """Stop the HTTP server."""
+        # Forcibly stop the fcgi server main event loop.
+        self.fcgiserver._keepGoing = False
+        # Force all worker threads to die off.
+        self.fcgiserver._threadPool.maxSpare = (
+            self.fcgiserver._threadPool._idleCount)
+        self.ready = False
+
+
+class FlupSCGIServer(object):
+
+    """Adapter for a flup.server.scgi.WSGIServer."""
+
+    def __init__(self, *args, **kwargs):
+        self.args = args
+        self.kwargs = kwargs
+        self.ready = False
+
+    def start(self):
+        """Start the SCGI server."""
+        # We have to instantiate the server class here because its __init__
+        # starts a threadpool. If we do it too early, daemonize won't work.
+        from flup.server.scgi import WSGIServer
+        self.scgiserver = WSGIServer(*self.args, **self.kwargs)
+        # TODO: report this bug upstream to flup.
+        # If we don't set _oldSIGs on Windows, we get:
+        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+        #   line 108, in run
+        #     self._restoreSignalHandlers()
+        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
+        #   line 156, in _restoreSignalHandlers
+        #     for signum,handler in self._oldSIGs:
+        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
+        self.scgiserver._installSignalHandlers = lambda: None
+        self.scgiserver._oldSIGs = []
+        self.ready = True
+        self.scgiserver.run()
+
+    def stop(self):
+        """Stop the HTTP server."""
+        self.ready = False
+        # Forcibly stop the scgi server main event loop.
+        self.scgiserver._keepGoing = False
+        # Force all worker threads to die off.
+        self.scgiserver._threadPool.maxSpare = 0
+
+
+@contextlib.contextmanager
+def _safe_wait(host, port):
+    """
+    On systems where a loopback interface is not available and the
+    server is bound to all interfaces, it's difficult to determine
+    whether the server is in fact occupying the port. In this case,
+    just issue a warning and move on. See issue #1100.
+    """
+    try:
+        yield
+    except portend.Timeout:
+        if host == portend.client_host(host):
+            raise
+        msg = 'Unable to verify that the server is bound on %r' % port
+        warnings.warn(msg)
diff --git a/libraries/cherrypy/process/win32.py b/libraries/cherrypy/process/win32.py
new file mode 100644
index 00000000..096b0278
--- /dev/null
+++ b/libraries/cherrypy/process/win32.py
@@ -0,0 +1,183 @@
+"""Windows service. Requires pywin32."""
+
+import os
+import win32api
+import win32con
+import win32event
+import win32service
+import win32serviceutil
+
+from cherrypy.process import wspbus, plugins
+
+
+class ConsoleCtrlHandler(plugins.SimplePlugin):
+
+    """A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
+
+    def __init__(self, bus):
+        self.is_set = False
+        plugins.SimplePlugin.__init__(self, bus)
+
+    def start(self):
+        if self.is_set:
+            self.bus.log('Handler for console events already set.', level=40)
+            return
+
+        result = win32api.SetConsoleCtrlHandler(self.handle, 1)
+        if result == 0:
+            self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
+                         win32api.GetLastError(), level=40)
+        else:
+            self.bus.log('Set handler for console events.', level=40)
+            self.is_set = True
+
+    def stop(self):
+        if not self.is_set:
+            self.bus.log('Handler for console events already off.', level=40)
+            return
+
+        try:
+            result = win32api.SetConsoleCtrlHandler(self.handle, 0)
+        except ValueError:
+            # "ValueError: The object has not been registered"
+            result = 1
+
+        if result == 0:
+            self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
+                         win32api.GetLastError(), level=40)
+        else:
+            self.bus.log('Removed handler for console events.', level=40)
+            self.is_set = False
+
+    def handle(self, event):
+        """Handle console control events (like Ctrl-C)."""
+        if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
+                     win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
+                     win32con.CTRL_CLOSE_EVENT):
+            self.bus.log('Console event %s: shutting down bus' % event)
+
+            # Remove self immediately so repeated Ctrl-C doesn't re-call it.
+            try:
+                self.stop()
+            except ValueError:
+                pass
+
+            self.bus.exit()
+            # 'First to return True stops the calls'
+            return 1
+        return 0
+
+
+class Win32Bus(wspbus.Bus):
+
+    """A Web Site Process Bus implementation for Win32.
+
+    Instead of time.sleep, this bus blocks using native win32event objects.
+    """
+
+    def __init__(self):
+        self.events = {}
+        wspbus.Bus.__init__(self)
+
+    def _get_state_event(self, state):
+        """Return a win32event for the given state (creating it if needed)."""
+        try:
+            return self.events[state]
+        except KeyError:
+            event = win32event.CreateEvent(None, 0, 0,
+                                           'WSPBus %s Event (pid=%r)' %
+                                           (state.name, os.getpid()))
+            self.events[state] = event
+            return event
+
+    @property
+    def state(self):
+        return self._state
+
+    @state.setter
+    def state(self, value):
+        self._state = value
+        event = self._get_state_event(value)
+        win32event.PulseEvent(event)
+
+    def wait(self, state, interval=0.1, channel=None):
+        """Wait for the given state(s), KeyboardInterrupt or SystemExit.
+
+        Since this class uses native win32event objects, the interval
+        argument is ignored.
+        """
+        if isinstance(state, (tuple, list)):
+            # Don't wait for an event that beat us to the punch ;)
+            if self.state not in state:
+                events = tuple([self._get_state_event(s) for s in state])
+                win32event.WaitForMultipleObjects(
+                    events, 0, win32event.INFINITE)
+        else:
+            # Don't wait for an event that beat us to the punch ;)
+            if self.state != state:
+                event = self._get_state_event(state)
+                win32event.WaitForSingleObject(event, win32event.INFINITE)
+
+
+class _ControlCodes(dict):
+
+    """Control codes used to "signal" a service via ControlService.
+
+    User-defined control codes are in the range 128-255. We generally use
+    the standard Python value for the Linux signal and add 128. Example:
+
+        >>> signal.SIGUSR1
+        10
+        control_codes['graceful'] = 128 + 10
+    """
+
+    def key_for(self, obj):
+        """For the given value, return its corresponding key."""
+        for key, val in self.items():
+            if val is obj:
+                return key
+        raise ValueError('The given object could not be found: %r' % obj)
+
+
+control_codes = _ControlCodes({'graceful': 138})
+
+
+def signal_child(service, command):
+    if command == 'stop':
+        win32serviceutil.StopService(service)
+    elif command == 'restart':
+        win32serviceutil.RestartService(service)
+    else:
+        win32serviceutil.ControlService(service, control_codes[command])
+
+
+class PyWebService(win32serviceutil.ServiceFramework):
+
+    """Python Web Service."""
+
+    _svc_name_ = 'Python Web Service'
+    _svc_display_name_ = 'Python Web Service'
+    _svc_deps_ = None        # sequence of service names on which this depends
+    _exe_name_ = 'pywebsvc'
+    _exe_args_ = None        # Default to no arguments
+
+    # Only exists on Windows 2000 or later, ignored on windows NT
+    _svc_description_ = 'Python Web Service'
+
+    def SvcDoRun(self):
+        from cherrypy import process
+        process.bus.start()
+        process.bus.block()
+
+    def SvcStop(self):
+        from cherrypy import process
+        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
+        process.bus.exit()
+
+    def SvcOther(self, control):
+        from cherrypy import process
+        process.bus.publish(control_codes.key_for(control))
+
+
+if __name__ == '__main__':
+    win32serviceutil.HandleCommandLine(PyWebService)
diff --git a/libraries/cherrypy/process/wspbus.py b/libraries/cherrypy/process/wspbus.py
new file mode 100644
index 00000000..39ac45bf
--- /dev/null
+++ b/libraries/cherrypy/process/wspbus.py
@@ -0,0 +1,590 @@
+r"""An implementation of the Web Site Process Bus.
+
+This module is completely standalone, depending only on the stdlib.
+
+Web Site Process Bus
+--------------------
+
+A Bus object is used to contain and manage site-wide behavior:
+daemonization, HTTP server start/stop, process reload, signal handling,
+drop privileges, PID file management, logging for all of these,
+and many more.
+
+In addition, a Bus object provides a place for each web framework
+to register code that runs in response to site-wide events (like
+process start and stop), or which controls or otherwise interacts with
+the site-wide components mentioned above. For example, a framework which
+uses file-based templates would add known template filenames to an
+autoreload component.
+
+Ideally, a Bus object will be flexible enough to be useful in a variety
+of invocation scenarios:
+
+ 1. The deployer starts a site from the command line via a
+    framework-neutral deployment script; applications from multiple frameworks
+    are mixed in a single site. Command-line arguments and configuration
+    files are used to define site-wide components such as the HTTP server,
+    WSGI component graph, autoreload behavior, signal handling, etc.
+ 2. The deployer starts a site via some other process, such as Apache;
+    applications from multiple frameworks are mixed in a single site.
+    Autoreload and signal handling (from Python at least) are disabled.
+ 3. The deployer starts a site via a framework-specific mechanism;
+    for example, when running tests, exploring tutorials, or deploying
+    single applications from a single framework. The framework controls
+    which site-wide components are enabled as it sees fit.
+
+The Bus object in this package uses topic-based publish-subscribe
+messaging to accomplish all this. A few topic channels are built in
+('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and
+site containers are free to define their own. If a message is sent to a
+channel that has not been defined or has no listeners, there is no effect.
+
+In general, there should only ever be a single Bus object per process.
+Frameworks and site containers share a single Bus object by publishing
+messages and subscribing listeners.
+
+The Bus object works as a finite state machine which models the current
+state of the process. Bus methods move it from one state to another;
+those methods then publish to subscribed listeners on the channel for
+the new state.::
+
+                        O
+                        |
+                        V
+       STOPPING --> STOPPED --> EXITING -> X
+          A   A         |
+          |    \___     |
+          |        \    |
+          |         V   V
+        STARTED <-- STARTING
+
+"""
+
+import atexit
+
+try:
+    import ctypes
+except (ImportError, MemoryError):
+    """Google AppEngine is shipped without ctypes
+
+    :seealso: http://stackoverflow.com/a/6523777/70170
+    """
+    ctypes = None
+
+import operator
+import os
+import sys
+import threading
+import time
+import traceback as _traceback
+import warnings
+import subprocess
+import functools
+
+import six
+
+
+# Here I save the value of os.getcwd(), which, if I am imported early enough,
+# will be the directory from which the startup script was run.  This is needed
+# by _do_execv(), to change back to the original directory before execv()ing a
+# new process.  This is a defense against the application having changed the
+# current working directory (which could make sys.executable "not found" if
+# sys.executable is a relative-path, and/or cause other problems).
+_startup_cwd = os.getcwd()
+
+
+class ChannelFailures(Exception):
+    """Exception raised during errors on Bus.publish()."""
+
+    delimiter = '\n'
+
+    def __init__(self, *args, **kwargs):
+        """Initialize ChannelFailures errors wrapper."""
+        super(ChannelFailures, self).__init__(*args, **kwargs)
+        self._exceptions = list()
+
+    def handle_exception(self):
+        """Append the current exception to self."""
+        self._exceptions.append(sys.exc_info()[1])
+
+    def get_instances(self):
+        """Return a list of seen exception instances."""
+        return self._exceptions[:]
+
+    def __str__(self):
+        """Render the list of errors, which happened in channel."""
+        exception_strings = map(repr, self.get_instances())
+        return self.delimiter.join(exception_strings)
+
+    __repr__ = __str__
+
+    def __bool__(self):
+        """Determine whether any error happened in channel."""
+        return bool(self._exceptions)
+    __nonzero__ = __bool__
+
+# Use a flag to indicate the state of the bus.
+
+
+class _StateEnum(object):
+
+    class State(object):
+        name = None
+
+        def __repr__(self):
+            return 'states.%s' % self.name
+
+    def __setattr__(self, key, value):
+        if isinstance(value, self.State):
+            value.name = key
+        object.__setattr__(self, key, value)
+
+
+states = _StateEnum()
+states.STOPPED = states.State()
+states.STARTING = states.State()
+states.STARTED = states.State()
+states.STOPPING = states.State()
+states.EXITING = states.State()
+
+
+try:
+    import fcntl
+except ImportError:
+    max_files = 0
+else:
+    try:
+        max_files = os.sysconf('SC_OPEN_MAX')
+    except AttributeError:
+        max_files = 1024
+
+
+class Bus(object):
+    """Process state-machine and messenger for HTTP site deployment.
+
+    All listeners for a given channel are guaranteed to be called even
+    if others at the same channel fail. Each failure is logged, but
+    execution proceeds on to the next listener. The only way to stop all
+    processing from inside a listener is to raise SystemExit and stop the
+    whole server.
+    """
+
+    states = states
+    state = states.STOPPED
+    execv = False
+    max_cloexec_files = max_files
+
+    def __init__(self):
+        """Initialize pub/sub bus."""
+        self.execv = False
+        self.state = states.STOPPED
+        channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
+        self.listeners = dict(
+            (channel, set())
+            for channel in channels
+        )
+        self._priorities = {}
+
+    def subscribe(self, channel, callback=None, priority=None):
+        """Add the given callback at the given channel (if not present).
+
+        If callback is None, return a partial suitable for decorating
+        the callback.
+        """
+        if callback is None:
+            return functools.partial(
+                self.subscribe,
+                channel,
+                priority=priority,
+            )
+
+        ch_listeners = self.listeners.setdefault(channel, set())
+        ch_listeners.add(callback)
+
+        if priority is None:
+            priority = getattr(callback, 'priority', 50)
+        self._priorities[(channel, callback)] = priority
+
+    def unsubscribe(self, channel, callback):
+        """Discard the given callback (if present)."""
+        listeners = self.listeners.get(channel)
+        if listeners and callback in listeners:
+            listeners.discard(callback)
+            del self._priorities[(channel, callback)]
+
+    def publish(self, channel, *args, **kwargs):
+        """Return output of all subscribers for the given channel."""
+        if channel not in self.listeners:
+            return []
+
+        exc = ChannelFailures()
+        output = []
+
+        raw_items = (
+            (self._priorities[(channel, listener)], listener)
+            for listener in self.listeners[channel]
+        )
+        items = sorted(raw_items, key=operator.itemgetter(0))
+        for priority, listener in items:
+            try:
+                output.append(listener(*args, **kwargs))
+            except KeyboardInterrupt:
+                raise
+            except SystemExit:
+                e = sys.exc_info()[1]
+                # If we have previous errors ensure the exit code is non-zero
+                if exc and e.code == 0:
+                    e.code = 1
+                raise
+            except Exception:
+                exc.handle_exception()
+                if channel == 'log':
+                    # Assume any further messages to 'log' will fail.
+                    pass
+                else:
+                    self.log('Error in %r listener %r' % (channel, listener),
+                             level=40, traceback=True)
+        if exc:
+            raise exc
+        return output
+
+    def _clean_exit(self):
+        """Assert that the Bus is not running in atexit handler callback."""
+        if self.state != states.EXITING:
+            warnings.warn(
+                'The main thread is exiting, but the Bus is in the %r state; '
+                'shutting it down automatically now. You must either call '
+                'bus.block() after start(), or call bus.exit() before the '
+                'main thread exits.' % self.state, RuntimeWarning)
+            self.exit()
+
+    def start(self):
+        """Start all services."""
+        atexit.register(self._clean_exit)
+
+        self.state = states.STARTING
+        self.log('Bus STARTING')
+        try:
+            self.publish('start')
+            self.state = states.STARTED
+            self.log('Bus STARTED')
+        except (KeyboardInterrupt, SystemExit):
+            raise
+        except Exception:
+            self.log('Shutting down due to error in start listener:',
+                     level=40, traceback=True)
+            e_info = sys.exc_info()[1]
+            try:
+                self.exit()
+            except Exception:
+                # Any stop/exit errors will be logged inside publish().
+                pass
+            # Re-raise the original error
+            raise e_info
+
+    def exit(self):
+        """Stop all services and prepare to exit the process."""
+        exitstate = self.state
+        EX_SOFTWARE = 70
+        try:
+            self.stop()
+
+            self.state = states.EXITING
+            self.log('Bus EXITING')
+            self.publish('exit')
+            # This isn't strictly necessary, but it's better than seeing
+            # "Waiting for child threads to terminate..." and then nothing.
+            self.log('Bus EXITED')
+        except Exception:
+            # This method is often called asynchronously (whether thread,
+            # signal handler, console handler, or atexit handler), so we
+            # can't just let exceptions propagate out unhandled.
+            # Assume it's been logged and just die.
+            os._exit(EX_SOFTWARE)
+
+        if exitstate == states.STARTING:
+            # exit() was called before start() finished, possibly due to
+            # Ctrl-C because a start listener got stuck. In this case,
+            # we could get stuck in a loop where Ctrl-C never exits the
+            # process, so we just call os.exit here.
+            os._exit(EX_SOFTWARE)
+
+    def restart(self):
+        """Restart the process (may close connections).
+
+        This method does not restart the process from the calling thread;
+        instead, it stops the bus and asks the main thread to call execv.
+        """
+        self.execv = True
+        self.exit()
+
+    def graceful(self):
+        """Advise all services to reload."""
+        self.log('Bus graceful')
+        self.publish('graceful')
+
+    def block(self, interval=0.1):
+        """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
+
+        This function is intended to be called only by the main thread.
+        After waiting for the EXITING state, it also waits for all threads
+        to terminate, and then calls os.execv if self.execv is True. This
+        design allows another thread to call bus.restart, yet have the main
+        thread perform the actual execv call (required on some platforms).
+        """
+        try:
+            self.wait(states.EXITING, interval=interval, channel='main')
+        except (KeyboardInterrupt, IOError):
+            # The time.sleep call might raise
+            # "IOError: [Errno 4] Interrupted function call" on KBInt.
+            self.log('Keyboard Interrupt: shutting down bus')
+            self.exit()
+        except SystemExit:
+            self.log('SystemExit raised: shutting down bus')
+            self.exit()
+            raise
+
+        # Waiting for ALL child threads to finish is necessary on OS X.
+        # See https://github.com/cherrypy/cherrypy/issues/581.
+        # It's also good to let them all shut down before allowing
+        # the main thread to call atexit handlers.
+        # See https://github.com/cherrypy/cherrypy/issues/751.
+        self.log('Waiting for child threads to terminate...')
+        for t in threading.enumerate():
+            # Validate the we're not trying to join the MainThread
+            # that will cause a deadlock and the case exist when
+            # implemented as a windows service and in any other case
+            # that another thread executes cherrypy.engine.exit()
+            if (
+                    t != threading.currentThread() and
+                    not isinstance(t, threading._MainThread) and
+                    # Note that any dummy (external) threads are
+                    # always daemonic.
+                    not t.daemon
+            ):
+                self.log('Waiting for thread %s.' % t.getName())
+                t.join()
+
+        if self.execv:
+            self._do_execv()
+
+    def wait(self, state, interval=0.1, channel=None):
+        """Poll for the given state(s) at intervals; publish to channel."""
+        if isinstance(state, (tuple, list)):
+            states = state
+        else:
+            states = [state]
+
+        while self.state not in states:
+            time.sleep(interval)
+            self.publish(channel)
+
+    def _do_execv(self):
+        """Re-execute the current process.
+
+        This must be called from the main thread, because certain platforms
+        (OS X) don't allow execv to be called in a child thread very well.
+        """
+        try:
+            args = self._get_true_argv()
+        except NotImplementedError:
+            """It's probably win32 or GAE"""
+            args = [sys.executable] + self._get_interpreter_argv() + sys.argv
+
+        self.log('Re-spawning %s' % ' '.join(args))
+
+        self._extend_pythonpath(os.environ)
+
+        if sys.platform[:4] == 'java':
+            from _systemrestart import SystemRestart
+            raise SystemRestart
+        else:
+            if sys.platform == 'win32':
+                args = ['"%s"' % arg for arg in args]
+
+            os.chdir(_startup_cwd)
+            if self.max_cloexec_files:
+                self._set_cloexec()
+            os.execv(sys.executable, args)
+
+    @staticmethod
+    def _get_interpreter_argv():
+        """Retrieve current Python interpreter's arguments.
+
+        Returns empty tuple in case of frozen mode, uses built-in arguments
+        reproduction function otherwise.
+
+        Frozen mode is possible for the app has been packaged into a binary
+        executable using py2exe. In this case the interpreter's arguments are
+        already built-in into that executable.
+
+        :seealso: https://github.com/cherrypy/cherrypy/issues/1526
+        Ref: https://pythonhosted.org/PyInstaller/runtime-information.html
+        """
+        return ([]
+                if getattr(sys, 'frozen', False)
+                else subprocess._args_from_interpreter_flags())
+
+    @staticmethod
+    def _get_true_argv():
+        """Retrieve all real arguments of the python interpreter.
+
+        ...even those not listed in ``sys.argv``
+
+        :seealso: http://stackoverflow.com/a/28338254/595220
+        :seealso: http://stackoverflow.com/a/6683222/595220
+        :seealso: http://stackoverflow.com/a/28414807/595220
+        """
+        try:
+            char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
+
+            argv = ctypes.POINTER(char_p)()
+            argc = ctypes.c_int()
+
+            ctypes.pythonapi.Py_GetArgcArgv(
+                ctypes.byref(argc),
+                ctypes.byref(argv),
+            )
+
+            _argv = argv[:argc.value]
+
+            # The code below is trying to correctly handle special cases.
+            # `-c`'s argument interpreted by Python itself becomes `-c` as
+            # well. Same applies to `-m`. This snippet is trying to survive
+            # at least the case with `-m`
+            # Ref: https://github.com/cherrypy/cherrypy/issues/1545
+            # Ref: python/cpython@418baf9
+            argv_len, is_command, is_module = len(_argv), False, False
+
+            try:
+                m_ind = _argv.index('-m')
+                if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'):
+                    """
+                    In some older Python versions `-m`'s argument may be
+                    substituted with `-c`, not `-m`
+                    """
+                    is_module = True
+            except (IndexError, ValueError):
+                m_ind = None
+
+            try:
+                c_ind = _argv.index('-c')
+                if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
+                    is_command = True
+            except (IndexError, ValueError):
+                c_ind = None
+
+            if is_module:
+                """It's containing `-m -m` sequence of arguments"""
+                if is_command and c_ind < m_ind:
+                    """There's `-c -c` before `-m`"""
+                    raise RuntimeError(
+                        "Cannot reconstruct command from '-c'. Ref: "
+                        'https://github.com/cherrypy/cherrypy/issues/1545')
+                # Survive module argument here
+                original_module = sys.argv[0]
+                if not os.access(original_module, os.R_OK):
+                    """There's no such module exist"""
+                    raise AttributeError(
+                        "{} doesn't seem to be a module "
+                        'accessible by current user'.format(original_module))
+                del _argv[m_ind:m_ind + 2]  # remove `-m -m`
+                # ... and substitute it with the original module path:
+                _argv.insert(m_ind, original_module)
+            elif is_command:
+                """It's containing just `-c -c` sequence of arguments"""
+                raise RuntimeError(
+                    "Cannot reconstruct command from '-c'. "
+                    'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
+        except AttributeError:
+            """It looks Py_GetArgcArgv is completely absent in some environments
+
+            It is known, that there's no Py_GetArgcArgv in MS Windows and
+            ``ctypes`` module is completely absent in Google AppEngine
+
+            :seealso: https://github.com/cherrypy/cherrypy/issues/1506
+            :seealso: https://github.com/cherrypy/cherrypy/issues/1512
+            :ref: http://bit.ly/2gK6bXK
+            """
+            raise NotImplementedError
+        else:
+            return _argv
+
+    @staticmethod
+    def _extend_pythonpath(env):
+        """Prepend current working dir to PATH environment variable if needed.
+
+        If sys.path[0] is an empty string, the interpreter was likely
+        invoked with -m and the effective path is about to change on
+        re-exec.  Add the current directory to $PYTHONPATH to ensure
+        that the new process sees the same path.
+
+        This issue cannot be addressed in the general case because
+        Python cannot reliably reconstruct the
+        original command line (http://bugs.python.org/issue14208).
+
+        (This idea filched from tornado.autoreload)
+        """
+        path_prefix = '.' + os.pathsep
+        existing_path = env.get('PYTHONPATH', '')
+        needs_patch = (
+            sys.path[0] == '' and
+            not existing_path.startswith(path_prefix)
+        )
+
+        if needs_patch:
+            env['PYTHONPATH'] = path_prefix + existing_path
+
+    def _set_cloexec(self):
+        """Set the CLOEXEC flag on all open files (except stdin/out/err).
+
+        If self.max_cloexec_files is an integer (the default), then on
+        platforms which support it, it represents the max open files setting
+        for the operating system. This function will be called just before
+        the process is restarted via os.execv() to prevent open files
+        from persisting into the new process.
+
+        Set self.max_cloexec_files to 0 to disable this behavior.
+        """
+        for fd in range(3, self.max_cloexec_files):  # skip stdin/out/err
+            try:
+                flags = fcntl.fcntl(fd, fcntl.F_GETFD)
+            except IOError:
+                continue
+            fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
+
+    def stop(self):
+        """Stop all services."""
+        self.state = states.STOPPING
+        self.log('Bus STOPPING')
+        self.publish('stop')
+        self.state = states.STOPPED
+        self.log('Bus STOPPED')
+
+    def start_with_callback(self, func, args=None, kwargs=None):
+        """Start 'func' in a new thread T, then start self (and return T)."""
+        if args is None:
+            args = ()
+        if kwargs is None:
+            kwargs = {}
+        args = (func,) + args
+
+        def _callback(func, *a, **kw):
+            self.wait(states.STARTED)
+            func(*a, **kw)
+        t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
+        t.setName('Bus Callback ' + t.getName())
+        t.start()
+
+        self.start()
+
+        return t
+
+    def log(self, msg='', level=20, traceback=False):
+        """Log the given message. Append the last traceback if requested."""
+        if traceback:
+            msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
+        self.publish('log', msg, level)
+
+
+bus = Bus()
diff --git a/libraries/cherrypy/scaffold/__init__.py b/libraries/cherrypy/scaffold/__init__.py
new file mode 100644
index 00000000..bcddba2d
--- /dev/null
+++ b/libraries/cherrypy/scaffold/__init__.py
@@ -0,0 +1,63 @@
+"""<MyProject>, a CherryPy application.
+
+Use this as a base for creating new CherryPy applications. When you want
+to make a new app, copy and paste this folder to some other location
+(maybe site-packages) and rename it to the name of your project,
+then tweak as desired.
+
+Even before any tweaking, this should serve a few demonstration pages.
+Change to this directory and run:
+
+    cherryd -c site.conf
+
+"""
+
+import cherrypy
+from cherrypy import tools, url
+
+import os
+local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+@cherrypy.config(**{'tools.log_tracebacks.on': True})
+class Root:
+    """Declaration of the CherryPy app URI structure."""
+
+    @cherrypy.expose
+    def index(self):
+        """Render HTML-template at the root path of the web-app."""
+        return """<html>
+<body>Try some <a href='%s?a=7'>other</a> path,
+or a <a href='%s?n=14'>default</a> path.<br />
+Or, just look at the pretty picture:<br />
+<img src='%s' />
+</body></html>""" % (url('other'), url('else'),
+                     url('files/made_with_cherrypy_small.png'))
+
+    @cherrypy.expose
+    def default(self, *args, **kwargs):
+        """Render catch-all args and kwargs."""
+        return 'args: %s kwargs: %s' % (args, kwargs)
+
+    @cherrypy.expose
+    def other(self, a=2, b='bananas', c=None):
+        """Render number of fruits based on third argument."""
+        cherrypy.response.headers['Content-Type'] = 'text/plain'
+        if c is None:
+            return 'Have %d %s.' % (int(a), b)
+        else:
+            return 'Have %d %s, %s.' % (int(a), b, c)
+
+    files = tools.staticdir.handler(
+        section='/files',
+        dir=os.path.join(local_dir, 'static'),
+        # Ignore .php files, etc.
+                match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
+    )
+
+
+root = Root()
+
+# Uncomment the following to use your own favicon instead of CP's default.
+# favicon_path = os.path.join(local_dir, "favicon.ico")
+# root.favicon_ico = tools.staticfile.handler(filename=favicon_path)
diff --git a/libraries/cherrypy/scaffold/apache-fcgi.conf b/libraries/cherrypy/scaffold/apache-fcgi.conf
new file mode 100644
index 00000000..6e4f144c
--- /dev/null
+++ b/libraries/cherrypy/scaffold/apache-fcgi.conf
@@ -0,0 +1,22 @@
+# Apache2 server conf file for using CherryPy with mod_fcgid.
+
+# This doesn't have to be "C:/", but it has to be a directory somewhere, and
+# MUST match the directory used in the FastCgiExternalServer directive, below.
+DocumentRoot "C:/"
+
+ServerName 127.0.0.1
+Listen 80
+LoadModule fastcgi_module modules/mod_fastcgi.dll
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+# Send requests for any URI to our fastcgi handler.
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+
+# The FastCgiExternalServer directive defines filename as an external FastCGI application.
+# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot.
+# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this
+# filename will be handled by this external FastCGI application.
+FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088
diff --git a/libraries/cherrypy/scaffold/example.conf b/libraries/cherrypy/scaffold/example.conf
new file mode 100644
index 00000000..63250fe3
--- /dev/null
+++ b/libraries/cherrypy/scaffold/example.conf
@@ -0,0 +1,3 @@
+[/]
+log.error_file: "error.log"
+log.access_file: "access.log"
diff --git a/libraries/cherrypy/scaffold/site.conf b/libraries/cherrypy/scaffold/site.conf
new file mode 100644
index 00000000..6ed38983
--- /dev/null
+++ b/libraries/cherrypy/scaffold/site.conf
@@ -0,0 +1,14 @@
+[global]
+# Uncomment this when you're done developing
+#environment: "production"
+
+server.socket_host: "0.0.0.0"
+server.socket_port: 8088
+
+# Uncomment the following lines to run on HTTPS at the same time
+#server.2.socket_host: "0.0.0.0"
+#server.2.socket_port: 8433
+#server.2.ssl_certificate: '../test/test.pem'
+#server.2.ssl_private_key: '../test/test.pem'
+
+tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf")
diff --git a/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png
new file mode 100644
index 0000000000000000000000000000000000000000..724f9d72d9ca5aede0b788fc1216286aa967e212
GIT binary patch
literal 6347
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
N002ovPDHLkV1nY?RmA`R

literal 0
HcmV?d00001

diff --git a/libraries/cherrypy/test/__init__.py b/libraries/cherrypy/test/__init__.py
new file mode 100644
index 00000000..068382be
--- /dev/null
+++ b/libraries/cherrypy/test/__init__.py
@@ -0,0 +1,24 @@
+"""
+Regression test suite for CherryPy.
+"""
+
+import os
+import sys
+
+
+def newexit():
+    os._exit(1)
+
+
+def setup():
+    # We want to monkey patch sys.exit so that we can get some
+    # information about where exit is being called.
+    newexit._old = sys.exit
+    sys.exit = newexit
+
+
+def teardown():
+    try:
+        sys.exit = sys.exit._old
+    except AttributeError:
+        sys.exit = sys._exit
diff --git a/libraries/cherrypy/test/_test_decorators.py b/libraries/cherrypy/test/_test_decorators.py
new file mode 100644
index 00000000..74832e40
--- /dev/null
+++ b/libraries/cherrypy/test/_test_decorators.py
@@ -0,0 +1,39 @@
+"""Test module for the @-decorator syntax, which is version-specific"""
+
+import cherrypy
+from cherrypy import expose, tools
+
+
+class ExposeExamples(object):
+
+    @expose
+    def no_call(self):
+        return 'Mr E. R. Bradshaw'
+
+    @expose()
+    def call_empty(self):
+        return 'Mrs. B.J. Smegma'
+
+    @expose('call_alias')
+    def nesbitt(self):
+        return 'Mr Nesbitt'
+
+    @expose(['alias1', 'alias2'])
+    def andrews(self):
+        return 'Mr Ken Andrews'
+
+    @expose(alias='alias3')
+    def watson(self):
+        return 'Mr. and Mrs. Watson'
+
+
+class ToolExamples(object):
+
+    @expose
+    # This is here to demonstrate that using the config decorator
+    # does not overwrite other config attributes added by the Tool
+    # decorator (in this case response_headers).
+    @cherrypy.config(**{'response.stream': True})
+    @tools.response_headers(headers=[('Content-Type', 'application/data')])
+    def blah(self):
+        yield b'blah'
diff --git a/libraries/cherrypy/test/_test_states_demo.py b/libraries/cherrypy/test/_test_states_demo.py
new file mode 100644
index 00000000..a49407ba
--- /dev/null
+++ b/libraries/cherrypy/test/_test_states_demo.py
@@ -0,0 +1,69 @@
+import os
+import sys
+import time
+
+import cherrypy
+
+starttime = time.time()
+
+
+class Root:
+
+    @cherrypy.expose
+    def index(self):
+        return 'Hello World'
+
+    @cherrypy.expose
+    def mtimes(self):
+        return repr(cherrypy.engine.publish('Autoreloader', 'mtimes'))
+
+    @cherrypy.expose
+    def pid(self):
+        return str(os.getpid())
+
+    @cherrypy.expose
+    def start(self):
+        return repr(starttime)
+
+    @cherrypy.expose
+    def exit(self):
+        # This handler might be called before the engine is STARTED if an
+        # HTTP worker thread handles it before the HTTP server returns
+        # control to engine.start. We avoid that race condition here
+        # by waiting for the Bus to be STARTED.
+        cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
+        cherrypy.engine.exit()
+
+
+@cherrypy.engine.subscribe('start', priority=100)
+def unsub_sig():
+    cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False))
+    if cherrypy.config.get('unsubsig', False):
+        cherrypy.log('Unsubscribing the default cherrypy signal handler')
+        cherrypy.engine.signal_handler.unsubscribe()
+    try:
+        from signal import signal, SIGTERM
+    except ImportError:
+        pass
+    else:
+        def old_term_handler(signum=None, frame=None):
+            cherrypy.log('I am an old SIGTERM handler.')
+            sys.exit(0)
+        cherrypy.log('Subscribing the new one.')
+        signal(SIGTERM, old_term_handler)
+
+
+@cherrypy.engine.subscribe('start', priority=6)
+def starterror():
+    if cherrypy.config.get('starterror', False):
+        1 / 0
+
+
+@cherrypy.engine.subscribe('start', priority=6)
+def log_test_case_name():
+    if cherrypy.config.get('test_case_name', False):
+        cherrypy.log('STARTED FROM: %s' %
+                     cherrypy.config.get('test_case_name'))
+
+
+cherrypy.tree.mount(Root(), '/', {'/': {}})
diff --git a/libraries/cherrypy/test/benchmark.py b/libraries/cherrypy/test/benchmark.py
new file mode 100644
index 00000000..44dfeff1
--- /dev/null
+++ b/libraries/cherrypy/test/benchmark.py
@@ -0,0 +1,425 @@
+"""CherryPy Benchmark Tool
+
+    Usage:
+        benchmark.py [options]
+
+    --null:        use a null Request object (to bench the HTTP server only)
+    --notests:     start the server but do not run the tests; this allows
+                   you to check the tested pages with a browser
+    --help:        show this help message
+    --cpmodpy:     run tests via apache on 54583 (with the builtin _cpmodpy)
+    --modpython:   run tests via apache on 54583 (with modpython_gateway)
+    --ab=path:     Use the ab script/executable at 'path' (see below)
+    --apache=path: Use the apache script/exe at 'path' (see below)
+
+    To run the benchmarks, the Apache Benchmark tool "ab" must either be on
+    your system path, or specified via the --ab=path option.
+
+    To run the modpython tests, the "apache" executable or script must be
+    on your system path, or provided via the --apache=path option. On some
+    platforms, "apache" may be called "apachectl" or "apache2ctl"--create
+    a symlink to them if needed.
+"""
+
+import getopt
+import os
+import re
+import sys
+import time
+
+import cherrypy
+from cherrypy import _cperror, _cpmodpy
+from cherrypy.lib import httputil
+
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+AB_PATH = ''
+APACHE_PATH = 'apache'
+SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog'
+
+__all__ = ['ABSession', 'Root', 'print_report',
+           'run_standard_benchmarks', 'safe_threads',
+           'size_report', 'thread_report',
+           ]
+
+size_cache = {}
+
+
+class Root:
+
+    @cherrypy.expose
+    def index(self):
+        return """<html>
+<head>
+    <title>CherryPy Benchmark</title>
+</head>
+<body>
+    <ul>
+        <li><a href="hello">Hello, world! (14 byte dynamic)</a></li>
+        <li><a href="static/index.html">Static file (14 bytes static)</a></li>
+        <li><form action="sizer">Response of length:
+            <input type='text' name='size' value='10' /></form>
+        </li>
+    </ul>
+</body>
+</html>"""
+
+    @cherrypy.expose
+    def hello(self):
+        return 'Hello, world\r\n'
+
+    @cherrypy.expose
+    def sizer(self, size):
+        resp = size_cache.get(size, None)
+        if resp is None:
+            size_cache[size] = resp = 'X' * int(size)
+        return resp
+
+
+def init():
+
+    cherrypy.config.update({
+        'log.error.file': '',
+        'environment': 'production',
+        'server.socket_host': '127.0.0.1',
+        'server.socket_port': 54583,
+        'server.max_request_header_size': 0,
+        'server.max_request_body_size': 0,
+    })
+
+    # Cheat mode on ;)
+    del cherrypy.config['tools.log_tracebacks.on']
+    del cherrypy.config['tools.log_headers.on']
+    del cherrypy.config['tools.trailing_slash.on']
+
+    appconf = {
+        '/static': {
+            'tools.staticdir.on': True,
+            'tools.staticdir.dir': 'static',
+            'tools.staticdir.root': curdir,
+        },
+    }
+    globals().update(
+        app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf),
+    )
+
+
+class NullRequest:
+
+    """A null HTTP request class, returning 200 and an empty body."""
+
+    def __init__(self, local, remote, scheme='http'):
+        pass
+
+    def close(self):
+        pass
+
+    def run(self, method, path, query_string, protocol, headers, rfile):
+        cherrypy.response.status = '200 OK'
+        cherrypy.response.header_list = [('Content-Type', 'text/html'),
+                                         ('Server', 'Null CherryPy'),
+                                         ('Date', httputil.HTTPDate()),
+                                         ('Content-Length', '0'),
+                                         ]
+        cherrypy.response.body = ['']
+        return cherrypy.response
+
+
+class NullResponse:
+    pass
+
+
+class ABSession:
+
+    """A session of 'ab', the Apache HTTP server benchmarking tool.
+
+Example output from ab:
+
+This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
+Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
+Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
+
+Benchmarking 127.0.0.1 (be patient)
+Completed 100 requests
+Completed 200 requests
+Completed 300 requests
+Completed 400 requests
+Completed 500 requests
+Completed 600 requests
+Completed 700 requests
+Completed 800 requests
+Completed 900 requests
+
+
+Server Software:        CherryPy/3.1beta
+Server Hostname:        127.0.0.1
+Server Port:            54583
+
+Document Path:          /static/index.html
+Document Length:        14 bytes
+
+Concurrency Level:      10
+Time taken for tests:   9.643867 seconds
+Complete requests:      1000
+Failed requests:        0
+Write errors:           0
+Total transferred:      189000 bytes
+HTML transferred:       14000 bytes
+Requests per second:    103.69 [#/sec] (mean)
+Time per request:       96.439 [ms] (mean)
+Time per request:       9.644 [ms] (mean, across all concurrent requests)
+Transfer rate:          19.08 [Kbytes/sec] received
+
+Connection Times (ms)
+              min  mean[+/-sd] median   max
+Connect:        0    0   2.9      0      10
+Processing:    20   94   7.3     90     130
+Waiting:        0   43  28.1     40     100
+Total:         20   95   7.3    100     130
+
+Percentage of the requests served within a certain time (ms)
+  50%    100
+  66%    100
+  75%    100
+  80%    100
+  90%    100
+  95%    100
+  98%    100
+  99%    110
+ 100%    130 (longest request)
+Finished 1000 requests
+"""
+
+    parse_patterns = [
+        ('complete_requests', 'Completed',
+         br'^Complete requests:\s*(\d+)'),
+        ('failed_requests', 'Failed',
+         br'^Failed requests:\s*(\d+)'),
+        ('requests_per_second', 'req/sec',
+         br'^Requests per second:\s*([0-9.]+)'),
+        ('time_per_request_concurrent', 'msec/req',
+         br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'),
+        ('transfer_rate', 'KB/sec',
+         br'^Transfer rate:\s*([0-9.]+)')
+    ]
+
+    def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000,
+                 concurrency=10):
+        self.path = path
+        self.requests = requests
+        self.concurrency = concurrency
+
+    def args(self):
+        port = cherrypy.server.socket_port
+        assert self.concurrency > 0
+        assert self.requests > 0
+        # Don't use "localhost".
+        # Cf
+        # http://mail.python.org/pipermail/python-win32/2008-March/007050.html
+        return ('-k -n %s -c %s http://127.0.0.1:%s%s' %
+                (self.requests, self.concurrency, port, self.path))
+
+    def run(self):
+        # Parse output of ab, setting attributes on self
+        try:
+            self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args())
+        except Exception:
+            print(_cperror.format_exc())
+            raise
+
+        for attr, name, pattern in self.parse_patterns:
+            val = re.search(pattern, self.output, re.MULTILINE)
+            if val:
+                val = val.group(1)
+                setattr(self, attr, val)
+            else:
+                setattr(self, attr, None)
+
+
+safe_threads = (25, 50, 100, 200, 400)
+if sys.platform in ('win32',):
+    # For some reason, ab crashes with > 50 threads on my Win2k laptop.
+    safe_threads = (10, 20, 30, 40, 50)
+
+
+def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads):
+    sess = ABSession(path)
+    attrs, names, patterns = list(zip(*sess.parse_patterns))
+    avg = dict.fromkeys(attrs, 0.0)
+
+    yield ('threads',) + names
+    for c in concurrency:
+        sess.concurrency = c
+        sess.run()
+        row = [c]
+        for attr in attrs:
+            val = getattr(sess, attr)
+            if val is None:
+                print(sess.output)
+                row = None
+                break
+            val = float(val)
+            avg[attr] += float(val)
+            row.append(val)
+        if row:
+            yield row
+
+    # Add a row of averages.
+    yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs]
+
+
+def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
+                concurrency=50):
+    sess = ABSession(concurrency=concurrency)
+    attrs, names, patterns = list(zip(*sess.parse_patterns))
+    yield ('bytes',) + names
+    for sz in sizes:
+        sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz)
+        sess.run()
+        yield [sz] + [getattr(sess, attr) for attr in attrs]
+
+
+def print_report(rows):
+    for row in rows:
+        print('')
+        for val in row:
+            sys.stdout.write(str(val).rjust(10) + ' | ')
+    print('')
+
+
+def run_standard_benchmarks():
+    print('')
+    print('Client Thread Report (1000 requests, 14 byte response body, '
+          '%s server threads):' % cherrypy.server.thread_pool)
+    print_report(thread_report())
+
+    print('')
+    print('Client Thread Report (1000 requests, 14 bytes via staticdir, '
+          '%s server threads):' % cherrypy.server.thread_pool)
+    print_report(thread_report('%s/static/index.html' % SCRIPT_NAME))
+
+    print('')
+    print('Size Report (1000 requests, 50 client threads, '
+          '%s server threads):' % cherrypy.server.thread_pool)
+    print_report(size_report())
+
+
+#                         modpython and other WSGI                         #
+
+def startup_modpython(req=None):
+    """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).
+    """
+    if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
+        if req:
+            if 'nullreq' in req.get_options():
+                cherrypy.engine.request_class = NullRequest
+                cherrypy.engine.response_class = NullResponse
+            ab_opt = req.get_options().get('ab', '')
+            if ab_opt:
+                global AB_PATH
+                AB_PATH = ab_opt
+        cherrypy.engine.start()
+    if cherrypy.engine.state == cherrypy._cpengine.STARTING:
+        cherrypy.engine.wait()
+    return 0  # apache.OK
+
+
+def run_modpython(use_wsgi=False):
+    print('Starting mod_python...')
+    pyopts = []
+
+    # Pass the null and ab=path options through Apache
+    if '--null' in opts:
+        pyopts.append(('nullreq', ''))
+
+    if '--ab' in opts:
+        pyopts.append(('ab', opts['--ab']))
+
+    s = _cpmodpy.ModPythonServer
+    if use_wsgi:
+        pyopts.append(('wsgi.application', 'cherrypy::tree'))
+        pyopts.append(
+            ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython'))
+        handler = 'modpython_gateway::handler'
+        s = s(port=54583, opts=pyopts,
+              apache_path=APACHE_PATH, handler=handler)
+    else:
+        pyopts.append(
+            ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython'))
+        s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH)
+
+    try:
+        s.start()
+        run()
+    finally:
+        s.stop()
+
+
+if __name__ == '__main__':
+    init()
+
+    longopts = ['cpmodpy', 'modpython', 'null', 'notests',
+                'help', 'ab=', 'apache=']
+    try:
+        switches, args = getopt.getopt(sys.argv[1:], '', longopts)
+        opts = dict(switches)
+    except getopt.GetoptError:
+        print(__doc__)
+        sys.exit(2)
+
+    if '--help' in opts:
+        print(__doc__)
+        sys.exit(0)
+
+    if '--ab' in opts:
+        AB_PATH = opts['--ab']
+
+    if '--notests' in opts:
+        # Return without stopping the server, so that the pages
+        # can be tested from a standard web browser.
+        def run():
+            port = cherrypy.server.socket_port
+            print('You may now open http://127.0.0.1:%s%s/' %
+                  (port, SCRIPT_NAME))
+
+            if '--null' in opts:
+                print('Using null Request object')
+    else:
+        def run():
+            end = time.time() - start
+            print('Started in %s seconds' % end)
+            if '--null' in opts:
+                print('\nUsing null Request object')
+            try:
+                try:
+                    run_standard_benchmarks()
+                except Exception:
+                    print(_cperror.format_exc())
+                    raise
+            finally:
+                cherrypy.engine.exit()
+
+    print('Starting CherryPy app server...')
+
+    class NullWriter(object):
+
+        """Suppresses the printing of socket errors."""
+
+        def write(self, data):
+            pass
+    sys.stderr = NullWriter()
+
+    start = time.time()
+
+    if '--cpmodpy' in opts:
+        run_modpython()
+    elif '--modpython' in opts:
+        run_modpython(use_wsgi=True)
+    else:
+        if '--null' in opts:
+            cherrypy.server.request_class = NullRequest
+            cherrypy.server.response_class = NullResponse
+
+        cherrypy.engine.start_with_callback(run)
+        cherrypy.engine.block()
diff --git a/libraries/cherrypy/test/checkerdemo.py b/libraries/cherrypy/test/checkerdemo.py
new file mode 100644
index 00000000..3438bd0c
--- /dev/null
+++ b/libraries/cherrypy/test/checkerdemo.py
@@ -0,0 +1,49 @@
+"""Demonstration app for cherrypy.checker.
+
+This application is intentionally broken and badly designed.
+To demonstrate the output of the CherryPy Checker, simply execute
+this module.
+"""
+
+import os
+import cherrypy
+thisdir = os.path.dirname(os.path.abspath(__file__))
+
+
+class Root:
+    pass
+
+
+if __name__ == '__main__':
+    conf = {'/base': {'tools.staticdir.root': thisdir,
+                      # Obsolete key.
+                      'throw_errors': True,
+                      },
+            # This entry should be OK.
+            '/base/static': {'tools.staticdir.on': True,
+                             'tools.staticdir.dir': 'static'},
+            # Warn on missing folder.
+            '/base/js': {'tools.staticdir.on': True,
+                         'tools.staticdir.dir': 'js'},
+            # Warn on dir with an abs path even though we provide root.
+            '/base/static2': {'tools.staticdir.on': True,
+                              'tools.staticdir.dir': '/static'},
+            # Warn on dir with a relative path with no root.
+            '/static3': {'tools.staticdir.on': True,
+                         'tools.staticdir.dir': 'static'},
+            # Warn on unknown namespace
+            '/unknown': {'toobles.gzip.on': True},
+            # Warn special on cherrypy.<known ns>.*
+            '/cpknown': {'cherrypy.tools.encode.on': True},
+            # Warn on mismatched types
+            '/conftype': {'request.show_tracebacks': 14},
+            # Warn on unknown tool.
+            '/web': {'tools.unknown.on': True},
+            # Warn on server.* in app config.
+            '/app1': {'server.socket_host': '0.0.0.0'},
+            # Warn on 'localhost'
+            'global': {'server.socket_host': 'localhost'},
+            # Warn on '[name]'
+            '[/extra_brackets]': {},
+            }
+    cherrypy.quickstart(Root(), config=conf)
diff --git a/libraries/cherrypy/test/fastcgi.conf b/libraries/cherrypy/test/fastcgi.conf
new file mode 100644
index 00000000..e5c5163c
--- /dev/null
+++ b/libraries/cherrypy/test/fastcgi.conf
@@ -0,0 +1,18 @@
+
+# Apache2 server conf file for testing CherryPy with mod_fastcgi.
+# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
+ServerRoot /usr/lib/apache2
+User #1000
+ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log
+
+DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test"
+ServerName 127.0.0.1
+Listen 8080
+LoadModule fastcgi_module modules/mod_fastcgi.so
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options +ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000
diff --git a/libraries/cherrypy/test/fcgi.conf b/libraries/cherrypy/test/fcgi.conf
new file mode 100644
index 00000000..3062eb35
--- /dev/null
+++ b/libraries/cherrypy/test/fcgi.conf
@@ -0,0 +1,14 @@
+
+# Apache2 server conf file for testing CherryPy with mod_fcgid.
+
+DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test"
+ServerName 127.0.0.1
+Listen 8080
+LoadModule fastcgi_module modules/mod_fastcgi.dll
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000
diff --git a/libraries/cherrypy/test/helper.py b/libraries/cherrypy/test/helper.py
new file mode 100644
index 00000000..01c5a0c0
--- /dev/null
+++ b/libraries/cherrypy/test/helper.py
@@ -0,0 +1,542 @@
+"""A library of helper functions for the CherryPy test suite."""
+
+import datetime
+import io
+import logging
+import os
+import re
+import subprocess
+import sys
+import time
+import unittest
+import warnings
+
+import portend
+import pytest
+import six
+
+from cheroot.test import webtest
+
+import cherrypy
+from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob
+from cherrypy.lib import httputil
+from cherrypy.lib import gctools
+
+log = logging.getLogger(__name__)
+thisdir = os.path.abspath(os.path.dirname(__file__))
+serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
+
+
+class Supervisor(object):
+
+    """Base class for modeling and controlling servers during testing."""
+
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            if k == 'port':
+                setattr(self, k, int(v))
+            setattr(self, k, v)
+
+
+def log_to_stderr(msg, level):
+    return sys.stderr.write(msg + os.linesep)
+
+
+class LocalSupervisor(Supervisor):
+
+    """Base class for modeling/controlling servers which run in the same
+    process.
+
+    When the server side runs in a different process, start/stop can dump all
+    state between each test module easily. When the server side runs in the
+    same process as the client, however, we have to do a bit more work to
+    ensure config and mounted apps are reset between tests.
+    """
+
+    using_apache = False
+    using_wsgi = False
+
+    def __init__(self, **kwargs):
+        for k, v in kwargs.items():
+            setattr(self, k, v)
+
+        cherrypy.server.httpserver = self.httpserver_class
+
+        # This is perhaps the wrong place for this call but this is the only
+        # place that i've found so far that I KNOW is early enough to set this.
+        cherrypy.config.update({'log.screen': False})
+        engine = cherrypy.engine
+        if hasattr(engine, 'signal_handler'):
+            engine.signal_handler.subscribe()
+        if hasattr(engine, 'console_control_handler'):
+            engine.console_control_handler.subscribe()
+
+    def start(self, modulename=None):
+        """Load and start the HTTP server."""
+        if modulename:
+            # Unhook httpserver so cherrypy.server.start() creates a new
+            # one (with config from setup_server, if declared).
+            cherrypy.server.httpserver = None
+
+        cherrypy.engine.start()
+
+        self.sync_apps()
+
+    def sync_apps(self):
+        """Tell the server about any apps which the setup functions mounted."""
+        pass
+
+    def stop(self):
+        td = getattr(self, 'teardown', None)
+        if td:
+            td()
+
+        cherrypy.engine.exit()
+
+        servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {})))
+        for name, server in servers_copy:
+            server.unsubscribe()
+            del cherrypy.servers[name]
+
+
+class NativeServerSupervisor(LocalSupervisor):
+
+    """Server supervisor for the builtin HTTP server."""
+
+    httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer'
+    using_apache = False
+    using_wsgi = False
+
+    def __str__(self):
+        return 'Builtin HTTP Server on %s:%s' % (self.host, self.port)
+
+
+class LocalWSGISupervisor(LocalSupervisor):
+
+    """Server supervisor for the builtin WSGI server."""
+
+    httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer'
+    using_apache = False
+    using_wsgi = True
+
+    def __str__(self):
+        return 'Builtin WSGI Server on %s:%s' % (self.host, self.port)
+
+    def sync_apps(self):
+        """Hook a new WSGI app into the origin server."""
+        cherrypy.server.httpserver.wsgi_app = self.get_app()
+
+    def get_app(self, app=None):
+        """Obtain a new (decorated) WSGI app to hook into the origin server."""
+        if app is None:
+            app = cherrypy.tree
+
+        if self.validate:
+            try:
+                from wsgiref import validate
+            except ImportError:
+                warnings.warn(
+                    'Error importing wsgiref. The validator will not run.')
+            else:
+                # wraps the app in the validator
+                app = validate.validator(app)
+
+        return app
+
+
+def get_cpmodpy_supervisor(**options):
+    from cherrypy.test import modpy
+    sup = modpy.ModPythonSupervisor(**options)
+    sup.template = modpy.conf_cpmodpy
+    return sup
+
+
+def get_modpygw_supervisor(**options):
+    from cherrypy.test import modpy
+    sup = modpy.ModPythonSupervisor(**options)
+    sup.template = modpy.conf_modpython_gateway
+    sup.using_wsgi = True
+    return sup
+
+
+def get_modwsgi_supervisor(**options):
+    from cherrypy.test import modwsgi
+    return modwsgi.ModWSGISupervisor(**options)
+
+
+def get_modfcgid_supervisor(**options):
+    from cherrypy.test import modfcgid
+    return modfcgid.ModFCGISupervisor(**options)
+
+
+def get_modfastcgi_supervisor(**options):
+    from cherrypy.test import modfastcgi
+    return modfastcgi.ModFCGISupervisor(**options)
+
+
+def get_wsgi_u_supervisor(**options):
+    cherrypy.server.wsgi_version = ('u', 0)
+    return LocalWSGISupervisor(**options)
+
+
+class CPWebCase(webtest.WebCase):
+
+    script_name = ''
+    scheme = 'http'
+
+    available_servers = {'wsgi': LocalWSGISupervisor,
+                         'wsgi_u': get_wsgi_u_supervisor,
+                         'native': NativeServerSupervisor,
+                         'cpmodpy': get_cpmodpy_supervisor,
+                         'modpygw': get_modpygw_supervisor,
+                         'modwsgi': get_modwsgi_supervisor,
+                         'modfcgid': get_modfcgid_supervisor,
+                         'modfastcgi': get_modfastcgi_supervisor,
+                         }
+    default_server = 'wsgi'
+
+    @classmethod
+    def _setup_server(cls, supervisor, conf):
+        v = sys.version.split()[0]
+        log.info('Python version used to run this test script: %s' % v)
+        log.info('CherryPy version: %s' % cherrypy.__version__)
+        if supervisor.scheme == 'https':
+            ssl = ' (ssl)'
+        else:
+            ssl = ''
+        log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl))
+        log.info('PID: %s' % os.getpid())
+
+        cherrypy.server.using_apache = supervisor.using_apache
+        cherrypy.server.using_wsgi = supervisor.using_wsgi
+
+        if sys.platform[:4] == 'java':
+            cherrypy.config.update({'server.nodelay': False})
+
+        if isinstance(conf, text_or_bytes):
+            parser = cherrypy.lib.reprconf.Parser()
+            conf = parser.dict_from_file(conf).get('global', {})
+        else:
+            conf = conf or {}
+        baseconf = conf.copy()
+        baseconf.update({'server.socket_host': supervisor.host,
+                         'server.socket_port': supervisor.port,
+                         'server.protocol_version': supervisor.protocol,
+                         'environment': 'test_suite',
+                         })
+        if supervisor.scheme == 'https':
+            # baseconf['server.ssl_module'] = 'builtin'
+            baseconf['server.ssl_certificate'] = serverpem
+            baseconf['server.ssl_private_key'] = serverpem
+
+        # helper must be imported lazily so the coverage tool
+        # can run against module-level statements within cherrypy.
+        # Also, we have to do "from cherrypy.test import helper",
+        # exactly like each test module does, because a relative import
+        # would stick a second instance of webtest in sys.modules,
+        # and we wouldn't be able to globally override the port anymore.
+        if supervisor.scheme == 'https':
+            webtest.WebCase.HTTP_CONN = HTTPSConnection
+        return baseconf
+
+    @classmethod
+    def setup_class(cls):
+        ''
+        # Creates a server
+        conf = {
+            'scheme': 'http',
+            'protocol': 'HTTP/1.1',
+            'port': 54583,
+            'host': '127.0.0.1',
+            'validate': False,
+            'server': 'wsgi',
+        }
+        supervisor_factory = cls.available_servers.get(
+            conf.get('server', 'wsgi'))
+        if supervisor_factory is None:
+            raise RuntimeError('Unknown server in config: %s' % conf['server'])
+        supervisor = supervisor_factory(**conf)
+
+        # Copied from "run_test_suite"
+        cherrypy.config.reset()
+        baseconf = cls._setup_server(supervisor, conf)
+        cherrypy.config.update(baseconf)
+        setup_client()
+
+        if hasattr(cls, 'setup_server'):
+            # Clear the cherrypy tree and clear the wsgi server so that
+            # it can be updated with the new root
+            cherrypy.tree = cherrypy._cptree.Tree()
+            cherrypy.server.httpserver = None
+            cls.setup_server()
+            # Add a resource for verifying there are no refleaks
+            # to *every* test class.
+            cherrypy.tree.mount(gctools.GCRoot(), '/gc')
+            cls.do_gc_test = True
+            supervisor.start(cls.__module__)
+
+        cls.supervisor = supervisor
+
+    @classmethod
+    def teardown_class(cls):
+        ''
+        if hasattr(cls, 'setup_server'):
+            cls.supervisor.stop()
+
+    do_gc_test = False
+
+    def test_gc(self):
+        if not self.do_gc_test:
+            return
+
+        self.getPage('/gc/stats')
+        try:
+            self.assertBody('Statistics:')
+        except Exception:
+            'Failures occur intermittently. See #1420'
+
+    def prefix(self):
+        return self.script_name.rstrip('/')
+
+    def base(self):
+        if ((self.scheme == 'http' and self.PORT == 80) or
+                (self.scheme == 'https' and self.PORT == 443)):
+            port = ''
+        else:
+            port = ':%s' % self.PORT
+
+        return '%s://%s%s%s' % (self.scheme, self.HOST, port,
+                                self.script_name.rstrip('/'))
+
+    def exit(self):
+        sys.exit()
+
+    def getPage(self, url, headers=None, method='GET', body=None,
+                protocol=None, raise_subcls=None):
+        """Open the url. Return status, headers, body.
+
+        `raise_subcls` must be a tuple with the exceptions classes
+        or a single exception class that are not going to be considered
+        a socket.error regardless that they were are subclass of a
+        socket.error and therefore not considered for a connection retry.
+        """
+        if self.script_name:
+            url = httputil.urljoin(self.script_name, url)
+        return webtest.WebCase.getPage(self, url, headers, method, body,
+                                       protocol, raise_subcls)
+
+    def skip(self, msg='skipped '):
+        pytest.skip(msg)
+
+    def assertErrorPage(self, status, message=None, pattern=''):
+        """Compare the response body with a built in error page.
+
+        The function will optionally look for the regexp pattern,
+        within the exception embedded in the error page."""
+
+        # This will never contain a traceback
+        page = cherrypy._cperror.get_error_page(status, message=message)
+
+        # First, test the response body without checking the traceback.
+        # Stick a match-all group (.*) in to grab the traceback.
+        def esc(text):
+            return re.escape(ntob(text))
+        epage = re.escape(page)
+        epage = epage.replace(
+            esc('<pre id="traceback"></pre>'),
+            esc('<pre id="traceback">') + b'(.*)' + esc('</pre>'))
+        m = re.match(epage, self.body, re.DOTALL)
+        if not m:
+            self._handlewebError(
+                'Error page does not match; expected:\n' + page)
+            return
+
+        # Now test the pattern against the traceback
+        if pattern is None:
+            # Special-case None to mean that there should be *no* traceback.
+            if m and m.group(1):
+                self._handlewebError('Error page contains traceback')
+        else:
+            if (m is None) or (
+                not re.search(ntob(re.escape(pattern), self.encoding),
+                              m.group(1))):
+                msg = 'Error page does not contain %s in traceback'
+                self._handlewebError(msg % repr(pattern))
+
+    date_tolerance = 2
+
+    def assertEqualDates(self, dt1, dt2, seconds=None):
+        """Assert abs(dt1 - dt2) is within Y seconds."""
+        if seconds is None:
+            seconds = self.date_tolerance
+
+        if dt1 > dt2:
+            diff = dt1 - dt2
+        else:
+            diff = dt2 - dt1
+        if not diff < datetime.timedelta(seconds=seconds):
+            raise AssertionError('%r and %r are not within %r seconds.' %
+                                 (dt1, dt2, seconds))
+
+
+def _test_method_sorter(_, x, y):
+    """Monkeypatch the test sorter to always run test_gc last in each suite."""
+    if x == 'test_gc':
+        return 1
+    if y == 'test_gc':
+        return -1
+    if x > y:
+        return 1
+    if x < y:
+        return -1
+    return 0
+
+
+unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter
+
+
+def setup_client():
+    """Set up the WebCase classes to match the server's socket settings."""
+    webtest.WebCase.PORT = cherrypy.server.socket_port
+    webtest.WebCase.HOST = cherrypy.server.socket_host
+    if cherrypy.server.ssl_certificate:
+        CPWebCase.scheme = 'https'
+
+# --------------------------- Spawning helpers --------------------------- #
+
+
+class CPProcess(object):
+
+    pid_file = os.path.join(thisdir, 'test.pid')
+    config_file = os.path.join(thisdir, 'test.conf')
+    config_template = """[global]
+server.socket_host: '%(host)s'
+server.socket_port: %(port)s
+checker.on: False
+log.screen: False
+log.error_file: r'%(error_log)s'
+log.access_file: r'%(access_log)s'
+%(ssl)s
+%(extra)s
+"""
+    error_log = os.path.join(thisdir, 'test.error.log')
+    access_log = os.path.join(thisdir, 'test.access.log')
+
+    def __init__(self, wait=False, daemonize=False, ssl=False,
+                 socket_host=None, socket_port=None):
+        self.wait = wait
+        self.daemonize = daemonize
+        self.ssl = ssl
+        self.host = socket_host or cherrypy.server.socket_host
+        self.port = socket_port or cherrypy.server.socket_port
+
+    def write_conf(self, extra=''):
+        if self.ssl:
+            serverpem = os.path.join(thisdir, 'test.pem')
+            ssl = """
+server.ssl_certificate: r'%s'
+server.ssl_private_key: r'%s'
+""" % (serverpem, serverpem)
+        else:
+            ssl = ''
+
+        conf = self.config_template % {
+            'host': self.host,
+            'port': self.port,
+            'error_log': self.error_log,
+            'access_log': self.access_log,
+            'ssl': ssl,
+            'extra': extra,
+        }
+        with io.open(self.config_file, 'w', encoding='utf-8') as f:
+            f.write(six.text_type(conf))
+
+    def start(self, imports=None):
+        """Start cherryd in a subprocess."""
+        portend.free(self.host, self.port, timeout=1)
+
+        args = [
+            '-m',
+            'cherrypy',
+            '-c', self.config_file,
+            '-p', self.pid_file,
+        ]
+        r"""
+        Command for running cherryd server with autoreload enabled
+
+        Using
+
+        ```
+        ['-c',
+         "__requires__ = 'CherryPy'; \
+         import pkg_resources, re, sys; \
+         sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \
+         sys.exit(\
+            pkg_resources.load_entry_point(\
+                'CherryPy', 'console_scripts', 'cherryd')())"]
+        ```
+
+        doesn't work as it's impossible to reconstruct the `-c`'s contents.
+        Ref: https://github.com/cherrypy/cherrypy/issues/1545
+        """
+
+        if not isinstance(imports, (list, tuple)):
+            imports = [imports]
+        for i in imports:
+            if i:
+                args.append('-i')
+                args.append(i)
+
+        if self.daemonize:
+            args.append('-d')
+
+        env = os.environ.copy()
+        # Make sure we import the cherrypy package in which this module is
+        # defined.
+        grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
+        if env.get('PYTHONPATH', ''):
+            env['PYTHONPATH'] = os.pathsep.join(
+                (grandparentdir, env['PYTHONPATH']))
+        else:
+            env['PYTHONPATH'] = grandparentdir
+        self._proc = subprocess.Popen([sys.executable] + args, env=env)
+        if self.wait:
+            self.exit_code = self._proc.wait()
+        else:
+            portend.occupied(self.host, self.port, timeout=5)
+
+        # Give the engine a wee bit more time to finish STARTING
+        if self.daemonize:
+            time.sleep(2)
+        else:
+            time.sleep(1)
+
+    def get_pid(self):
+        if self.daemonize:
+            return int(open(self.pid_file, 'rb').read())
+        return self._proc.pid
+
+    def join(self):
+        """Wait for the process to exit."""
+        if self.daemonize:
+            return self._join_daemon()
+        self._proc.wait()
+
+    def _join_daemon(self):
+        try:
+            try:
+                # Mac, UNIX
+                os.wait()
+            except AttributeError:
+                # Windows
+                try:
+                    pid = self.get_pid()
+                except IOError:
+                    # Assume the subprocess deleted the pidfile on shutdown.
+                    pass
+                else:
+                    os.waitpid(pid, 0)
+        except OSError:
+            x = sys.exc_info()[1]
+            if x.args != (10, 'No child processes'):
+                raise
diff --git a/libraries/cherrypy/test/logtest.py b/libraries/cherrypy/test/logtest.py
new file mode 100644
index 00000000..ed8f1540
--- /dev/null
+++ b/libraries/cherrypy/test/logtest.py
@@ -0,0 +1,228 @@
+"""logtest, a unittest.TestCase helper for testing log output."""
+
+import sys
+import time
+from uuid import UUID
+
+import six
+
+from cherrypy._cpcompat import text_or_bytes, ntob
+
+
+try:
+    # On Windows, msvcrt.getch reads a single char without output.
+    import msvcrt
+
+    def getchar():
+        return msvcrt.getch()
+except ImportError:
+    # Unix getchr
+    import tty
+    import termios
+
+    def getchar():
+        fd = sys.stdin.fileno()
+        old_settings = termios.tcgetattr(fd)
+        try:
+            tty.setraw(sys.stdin.fileno())
+            ch = sys.stdin.read(1)
+        finally:
+            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
+        return ch
+
+
+class LogCase(object):
+
+    """unittest.TestCase mixin for testing log messages.
+
+    logfile: a filename for the desired log. Yes, I know modes are evil,
+        but it makes the test functions so much cleaner to set this once.
+
+    lastmarker: the last marker in the log. This can be used to search for
+        messages since the last marker.
+
+    markerPrefix: a string with which to prefix log markers. This should be
+        unique enough from normal log output to use for marker identification.
+    """
+
+    logfile = None
+    lastmarker = None
+    markerPrefix = b'test suite marker: '
+
+    def _handleLogError(self, msg, data, marker, pattern):
+        print('')
+        print('    ERROR: %s' % msg)
+
+        if not self.interactive:
+            raise self.failureException(msg)
+
+        p = ('    Show: '
+             '[L]og [M]arker [P]attern; '
+             '[I]gnore, [R]aise, or sys.e[X]it >> ')
+        sys.stdout.write(p + ' ')
+        # ARGH
+        sys.stdout.flush()
+        while True:
+            i = getchar().upper()
+            if i not in 'MPLIRX':
+                continue
+            print(i.upper())  # Also prints new line
+            if i == 'L':
+                for x, line in enumerate(data):
+                    if (x + 1) % self.console_height == 0:
+                        # The \r and comma should make the next line overwrite
+                        sys.stdout.write('<-- More -->\r ')
+                        m = getchar().lower()
+                        # Erase our "More" prompt
+                        sys.stdout.write('            \r ')
+                        if m == 'q':
+                            break
+                    print(line.rstrip())
+            elif i == 'M':
+                print(repr(marker or self.lastmarker))
+            elif i == 'P':
+                print(repr(pattern))
+            elif i == 'I':
+                # return without raising the normal exception
+                return
+            elif i == 'R':
+                raise self.failureException(msg)
+            elif i == 'X':
+                self.exit()
+            sys.stdout.write(p + ' ')
+
+    def exit(self):
+        sys.exit()
+
+    def emptyLog(self):
+        """Overwrite self.logfile with 0 bytes."""
+        open(self.logfile, 'wb').write('')
+
+    def markLog(self, key=None):
+        """Insert a marker line into the log and set self.lastmarker."""
+        if key is None:
+            key = str(time.time())
+        self.lastmarker = key
+
+        open(self.logfile, 'ab+').write(
+            ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8'))
+
+    def _read_marked_region(self, marker=None):
+        """Return lines from self.logfile in the marked region.
+
+        If marker is None, self.lastmarker is used. If the log hasn't
+        been marked (using self.markLog), the entire log will be returned.
+        """
+# Give the logger time to finish writing?
+# time.sleep(0.5)
+
+        logfile = self.logfile
+        marker = marker or self.lastmarker
+        if marker is None:
+            return open(logfile, 'rb').readlines()
+
+        if isinstance(marker, six.text_type):
+            marker = marker.encode('utf-8')
+        data = []
+        in_region = False
+        for line in open(logfile, 'rb'):
+            if in_region:
+                if line.startswith(self.markerPrefix) and marker not in line:
+                    break
+                else:
+                    data.append(line)
+            elif marker in line:
+                in_region = True
+        return data
+
+    def assertInLog(self, line, marker=None):
+        """Fail if the given (partial) line is not in the log.
+
+        The log will be searched from the given marker to the next marker.
+        If marker is None, self.lastmarker is used. If the log hasn't
+        been marked (using self.markLog), the entire log will be searched.
+        """
+        data = self._read_marked_region(marker)
+        for logline in data:
+            if line in logline:
+                return
+        msg = '%r not found in log' % line
+        self._handleLogError(msg, data, marker, line)
+
+    def assertNotInLog(self, line, marker=None):
+        """Fail if the given (partial) line is in the log.
+
+        The log will be searched from the given marker to the next marker.
+        If marker is None, self.lastmarker is used. If the log hasn't
+        been marked (using self.markLog), the entire log will be searched.
+        """
+        data = self._read_marked_region(marker)
+        for logline in data:
+            if line in logline:
+                msg = '%r found in log' % line
+                self._handleLogError(msg, data, marker, line)
+
+    def assertValidUUIDv4(self, marker=None):
+        """Fail if the given UUIDv4 is not valid.
+
+        The log will be searched from the given marker to the next marker.
+        If marker is None, self.lastmarker is used. If the log hasn't
+        been marked (using self.markLog), the entire log will be searched.
+        """
+        data = self._read_marked_region(marker)
+        data = [
+            chunk.decode('utf-8').rstrip('\n').rstrip('\r')
+            for chunk in data
+        ]
+        for log_chunk in data:
+            try:
+                uuid_log = data[-1]
+                uuid_obj = UUID(uuid_log, version=4)
+            except (TypeError, ValueError):
+                pass  # it might be in other chunk
+            else:
+                if str(uuid_obj) == uuid_log:
+                    return
+                msg = '%r is not a valid UUIDv4' % uuid_log
+                self._handleLogError(msg, data, marker, log_chunk)
+
+        msg = 'UUIDv4 not found in log'
+        self._handleLogError(msg, data, marker, log_chunk)
+
+    def assertLog(self, sliceargs, lines, marker=None):
+        """Fail if log.readlines()[sliceargs] is not contained in 'lines'.
+
+        The log will be searched from the given marker to the next marker.
+        If marker is None, self.lastmarker is used. If the log hasn't
+        been marked (using self.markLog), the entire log will be searched.
+        """
+        data = self._read_marked_region(marker)
+        if isinstance(sliceargs, int):
+            # Single arg. Use __getitem__ and allow lines to be str or list.
+            if isinstance(lines, (tuple, list)):
+                lines = lines[0]
+            if isinstance(lines, six.text_type):
+                lines = lines.encode('utf-8')
+            if lines not in data[sliceargs]:
+                msg = '%r not found on log line %r' % (lines, sliceargs)
+                self._handleLogError(
+                    msg,
+                    [data[sliceargs], '--EXTRA CONTEXT--'] + data[
+                        sliceargs + 1:sliceargs + 6],
+                    marker,
+                    lines)
+        else:
+            # Multiple args. Use __getslice__ and require lines to be list.
+            if isinstance(lines, tuple):
+                lines = list(lines)
+            elif isinstance(lines, text_or_bytes):
+                raise TypeError("The 'lines' arg must be a list when "
+                                "'sliceargs' is a tuple.")
+
+            start, stop = sliceargs
+            for line, logline in zip(lines, data[start:stop]):
+                if isinstance(line, six.text_type):
+                    line = line.encode('utf-8')
+                if line not in logline:
+                    msg = '%r not found in log' % line
+                    self._handleLogError(msg, data[start:stop], marker, line)
diff --git a/libraries/cherrypy/test/modfastcgi.py b/libraries/cherrypy/test/modfastcgi.py
new file mode 100644
index 00000000..79ec3d18
--- /dev/null
+++ b/libraries/cherrypy/test/modfastcgi.py
@@ -0,0 +1,136 @@
+"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing.
+
+To autostart fastcgi, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl", "apache2ctl",
+or "httpd"--create a symlink to them if needed.
+
+You'll also need the WSGIServer from flup.servers.
+See http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+    output is then truncated again by Apache. See test_core.testRanges.
+    This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+    See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+    CherryPy may set "304 Not modified" but Apache will write out
+    "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+    Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+    mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+    no response entity body. This isn't really a bug; it just differs from
+    the CherryPy default.
+"""
+
+import os
+import re
+
+import cherrypy
+from cherrypy.process import servers
+from cherrypy.test import helper
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+def read_process(cmd, args=''):
+    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
+    try:
+        firstline = pipeout.readline()
+        if (re.search(r'(not recognized|No such file|not found)', firstline,
+                      re.IGNORECASE)):
+            raise IOError('%s must be on your system path.' % cmd)
+        output = firstline + pipeout.read()
+    finally:
+        pipeout.close()
+    return output
+
+
+APACHE_PATH = 'apache2ctl'
+CONF_PATH = 'fastcgi.conf'
+
+conf_fastcgi = """
+# Apache2 server conf file for testing CherryPy with mod_fastcgi.
+# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
+ServerRoot /usr/lib/apache2
+User #1000
+ErrorLog %(root)s/mod_fastcgi.error.log
+
+DocumentRoot "%(root)s"
+ServerName 127.0.0.1
+Listen %(port)s
+LoadModule fastcgi_module modules/mod_fastcgi.so
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options +ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
+"""
+
+
+def erase_script_name(environ, start_response):
+    environ['SCRIPT_NAME'] = ''
+    return cherrypy.tree(environ, start_response)
+
+
+class ModFCGISupervisor(helper.LocalWSGISupervisor):
+
+    httpserver_class = 'cherrypy.process.servers.FlupFCGIServer'
+    using_apache = True
+    using_wsgi = True
+    template = conf_fastcgi
+
+    def __str__(self):
+        return 'FCGI Server on %s:%s' % (self.host, self.port)
+
+    def start(self, modulename):
+        cherrypy.server.httpserver = servers.FlupFCGIServer(
+            application=erase_script_name, bindAddress=('127.0.0.1', 4000))
+        cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
+        cherrypy.server.socket_port = 4000
+        # For FCGI, we both start apache...
+        self.start_apache()
+        # ...and our local server
+        cherrypy.engine.start()
+        self.sync_apps()
+
+    def start_apache(self):
+        fcgiconf = CONF_PATH
+        if not os.path.isabs(fcgiconf):
+            fcgiconf = os.path.join(curdir, fcgiconf)
+
+        # Write the Apache conf file.
+        f = open(fcgiconf, 'wb')
+        try:
+            server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
+            output = self.template % {'port': self.port, 'root': curdir,
+                                      'server': server}
+            output = output.replace('\r\n', '\n')
+            f.write(output)
+        finally:
+            f.close()
+
+        result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
+        if result:
+            print(result)
+
+    def stop(self):
+        """Gracefully shutdown a server that is serving forever."""
+        read_process(APACHE_PATH, '-k stop')
+        helper.LocalWSGISupervisor.stop(self)
+
+    def sync_apps(self):
+        cherrypy.server.httpserver.fcgiserver.application = self.get_app(
+            erase_script_name)
diff --git a/libraries/cherrypy/test/modfcgid.py b/libraries/cherrypy/test/modfcgid.py
new file mode 100644
index 00000000..d101bd67
--- /dev/null
+++ b/libraries/cherrypy/test/modfcgid.py
@@ -0,0 +1,124 @@
+"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
+
+To autostart fcgid, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl", "apache2ctl",
+or "httpd"--create a symlink to them if needed.
+
+You'll also need the WSGIServer from flup.servers.
+See http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+    output is then truncated again by Apache. See test_core.testRanges.
+    This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+    See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+    CherryPy may set "304 Not modified" but Apache will write out
+    "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+    Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+    mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+    no response entity body. This isn't really a bug; it just differs from
+    the CherryPy default.
+"""
+
+import os
+import re
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy.process import servers
+from cherrypy.test import helper
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+def read_process(cmd, args=''):
+    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
+    try:
+        firstline = pipeout.readline()
+        if (re.search(r'(not recognized|No such file|not found)', firstline,
+                      re.IGNORECASE)):
+            raise IOError('%s must be on your system path.' % cmd)
+        output = firstline + pipeout.read()
+    finally:
+        pipeout.close()
+    return output
+
+
+APACHE_PATH = 'httpd'
+CONF_PATH = 'fcgi.conf'
+
+conf_fcgid = """
+# Apache2 server conf file for testing CherryPy with mod_fcgid.
+
+DocumentRoot "%(root)s"
+ServerName 127.0.0.1
+Listen %(port)s
+LoadModule fastcgi_module modules/mod_fastcgi.dll
+LoadModule rewrite_module modules/mod_rewrite.so
+
+Options ExecCGI
+SetHandler fastcgi-script
+RewriteEngine On
+RewriteRule ^(.*)$ /fastcgi.pyc [L]
+FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
+"""
+
+
+class ModFCGISupervisor(helper.LocalSupervisor):
+
+    using_apache = True
+    using_wsgi = True
+    template = conf_fcgid
+
+    def __str__(self):
+        return 'FCGI Server on %s:%s' % (self.host, self.port)
+
+    def start(self, modulename):
+        cherrypy.server.httpserver = servers.FlupFCGIServer(
+            application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
+        cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
+        # For FCGI, we both start apache...
+        self.start_apache()
+        # ...and our local server
+        helper.LocalServer.start(self, modulename)
+
+    def start_apache(self):
+        fcgiconf = CONF_PATH
+        if not os.path.isabs(fcgiconf):
+            fcgiconf = os.path.join(curdir, fcgiconf)
+
+        # Write the Apache conf file.
+        f = open(fcgiconf, 'wb')
+        try:
+            server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
+            output = self.template % {'port': self.port, 'root': curdir,
+                                      'server': server}
+            output = ntob(output.replace('\r\n', '\n'))
+            f.write(output)
+        finally:
+            f.close()
+
+        result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
+        if result:
+            print(result)
+
+    def stop(self):
+        """Gracefully shutdown a server that is serving forever."""
+        read_process(APACHE_PATH, '-k stop')
+        helper.LocalServer.stop(self)
+
+    def sync_apps(self):
+        cherrypy.server.httpserver.fcgiserver.application = self.get_app()
diff --git a/libraries/cherrypy/test/modpy.py b/libraries/cherrypy/test/modpy.py
new file mode 100644
index 00000000..7c288d2c
--- /dev/null
+++ b/libraries/cherrypy/test/modpy.py
@@ -0,0 +1,164 @@
+"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing.
+
+To autostart modpython, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
+create a symlink to them if needed.
+
+If you wish to test the WSGI interface instead of our _cpmodpy interface,
+you also need the 'modpython_gateway' module at:
+http://projects.amor.org/misc/wiki/ModPythonGateway
+
+
+KNOWN BUGS
+==========
+
+1. Apache processes Range headers automatically; CherryPy's truncated
+    output is then truncated again by Apache. See test_core.testRanges.
+    This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+    See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+4. Apache replaces status "reason phrases" automatically. For example,
+    CherryPy may set "304 Not modified" but Apache will write out
+    "304 Not Modified" (capital "M").
+5. Apache does not allow custom error codes as per the spec.
+6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+    Request-URI too early.
+7. mod_python will not read request bodies which use the "chunked"
+    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+    mod_python's requestobject.c).
+8. Apache will output a "Content-Length: 0" response header even if there's
+    no response entity body. This isn't really a bug; it just differs from
+    the CherryPy default.
+"""
+
+import os
+import re
+
+import cherrypy
+from cherrypy.test import helper
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+def read_process(cmd, args=''):
+    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
+    try:
+        firstline = pipeout.readline()
+        if (re.search(r'(not recognized|No such file|not found)', firstline,
+                      re.IGNORECASE)):
+            raise IOError('%s must be on your system path.' % cmd)
+        output = firstline + pipeout.read()
+    finally:
+        pipeout.close()
+    return output
+
+
+APACHE_PATH = 'httpd'
+CONF_PATH = 'test_mp.conf'
+
+conf_modpython_gateway = """
+# Apache2 server conf file for testing CherryPy with modpython_gateway.
+
+ServerName 127.0.0.1
+DocumentRoot "/"
+Listen %(port)s
+LoadModule python_module modules/mod_python.so
+
+SetHandler python-program
+PythonFixupHandler cherrypy.test.modpy::wsgisetup
+PythonOption testmod %(modulename)s
+PythonHandler modpython_gateway::handler
+PythonOption wsgi.application cherrypy::tree
+PythonOption socket_host %(host)s
+PythonDebug On
+"""
+
+conf_cpmodpy = """
+# Apache2 server conf file for testing CherryPy with _cpmodpy.
+
+ServerName 127.0.0.1
+DocumentRoot "/"
+Listen %(port)s
+LoadModule python_module modules/mod_python.so
+
+SetHandler python-program
+PythonFixupHandler cherrypy.test.modpy::cpmodpysetup
+PythonHandler cherrypy._cpmodpy::handler
+PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server
+PythonOption socket_host %(host)s
+PythonDebug On
+"""
+
+
+class ModPythonSupervisor(helper.Supervisor):
+
+    using_apache = True
+    using_wsgi = False
+    template = None
+
+    def __str__(self):
+        return 'ModPython Server on %s:%s' % (self.host, self.port)
+
+    def start(self, modulename):
+        mpconf = CONF_PATH
+        if not os.path.isabs(mpconf):
+            mpconf = os.path.join(curdir, mpconf)
+
+        f = open(mpconf, 'wb')
+        try:
+            f.write(self.template %
+                    {'port': self.port, 'modulename': modulename,
+                     'host': self.host})
+        finally:
+            f.close()
+
+        result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
+        if result:
+            print(result)
+
+    def stop(self):
+        """Gracefully shutdown a server that is serving forever."""
+        read_process(APACHE_PATH, '-k stop')
+
+
+loaded = False
+
+
+def wsgisetup(req):
+    global loaded
+    if not loaded:
+        loaded = True
+        options = req.get_options()
+
+        cherrypy.config.update({
+            'log.error_file': os.path.join(curdir, 'test.log'),
+            'environment': 'test_suite',
+            'server.socket_host': options['socket_host'],
+        })
+
+        modname = options['testmod']
+        mod = __import__(modname, globals(), locals(), [''])
+        mod.setup_server()
+
+        cherrypy.server.unsubscribe()
+        cherrypy.engine.start()
+    from mod_python import apache
+    return apache.OK
+
+
+def cpmodpysetup(req):
+    global loaded
+    if not loaded:
+        loaded = True
+        options = req.get_options()
+
+        cherrypy.config.update({
+            'log.error_file': os.path.join(curdir, 'test.log'),
+            'environment': 'test_suite',
+            'server.socket_host': options['socket_host'],
+        })
+    from mod_python import apache
+    return apache.OK
diff --git a/libraries/cherrypy/test/modwsgi.py b/libraries/cherrypy/test/modwsgi.py
new file mode 100644
index 00000000..f558e223
--- /dev/null
+++ b/libraries/cherrypy/test/modwsgi.py
@@ -0,0 +1,154 @@
+"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server.
+
+To autostart modwsgi, the "apache" executable or script must be
+on your system path, or you must override the global APACHE_PATH.
+On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
+create a symlink to them if needed.
+
+
+KNOWN BUGS
+==========
+
+##1. Apache processes Range headers automatically; CherryPy's truncated
+##    output is then truncated again by Apache. See test_core.testRanges.
+##    This was worked around in http://www.cherrypy.org/changeset/1319.
+2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
+    See test_core.testHTTPMethods.
+3. Max request header and body settings do not work with Apache.
+##4. Apache replaces status "reason phrases" automatically. For example,
+##    CherryPy may set "304 Not modified" but Apache will write out
+##    "304 Not Modified" (capital "M").
+##5. Apache does not allow custom error codes as per the spec.
+##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
+##    Request-URI too early.
+7. mod_wsgi will not read request bodies which use the "chunked"
+    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
+    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
+    mod_python's requestobject.c).
+8. When responding with 204 No Content, mod_wsgi adds a Content-Length
+    header for you.
+9. When an error is raised, mod_wsgi has no facility for printing a
+    traceback as the response content (it's sent to the Apache log instead).
+10. Startup and shutdown of Apache when running mod_wsgi seems slow.
+"""
+
+import os
+import re
+import sys
+import time
+
+import portend
+
+from cheroot.test import webtest
+
+import cherrypy
+from cherrypy.test import helper
+
+curdir = os.path.abspath(os.path.dirname(__file__))
+
+
+def read_process(cmd, args=''):
+    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
+    try:
+        firstline = pipeout.readline()
+        if (re.search(r'(not recognized|No such file|not found)', firstline,
+                      re.IGNORECASE)):
+            raise IOError('%s must be on your system path.' % cmd)
+        output = firstline + pipeout.read()
+    finally:
+        pipeout.close()
+    return output
+
+
+if sys.platform == 'win32':
+    APACHE_PATH = 'httpd'
+else:
+    APACHE_PATH = 'apache'
+
+CONF_PATH = 'test_mw.conf'
+
+conf_modwsgi = r"""
+# Apache2 server conf file for testing CherryPy with modpython_gateway.
+
+ServerName 127.0.0.1
+DocumentRoot "/"
+Listen %(port)s
+
+AllowEncodedSlashes On
+LoadModule rewrite_module modules/mod_rewrite.so
+RewriteEngine on
+RewriteMap escaping int:escape
+
+LoadModule log_config_module modules/mod_log_config.so
+LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
+CustomLog "%(curdir)s/apache.access.log" combined
+ErrorLog "%(curdir)s/apache.error.log"
+LogLevel debug
+
+LoadModule wsgi_module modules/mod_wsgi.so
+LoadModule env_module modules/mod_env.so
+
+WSGIScriptAlias / "%(curdir)s/modwsgi.py"
+SetEnv testmod %(testmod)s
+""" # noqa E501
+
+
+class ModWSGISupervisor(helper.Supervisor):
+
+    """Server Controller for ModWSGI and CherryPy."""
+
+    using_apache = True
+    using_wsgi = True
+    template = conf_modwsgi
+
+    def __str__(self):
+        return 'ModWSGI Server on %s:%s' % (self.host, self.port)
+
+    def start(self, modulename):
+        mpconf = CONF_PATH
+        if not os.path.isabs(mpconf):
+            mpconf = os.path.join(curdir, mpconf)
+
+        f = open(mpconf, 'wb')
+        try:
+            output = (self.template %
+                      {'port': self.port, 'testmod': modulename,
+                       'curdir': curdir})
+            f.write(output)
+        finally:
+            f.close()
+
+        result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
+        if result:
+            print(result)
+
+        # Make a request so mod_wsgi starts up our app.
+        # If we don't, concurrent initial requests will 404.
+        portend.occupied('127.0.0.1', self.port, timeout=5)
+        webtest.openURL('/ihopetheresnodefault', port=self.port)
+        time.sleep(1)
+
+    def stop(self):
+        """Gracefully shutdown a server that is serving forever."""
+        read_process(APACHE_PATH, '-k stop')
+
+
+loaded = False
+
+
+def application(environ, start_response):
+    global loaded
+    if not loaded:
+        loaded = True
+        modname = 'cherrypy.test.' + environ['testmod']
+        mod = __import__(modname, globals(), locals(), [''])
+        mod.setup_server()
+
+        cherrypy.config.update({
+            'log.error_file': os.path.join(curdir, 'test.error.log'),
+            'log.access_file': os.path.join(curdir, 'test.access.log'),
+            'environment': 'test_suite',
+            'engine.SIGHUP': None,
+            'engine.SIGTERM': None,
+        })
+    return cherrypy.tree(environ, start_response)
diff --git a/libraries/cherrypy/test/sessiondemo.py b/libraries/cherrypy/test/sessiondemo.py
new file mode 100644
index 00000000..8226c1b9
--- /dev/null
+++ b/libraries/cherrypy/test/sessiondemo.py
@@ -0,0 +1,161 @@
+#!/usr/bin/python
+"""A session demonstration app."""
+
+import calendar
+from datetime import datetime
+import sys
+
+import six
+
+import cherrypy
+from cherrypy.lib import sessions
+
+
+page = """
+<html>
+<head>
+<style type='text/css'>
+table { border-collapse: collapse; border: 1px solid #663333; }
+th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
+td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
+     border: 1px solid #663333; }
+.warn { font-family: serif; color: #990000; }
+</style>
+<script type="text/javascript">
+<!--
+function twodigit(d) { return d < 10 ? "0" + d : d; }
+function formattime(t) {
+    var month = t.getUTCMonth() + 1;
+    var day = t.getUTCDate();
+    var year = t.getUTCFullYear();
+    var hours = t.getUTCHours();
+    var minutes = t.getUTCMinutes();
+    return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " +
+            hours + ":" + twodigit(minutes) + " UTC");
+}
+
+function interval(s) {
+    // Return the given interval (in seconds) as an English phrase
+    var seconds = s %% 60;
+    s = Math.floor(s / 60);
+    var minutes = s %% 60;
+    s = Math.floor(s / 60);
+    var hours = s %% 24;
+    var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds);
+    var days = Math.floor(s / 24);
+    if (days != 0) v = days + ' days, ' + v;
+    return v;
+}
+
+var fudge_seconds = 5;
+
+function init() {
+    // Set the content of the 'btime' cell.
+    var currentTime = new Date();
+    var bunixtime = Math.floor(currentTime.getTime() / 1000);
+
+    var v = formattime(currentTime);
+    v += " (Unix time: " + bunixtime + ")";
+
+    var diff = Math.abs(%(serverunixtime)s - bunixtime);
+    if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
+
+    document.getElementById('btime').innerHTML = v;
+
+    // Warn if response cookie expires is not close to one hour in the future.
+    // Yes, we want this to happen when wit hit the 'Expire' link, too.
+    var expires = Date.parse("%(expires)s") / 1000;
+    var onehour = (60 * 60);
+    if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) {
+        diff = Math.floor(expires - bunixtime);
+        if (expires > (bunixtime + onehour)) {
+            var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future.";
+        } else {
+            var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past.";
+        }
+        document.getElementById('respcookiewarn').innerHTML = msg;
+    }
+}
+//-->
+</script>
+</head>
+
+<body onload='init()'>
+<h2>Session Demo</h2>
+<p>Reload this page. The session ID should not change from one reload to the next</p>
+<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p>
+<table>
+    <tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr>
+    <tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr>
+    <tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr>
+    <tr><th>Session Data</th><td>%(sessiondata)s</td></tr>
+    <tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr>
+    <tr><th>Browser Time</th><td id='btime'>&nbsp;</td></tr>
+    <tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr>
+    <tr><th>Python Version:</th><td>%(pyversion)s</td></tr>
+</table>
+</body></html>
+"""  # noqa E501
+
+
+class Root(object):
+
+    def page(self):
+        changemsg = []
+        if cherrypy.session.id != cherrypy.session.originalid:
+            if cherrypy.session.originalid is None:
+                changemsg.append(
+                    'Created new session because no session id was given.')
+            if cherrypy.session.missing:
+                changemsg.append(
+                    'Created new session due to missing '
+                    '(expired or malicious) session.')
+            if cherrypy.session.regenerated:
+                changemsg.append('Application generated a new session.')
+
+        try:
+            expires = cherrypy.response.cookie['session_id']['expires']
+        except KeyError:
+            expires = ''
+
+        return page % {
+            'sessionid': cherrypy.session.id,
+            'changemsg': '<br>'.join(changemsg),
+            'respcookie': cherrypy.response.cookie.output(),
+            'reqcookie': cherrypy.request.cookie.output(),
+            'sessiondata': list(six.iteritems(cherrypy.session)),
+            'servertime': (
+                datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
+            ),
+            'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
+            'cpversion': cherrypy.__version__,
+            'pyversion': sys.version,
+            'expires': expires,
+        }
+
+    @cherrypy.expose
+    def index(self):
+        # Must modify data or the session will not be saved.
+        cherrypy.session['color'] = 'green'
+        return self.page()
+
+    @cherrypy.expose
+    def expire(self):
+        sessions.expire()
+        return self.page()
+
+    @cherrypy.expose
+    def regen(self):
+        cherrypy.session.regenerate()
+        # Must modify data or the session will not be saved.
+        cherrypy.session['color'] = 'yellow'
+        return self.page()
+
+
+if __name__ == '__main__':
+    cherrypy.config.update({
+        # 'environment': 'production',
+        'log.screen': True,
+        'tools.sessions.on': True,
+    })
+    cherrypy.quickstart(Root())
diff --git a/libraries/cherrypy/test/static/404.html b/libraries/cherrypy/test/static/404.html
new file mode 100644
index 00000000..01b17b09
--- /dev/null
+++ b/libraries/cherrypy/test/static/404.html
@@ -0,0 +1,5 @@
+<html>
+ <body>
+  <h1>I couldn't find that thing you were looking for!</h1>
+ </body>
+</html>
diff --git a/libraries/cherrypy/test/static/dirback.jpg b/libraries/cherrypy/test/static/dirback.jpg
new file mode 100644
index 0000000000000000000000000000000000000000..80403dc227c19b9192158420f5288121e7f2669a
GIT binary patch
literal 16585
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
Sse&CiQ+RTS)_Y=q`u_k!0j`Vy

literal 0
HcmV?d00001

diff --git a/libraries/cherrypy/test/static/index.html b/libraries/cherrypy/test/static/index.html
new file mode 100644
index 00000000..a5c19667
--- /dev/null
+++ b/libraries/cherrypy/test/static/index.html
@@ -0,0 +1 @@
+Hello, world
diff --git a/libraries/cherrypy/test/style.css b/libraries/cherrypy/test/style.css
new file mode 100644
index 00000000..b266e93d
--- /dev/null
+++ b/libraries/cherrypy/test/style.css
@@ -0,0 +1 @@
+Dummy stylesheet
diff --git a/libraries/cherrypy/test/test.pem b/libraries/cherrypy/test/test.pem
new file mode 100644
index 00000000..47a47042
--- /dev/null
+++ b/libraries/cherrypy/test/test.pem
@@ -0,0 +1,38 @@
+-----BEGIN RSA PRIVATE KEY-----
+MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
+R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
+da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
+AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
+9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
+enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
+8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
+tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
+0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
+MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
+yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
+8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
+yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
+-----END RSA PRIVATE KEY-----
+-----BEGIN CERTIFICATE-----
+MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
+VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
+MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
+MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
+cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
+A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
+bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
+FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
+cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
+ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
+C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
+KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
+2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
+/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
+YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
+MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
+CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
+BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
+8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
+D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
+NluCaWQys3MS
+-----END CERTIFICATE-----
diff --git a/libraries/cherrypy/test/test_auth_basic.py b/libraries/cherrypy/test/test_auth_basic.py
new file mode 100644
index 00000000..d7e69a9b
--- /dev/null
+++ b/libraries/cherrypy/test/test_auth_basic.py
@@ -0,0 +1,135 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+from hashlib import md5
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy.lib import auth_basic
+from cherrypy.test import helper
+
+
+class BasicAuthTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'This is public.'
+
+        class BasicProtected:
+
+            @cherrypy.expose
+            def index(self):
+                return "Hello %s, you've been authorized." % (
+                    cherrypy.request.login)
+
+        class BasicProtected2:
+
+            @cherrypy.expose
+            def index(self):
+                return "Hello %s, you've been authorized." % (
+                    cherrypy.request.login)
+
+        class BasicProtected2_u:
+
+            @cherrypy.expose
+            def index(self):
+                return "Hello %s, you've been authorized." % (
+                    cherrypy.request.login)
+
+        userpassdict = {'xuser': 'xpassword'}
+        userhashdict = {'xuser': md5(b'xpassword').hexdigest()}
+        userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()}
+
+        def checkpasshash(realm, user, password):
+            p = userhashdict.get(user)
+            return p and p == md5(ntob(password)).hexdigest() or False
+
+        def checkpasshash_u(realm, user, password):
+            p = userhashdict_u.get(user)
+            return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False
+
+        basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict)
+        conf = {
+            '/basic': {
+                'tools.auth_basic.on': True,
+                'tools.auth_basic.realm': 'wonderland',
+                'tools.auth_basic.checkpassword': basic_checkpassword_dict
+            },
+            '/basic2': {
+                'tools.auth_basic.on': True,
+                'tools.auth_basic.realm': 'wonderland',
+                'tools.auth_basic.checkpassword': checkpasshash,
+                'tools.auth_basic.accept_charset': 'ISO-8859-1',
+            },
+            '/basic2_u': {
+                'tools.auth_basic.on': True,
+                'tools.auth_basic.realm': 'wonderland',
+                'tools.auth_basic.checkpassword': checkpasshash_u,
+                'tools.auth_basic.accept_charset': 'UTF-8',
+            },
+        }
+
+        root = Root()
+        root.basic = BasicProtected()
+        root.basic2 = BasicProtected2()
+        root.basic2_u = BasicProtected2_u()
+        cherrypy.tree.mount(root, config=conf)
+
+    def testPublic(self):
+        self.getPage('/')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+        self.assertBody('This is public.')
+
+    def testBasic(self):
+        self.getPage('/basic/')
+        self.assertStatus(401)
+        self.assertHeader(
+            'WWW-Authenticate',
+            'Basic realm="wonderland", charset="UTF-8"'
+        )
+
+        self.getPage('/basic/',
+                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
+        self.assertStatus(401)
+
+        self.getPage('/basic/',
+                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
+        self.assertStatus('200 OK')
+        self.assertBody("Hello xuser, you've been authorized.")
+
+    def testBasic2(self):
+        self.getPage('/basic2/')
+        self.assertStatus(401)
+        self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
+
+        self.getPage('/basic2/',
+                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
+        self.assertStatus(401)
+
+        self.getPage('/basic2/',
+                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
+        self.assertStatus('200 OK')
+        self.assertBody("Hello xuser, you've been authorized.")
+
+    def testBasic2_u(self):
+        self.getPage('/basic2_u/')
+        self.assertStatus(401)
+        self.assertHeader(
+            'WWW-Authenticate',
+            'Basic realm="wonderland", charset="UTF-8"'
+        )
+
+        self.getPage('/basic2_u/',
+                     [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')])
+        self.assertStatus(401)
+
+        self.getPage('/basic2_u/',
+                     [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')])
+        self.assertStatus('200 OK')
+        self.assertBody("Hello xюзер, you've been authorized.")
diff --git a/libraries/cherrypy/test/test_auth_digest.py b/libraries/cherrypy/test/test_auth_digest.py
new file mode 100644
index 00000000..512e39a5
--- /dev/null
+++ b/libraries/cherrypy/test/test_auth_digest.py
@@ -0,0 +1,134 @@
+# This file is part of CherryPy <http://www.cherrypy.org/>
+# -*- coding: utf-8 -*-
+# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
+
+import six
+
+
+import cherrypy
+from cherrypy.lib import auth_digest
+from cherrypy._cpcompat import ntob
+
+from cherrypy.test import helper
+
+
+def _fetch_users():
+    return {'test': 'test', '☃йюзер': 'їпароль'}
+
+
+get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users())
+
+
+class DigestAuthTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'This is public.'
+
+        class DigestProtected:
+
+            @cherrypy.expose
+            def index(self, *args, **kwargs):
+                return "Hello %s, you've been authorized." % (
+                    cherrypy.request.login)
+
+        conf = {'/digest': {'tools.auth_digest.on': True,
+                            'tools.auth_digest.realm': 'localhost',
+                            'tools.auth_digest.get_ha1': get_ha1,
+                            'tools.auth_digest.key': 'a565c27146791cfb',
+                            'tools.auth_digest.debug': True,
+                            'tools.auth_digest.accept_charset': 'UTF-8'}}
+
+        root = Root()
+        root.digest = DigestProtected()
+        cherrypy.tree.mount(root, config=conf)
+
+    def testPublic(self):
+        self.getPage('/')
+        assert self.status == '200 OK'
+        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+        assert self.body == b'This is public.'
+
+    def _test_parametric_digest(self, username, realm):
+        test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path'
+
+        self.getPage(test_uri)
+        assert self.status_code == 401
+
+        msg = 'Digest authentification scheme was not found'
+        www_auth_digest = tuple(filter(
+            lambda kv: kv[0].lower() == 'www-authenticate'
+            and kv[1].startswith('Digest '),
+            self.headers,
+        ))
+        assert len(www_auth_digest) == 1, msg
+
+        items = www_auth_digest[0][-1][7:].split(', ')
+        tokens = {}
+        for item in items:
+            key, value = item.split('=')
+            tokens[key.lower()] = value
+
+        assert tokens['realm'] == '"localhost"'
+        assert tokens['algorithm'] == '"MD5"'
+        assert tokens['qop'] == '"auth"'
+        assert tokens['charset'] == '"UTF-8"'
+
+        nonce = tokens['nonce'].strip('"')
+
+        # Test user agent response with a wrong value for 'realm'
+        base_auth = ('Digest username="%s", '
+                     'realm="%s", '
+                     'nonce="%s", '
+                     'uri="%s", '
+                     'algorithm=MD5, '
+                     'response="%s", '
+                     'qop=auth, '
+                     'nc=%s, '
+                     'cnonce="1522e61005789929"')
+
+        encoded_user = username
+        if six.PY3:
+            encoded_user = encoded_user.encode('utf-8')
+        encoded_user = encoded_user.decode('latin1')
+        auth_header = base_auth % (
+            encoded_user, realm, nonce, test_uri,
+            '11111111111111111111111111111111', '00000001',
+        )
+        auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
+        # calculate the response digest
+        ha1 = get_ha1(auth.realm, auth.username)
+        response = auth.request_digest(ha1)
+        auth_header = base_auth % (
+            encoded_user, realm, nonce, test_uri,
+            response, '00000001',
+        )
+        self.getPage(test_uri, [('Authorization', auth_header)])
+
+    def test_wrong_realm(self):
+        # send response with correct response digest, but wrong realm
+        self._test_parametric_digest(username='test', realm='wrong realm')
+        assert self.status_code == 401
+
+    def test_ascii_user(self):
+        self._test_parametric_digest(username='test', realm='localhost')
+        assert self.status == '200 OK'
+        assert self.body == b"Hello test, you've been authorized."
+
+    def test_unicode_user(self):
+        self._test_parametric_digest(username='☃йюзер', realm='localhost')
+        assert self.status == '200 OK'
+        assert self.body == ntob(
+            "Hello ☃йюзер, you've been authorized.", 'utf-8',
+        )
+
+    def test_wrong_scheme(self):
+        basic_auth = {
+            'Authorization': 'Basic foo:bar',
+        }
+        self.getPage('/digest/', headers=list(basic_auth.items()))
+        assert self.status_code == 401
diff --git a/libraries/cherrypy/test/test_bus.py b/libraries/cherrypy/test/test_bus.py
new file mode 100644
index 00000000..6026b47e
--- /dev/null
+++ b/libraries/cherrypy/test/test_bus.py
@@ -0,0 +1,274 @@
+import threading
+import time
+import unittest
+
+from cherrypy.process import wspbus
+
+
+msg = 'Listener %d on channel %s: %s.'
+
+
+class PublishSubscribeTests(unittest.TestCase):
+
+    def get_listener(self, channel, index):
+        def listener(arg=None):
+            self.responses.append(msg % (index, channel, arg))
+        return listener
+
+    def test_builtin_channels(self):
+        b = wspbus.Bus()
+
+        self.responses, expected = [], []
+
+        for channel in b.listeners:
+            for index, priority in enumerate([100, 50, 0, 51]):
+                b.subscribe(channel,
+                            self.get_listener(channel, index), priority)
+
+        for channel in b.listeners:
+            b.publish(channel)
+            expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
+            b.publish(channel, arg=79347)
+            expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
+
+        self.assertEqual(self.responses, expected)
+
+    def test_custom_channels(self):
+        b = wspbus.Bus()
+
+        self.responses, expected = [], []
+
+        custom_listeners = ('hugh', 'louis', 'dewey')
+        for channel in custom_listeners:
+            for index, priority in enumerate([None, 10, 60, 40]):
+                b.subscribe(channel,
+                            self.get_listener(channel, index), priority)
+
+        for channel in custom_listeners:
+            b.publish(channel, 'ah so')
+            expected.extend([msg % (i, channel, 'ah so')
+                            for i in (1, 3, 0, 2)])
+            b.publish(channel)
+            expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
+
+        self.assertEqual(self.responses, expected)
+
+    def test_listener_errors(self):
+        b = wspbus.Bus()
+
+        self.responses, expected = [], []
+        channels = [c for c in b.listeners if c != 'log']
+
+        for channel in channels:
+            b.subscribe(channel, self.get_listener(channel, 1))
+            # This will break since the lambda takes no args.
+            b.subscribe(channel, lambda: None, priority=20)
+
+        for channel in channels:
+            self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
+            expected.append(msg % (1, channel, 123))
+
+        self.assertEqual(self.responses, expected)
+
+
+class BusMethodTests(unittest.TestCase):
+
+    def log(self, bus):
+        self._log_entries = []
+
+        def logit(msg, level):
+            self._log_entries.append(msg)
+        bus.subscribe('log', logit)
+
+    def assertLog(self, entries):
+        self.assertEqual(self._log_entries, entries)
+
+    def get_listener(self, channel, index):
+        def listener(arg=None):
+            self.responses.append(msg % (index, channel, arg))
+        return listener
+
+    def test_start(self):
+        b = wspbus.Bus()
+        self.log(b)
+
+        self.responses = []
+        num = 3
+        for index in range(num):
+            b.subscribe('start', self.get_listener('start', index))
+
+        b.start()
+        try:
+            # The start method MUST call all 'start' listeners.
+            self.assertEqual(
+                set(self.responses),
+                set([msg % (i, 'start', None) for i in range(num)]))
+            # The start method MUST move the state to STARTED
+            # (or EXITING, if errors occur)
+            self.assertEqual(b.state, b.states.STARTED)
+            # The start method MUST log its states.
+            self.assertLog(['Bus STARTING', 'Bus STARTED'])
+        finally:
+            # Exit so the atexit handler doesn't complain.
+            b.exit()
+
+    def test_stop(self):
+        b = wspbus.Bus()
+        self.log(b)
+
+        self.responses = []
+        num = 3
+        for index in range(num):
+            b.subscribe('stop', self.get_listener('stop', index))
+
+        b.stop()
+
+        # The stop method MUST call all 'stop' listeners.
+        self.assertEqual(set(self.responses),
+                         set([msg % (i, 'stop', None) for i in range(num)]))
+        # The stop method MUST move the state to STOPPED
+        self.assertEqual(b.state, b.states.STOPPED)
+        # The stop method MUST log its states.
+        self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
+
+    def test_graceful(self):
+        b = wspbus.Bus()
+        self.log(b)
+
+        self.responses = []
+        num = 3
+        for index in range(num):
+            b.subscribe('graceful', self.get_listener('graceful', index))
+
+        b.graceful()
+
+        # The graceful method MUST call all 'graceful' listeners.
+        self.assertEqual(
+            set(self.responses),
+            set([msg % (i, 'graceful', None) for i in range(num)]))
+        # The graceful method MUST log its states.
+        self.assertLog(['Bus graceful'])
+
+    def test_exit(self):
+        b = wspbus.Bus()
+        self.log(b)
+
+        self.responses = []
+        num = 3
+        for index in range(num):
+            b.subscribe('stop', self.get_listener('stop', index))
+            b.subscribe('exit', self.get_listener('exit', index))
+
+        b.exit()
+
+        # The exit method MUST call all 'stop' listeners,
+        # and then all 'exit' listeners.
+        self.assertEqual(set(self.responses),
+                         set([msg % (i, 'stop', None) for i in range(num)] +
+                             [msg % (i, 'exit', None) for i in range(num)]))
+        # The exit method MUST move the state to EXITING
+        self.assertEqual(b.state, b.states.EXITING)
+        # The exit method MUST log its states.
+        self.assertLog(
+            ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
+
+    def test_wait(self):
+        b = wspbus.Bus()
+
+        def f(method):
+            time.sleep(0.2)
+            getattr(b, method)()
+
+        for method, states in [('start', [b.states.STARTED]),
+                               ('stop', [b.states.STOPPED]),
+                               ('start',
+                                [b.states.STARTING, b.states.STARTED]),
+                               ('exit', [b.states.EXITING]),
+                               ]:
+            threading.Thread(target=f, args=(method,)).start()
+            b.wait(states)
+
+            # The wait method MUST wait for the given state(s).
+            if b.state not in states:
+                self.fail('State %r not in %r' % (b.state, states))
+
+    def test_block(self):
+        b = wspbus.Bus()
+        self.log(b)
+
+        def f():
+            time.sleep(0.2)
+            b.exit()
+
+        def g():
+            time.sleep(0.4)
+        threading.Thread(target=f).start()
+        threading.Thread(target=g).start()
+        threads = [t for t in threading.enumerate() if not t.daemon]
+        self.assertEqual(len(threads), 3)
+
+        b.block()
+
+        # The block method MUST wait for the EXITING state.
+        self.assertEqual(b.state, b.states.EXITING)
+        # The block method MUST wait for ALL non-main, non-daemon threads to
+        # finish.
+        threads = [t for t in threading.enumerate() if not t.daemon]
+        self.assertEqual(len(threads), 1)
+        # The last message will mention an indeterminable thread name; ignore
+        # it
+        self.assertEqual(self._log_entries[:-1],
+                         ['Bus STOPPING', 'Bus STOPPED',
+                          'Bus EXITING', 'Bus EXITED',
+                          'Waiting for child threads to terminate...'])
+
+    def test_start_with_callback(self):
+        b = wspbus.Bus()
+        self.log(b)
+        try:
+            events = []
+
+            def f(*args, **kwargs):
+                events.append(('f', args, kwargs))
+
+            def g():
+                events.append('g')
+            b.subscribe('start', g)
+            b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
+            # Give wait() time to run f()
+            time.sleep(0.2)
+
+            # The callback method MUST wait for the STARTED state.
+            self.assertEqual(b.state, b.states.STARTED)
+            # The callback method MUST run after all start methods.
+            self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
+        finally:
+            b.exit()
+
+    def test_log(self):
+        b = wspbus.Bus()
+        self.log(b)
+        self.assertLog([])
+
+        # Try a normal message.
+        expected = []
+        for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
+            b.log(msg)
+            expected.append(msg)
+            self.assertLog(expected)
+
+        # Try an error message
+        try:
+            foo
+        except NameError:
+            b.log('You are lost and gone forever', traceback=True)
+            lastmsg = self._log_entries[-1]
+            if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
+                self.fail('Last log message %r did not contain '
+                          'the expected traceback.' % lastmsg)
+        else:
+            self.fail('NameError was not raised as expected.')
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/libraries/cherrypy/test/test_caching.py b/libraries/cherrypy/test/test_caching.py
new file mode 100644
index 00000000..1a6ed4f2
--- /dev/null
+++ b/libraries/cherrypy/test/test_caching.py
@@ -0,0 +1,392 @@
+import datetime
+from itertools import count
+import os
+import threading
+import time
+
+from six.moves import range
+from six.moves import urllib
+
+import pytest
+
+import cherrypy
+from cherrypy.lib import httputil
+
+from cherrypy.test import helper
+
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+gif_bytes = (
+    b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
+    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
+    b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
+)
+
+
+class CacheTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        @cherrypy.config(**{'tools.caching.on': True})
+        class Root:
+
+            def __init__(self):
+                self.counter = 0
+                self.control_counter = 0
+                self.longlock = threading.Lock()
+
+            @cherrypy.expose
+            def index(self):
+                self.counter += 1
+                msg = 'visit #%s' % self.counter
+                return msg
+
+            @cherrypy.expose
+            def control(self):
+                self.control_counter += 1
+                return 'visit #%s' % self.control_counter
+
+            @cherrypy.expose
+            def a_gif(self):
+                cherrypy.response.headers[
+                    'Last-Modified'] = httputil.HTTPDate()
+                return gif_bytes
+
+            @cherrypy.expose
+            def long_process(self, seconds='1'):
+                try:
+                    self.longlock.acquire()
+                    time.sleep(float(seconds))
+                finally:
+                    self.longlock.release()
+                return 'success!'
+
+            @cherrypy.expose
+            def clear_cache(self, path):
+                cherrypy._cache.store[cherrypy.request.base + path].clear()
+
+        @cherrypy.config(**{
+            'tools.caching.on': True,
+            'tools.response_headers.on': True,
+            'tools.response_headers.headers': [
+                ('Vary', 'Our-Varying-Header')
+            ],
+        })
+        class VaryHeaderCachingServer(object):
+
+            def __init__(self):
+                self.counter = count(1)
+
+            @cherrypy.expose
+            def index(self):
+                return 'visit #%s' % next(self.counter)
+
+        @cherrypy.config(**{
+            'tools.expires.on': True,
+            'tools.expires.secs': 60,
+            'tools.staticdir.on': True,
+            'tools.staticdir.dir': 'static',
+            'tools.staticdir.root': curdir,
+        })
+        class UnCached(object):
+
+            @cherrypy.expose
+            @cherrypy.config(**{'tools.expires.secs': 0})
+            def force(self):
+                cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+                self._cp_config['tools.expires.force'] = True
+                self._cp_config['tools.expires.secs'] = 0
+                return 'being forceful'
+
+            @cherrypy.expose
+            def dynamic(self):
+                cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+                cherrypy.response.headers['Cache-Control'] = 'private'
+                return 'D-d-d-dynamic!'
+
+            @cherrypy.expose
+            def cacheable(self):
+                cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
+                return "Hi, I'm cacheable."
+
+            @cherrypy.expose
+            @cherrypy.config(**{'tools.expires.secs': 86400})
+            def specific(self):
+                cherrypy.response.headers[
+                    'Etag'] = 'need_this_to_make_me_cacheable'
+                return 'I am being specific'
+
+            class Foo(object):
+                pass
+
+            @cherrypy.expose
+            @cherrypy.config(**{'tools.expires.secs': Foo()})
+            def wrongtype(self):
+                cherrypy.response.headers[
+                    'Etag'] = 'need_this_to_make_me_cacheable'
+                return 'Woops'
+
+        @cherrypy.config(**{
+            'tools.gzip.mime_types': ['text/*', 'image/*'],
+            'tools.caching.on': True,
+            'tools.staticdir.on': True,
+            'tools.staticdir.dir': 'static',
+            'tools.staticdir.root': curdir
+        })
+        class GzipStaticCache(object):
+            pass
+
+        cherrypy.tree.mount(Root())
+        cherrypy.tree.mount(UnCached(), '/expires')
+        cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers')
+        cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache')
+        cherrypy.config.update({'tools.gzip.on': True})
+
+    def testCaching(self):
+        elapsed = 0.0
+        for trial in range(10):
+            self.getPage('/')
+            # The response should be the same every time,
+            # except for the Age response header.
+            self.assertBody('visit #1')
+            if trial != 0:
+                age = int(self.assertHeader('Age'))
+                self.assert_(age >= elapsed)
+                elapsed = age
+
+        # POST, PUT, DELETE should not be cached.
+        self.getPage('/', method='POST')
+        self.assertBody('visit #2')
+        # Because gzip is turned on, the Vary header should always Vary for
+        # content-encoding
+        self.assertHeader('Vary', 'Accept-Encoding')
+        # The previous request should have invalidated the cache,
+        # so this request will recalc the response.
+        self.getPage('/', method='GET')
+        self.assertBody('visit #3')
+        # ...but this request should get the cached copy.
+        self.getPage('/', method='GET')
+        self.assertBody('visit #3')
+        self.getPage('/', method='DELETE')
+        self.assertBody('visit #4')
+
+        # The previous request should have invalidated the cache,
+        # so this request will recalc the response.
+        self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
+        self.assertHeader('Content-Encoding', 'gzip')
+        self.assertHeader('Vary')
+        self.assertEqual(
+            cherrypy.lib.encoding.decompress(self.body), b'visit #5')
+
+        # Now check that a second request gets the gzip header and gzipped body
+        # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
+        # response body was being gzipped a second time.
+        self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
+        self.assertHeader('Content-Encoding', 'gzip')
+        self.assertEqual(
+            cherrypy.lib.encoding.decompress(self.body), b'visit #5')
+
+        # Now check that a third request that doesn't accept gzip
+        # skips the cache (because the 'Vary' header denies it).
+        self.getPage('/', method='GET')
+        self.assertNoHeader('Content-Encoding')
+        self.assertBody('visit #6')
+
+    def testVaryHeader(self):
+        self.getPage('/varying_headers/')
+        self.assertStatus('200 OK')
+        self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
+        self.assertBody('visit #1')
+
+        # Now check that different 'Vary'-fields don't evict each other.
+        # This test creates 2 requests with different 'Our-Varying-Header'
+        # and then tests if the first one still exists.
+        self.getPage('/varying_headers/',
+                     headers=[('Our-Varying-Header', 'request 2')])
+        self.assertStatus('200 OK')
+        self.assertBody('visit #2')
+
+        self.getPage('/varying_headers/',
+                     headers=[('Our-Varying-Header', 'request 2')])
+        self.assertStatus('200 OK')
+        self.assertBody('visit #2')
+
+        self.getPage('/varying_headers/')
+        self.assertStatus('200 OK')
+        self.assertBody('visit #1')
+
+    def testExpiresTool(self):
+        # test setting an expires header
+        self.getPage('/expires/specific')
+        self.assertStatus('200 OK')
+        self.assertHeader('Expires')
+
+        # test exceptions for bad time values
+        self.getPage('/expires/wrongtype')
+        self.assertStatus(500)
+        self.assertInBody('TypeError')
+
+        # static content should not have "cache prevention" headers
+        self.getPage('/expires/index.html')
+        self.assertStatus('200 OK')
+        self.assertNoHeader('Pragma')
+        self.assertNoHeader('Cache-Control')
+        self.assertHeader('Expires')
+
+        # dynamic content that sets indicators should not have
+        # "cache prevention" headers
+        self.getPage('/expires/cacheable')
+        self.assertStatus('200 OK')
+        self.assertNoHeader('Pragma')
+        self.assertNoHeader('Cache-Control')
+        self.assertHeader('Expires')
+
+        self.getPage('/expires/dynamic')
+        self.assertBody('D-d-d-dynamic!')
+        # the Cache-Control header should be untouched
+        self.assertHeader('Cache-Control', 'private')
+        self.assertHeader('Expires')
+
+        # configure the tool to ignore indicators and replace existing headers
+        self.getPage('/expires/force')
+        self.assertStatus('200 OK')
+        # This also gives us a chance to test 0 expiry with no other headers
+        self.assertHeader('Pragma', 'no-cache')
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
+        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
+
+        # static content should now have "cache prevention" headers
+        self.getPage('/expires/index.html')
+        self.assertStatus('200 OK')
+        self.assertHeader('Pragma', 'no-cache')
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
+        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
+
+        # the cacheable handler should now have "cache prevention" headers
+        self.getPage('/expires/cacheable')
+        self.assertStatus('200 OK')
+        self.assertHeader('Pragma', 'no-cache')
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
+        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
+
+        self.getPage('/expires/dynamic')
+        self.assertBody('D-d-d-dynamic!')
+        # dynamic sets Cache-Control to private but it should  be
+        # overwritten here ...
+        self.assertHeader('Pragma', 'no-cache')
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
+        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
+
+    def _assert_resp_len_and_enc_for_gzip(self, uri):
+        """
+        Test that after querying gzipped content it's remains valid in
+        cache and available non-gzipped as well.
+        """
+        ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')]
+        content_len = None
+
+        for _ in range(3):
+            self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS)
+
+            if content_len is not None:
+                # all requests should get the same length
+                self.assertHeader('Content-Length', content_len)
+                self.assertHeader('Content-Encoding', 'gzip')
+
+            content_len = dict(self.headers)['Content-Length']
+
+        # check that we can still get non-gzipped version
+        self.getPage(uri, method='GET')
+        self.assertNoHeader('Content-Encoding')
+        # non-gzipped version should have a different content length
+        self.assertNoHeaderItemValue('Content-Length', content_len)
+
+    def testGzipStaticCache(self):
+        """Test that cache and gzip tools play well together when both enabled.
+
+        Ref GitHub issue #1190.
+        """
+        GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}'
+        resource_files = ('index.html', 'dirback.jpg')
+
+        for f in resource_files:
+            uri = GZIP_STATIC_CACHE_TMPL.format(f)
+            self._assert_resp_len_and_enc_for_gzip(uri)
+
+    def testLastModified(self):
+        self.getPage('/a.gif')
+        self.assertStatus(200)
+        self.assertBody(gif_bytes)
+        lm1 = self.assertHeader('Last-Modified')
+
+        # this request should get the cached copy.
+        self.getPage('/a.gif')
+        self.assertStatus(200)
+        self.assertBody(gif_bytes)
+        self.assertHeader('Age')
+        lm2 = self.assertHeader('Last-Modified')
+        self.assertEqual(lm1, lm2)
+
+        # this request should match the cached copy, but raise 304.
+        self.getPage('/a.gif', [('If-Modified-Since', lm1)])
+        self.assertStatus(304)
+        self.assertNoHeader('Last-Modified')
+        if not getattr(cherrypy.server, 'using_apache', False):
+            self.assertHeader('Age')
+
+    @pytest.mark.xfail(reason='#1536')
+    def test_antistampede(self):
+        SECONDS = 4
+        slow_url = '/long_process?seconds={SECONDS}'.format(**locals())
+        # We MUST make an initial synchronous request in order to create the
+        # AntiStampedeCache object, and populate its selecting_headers,
+        # before the actual stampede.
+        self.getPage(slow_url)
+        self.assertBody('success!')
+        path = urllib.parse.quote(slow_url, safe='')
+        self.getPage('/clear_cache?path=' + path)
+        self.assertStatus(200)
+
+        start = datetime.datetime.now()
+
+        def run():
+            self.getPage(slow_url)
+            # The response should be the same every time
+            self.assertBody('success!')
+        ts = [threading.Thread(target=run) for i in range(100)]
+        for t in ts:
+            t.start()
+        for t in ts:
+            t.join()
+        finish = datetime.datetime.now()
+        # Allow for overhead, two seconds for slow hosts
+        allowance = SECONDS + 2
+        self.assertEqualDates(start, finish, seconds=allowance)
+
+    def test_cache_control(self):
+        self.getPage('/control')
+        self.assertBody('visit #1')
+        self.getPage('/control')
+        self.assertBody('visit #1')
+
+        self.getPage('/control', headers=[('Cache-Control', 'no-cache')])
+        self.assertBody('visit #2')
+        self.getPage('/control')
+        self.assertBody('visit #2')
+
+        self.getPage('/control', headers=[('Pragma', 'no-cache')])
+        self.assertBody('visit #3')
+        self.getPage('/control')
+        self.assertBody('visit #3')
+
+        time.sleep(1)
+        self.getPage('/control', headers=[('Cache-Control', 'max-age=0')])
+        self.assertBody('visit #4')
+        self.getPage('/control')
+        self.assertBody('visit #4')
diff --git a/libraries/cherrypy/test/test_compat.py b/libraries/cherrypy/test/test_compat.py
new file mode 100644
index 00000000..44a9fa31
--- /dev/null
+++ b/libraries/cherrypy/test/test_compat.py
@@ -0,0 +1,34 @@
+"""Test Python 2/3 compatibility module."""
+from __future__ import unicode_literals
+
+import unittest
+
+import pytest
+import six
+
+from cherrypy import _cpcompat as compat
+
+
+class StringTester(unittest.TestCase):
+    """Tests for string conversion."""
+
+    @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2')
+    def test_ntob_non_native(self):
+        """ntob should raise an Exception on unicode.
+
+        (Python 2 only)
+
+        See #1132 for discussion.
+        """
+        self.assertRaises(TypeError, compat.ntob, 'fight')
+
+
+class EscapeTester(unittest.TestCase):
+    """Class to test escape_html function from _cpcompat."""
+
+    def test_escape_quote(self):
+        """test_escape_quote - Verify the output for &<>"' chars."""
+        self.assertEqual(
+            """xx&amp;&lt;&gt;"aa'""",
+            compat.escape_html("""xx&<>"aa'"""),
+        )
diff --git a/libraries/cherrypy/test/test_config.py b/libraries/cherrypy/test/test_config.py
new file mode 100644
index 00000000..be17df90
--- /dev/null
+++ b/libraries/cherrypy/test/test_config.py
@@ -0,0 +1,303 @@
+"""Tests for the CherryPy configuration system."""
+
+import io
+import os
+import sys
+import unittest
+
+import six
+
+import cherrypy
+
+from cherrypy.test import helper
+
+
+localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+def StringIOFromNative(x):
+    return io.StringIO(six.text_type(x))
+
+
+def setup_server():
+
+    @cherrypy.config(foo='this', bar='that')
+    class Root:
+
+        def __init__(self):
+            cherrypy.config.namespaces['db'] = self.db_namespace
+
+        def db_namespace(self, k, v):
+            if k == 'scheme':
+                self.db = v
+
+        @cherrypy.expose(alias=('global_', 'xyz'))
+        def index(self, key):
+            return cherrypy.request.config.get(key, 'None')
+
+        @cherrypy.expose
+        def repr(self, key):
+            return repr(cherrypy.request.config.get(key, None))
+
+        @cherrypy.expose
+        def dbscheme(self):
+            return self.db
+
+        @cherrypy.expose
+        @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']})
+        def plain(self, x):
+            return x
+
+        favicon_ico = cherrypy.tools.staticfile.handler(
+            filename=os.path.join(localDir, '../favicon.ico'))
+
+    @cherrypy.config(foo='this2', baz='that2')
+    class Foo:
+
+        @cherrypy.expose
+        def index(self, key):
+            return cherrypy.request.config.get(key, 'None')
+        nex = index
+
+        @cherrypy.expose
+        @cherrypy.config(**{'response.headers.X-silly': 'sillyval'})
+        def silly(self):
+            return 'Hello world'
+
+        # Test the expose and config decorators
+        @cherrypy.config(foo='this3', **{'bax': 'this4'})
+        @cherrypy.expose
+        def bar(self, key):
+            return repr(cherrypy.request.config.get(key, None))
+
+    class Another:
+
+        @cherrypy.expose
+        def index(self, key):
+            return str(cherrypy.request.config.get(key, 'None'))
+
+    def raw_namespace(key, value):
+        if key == 'input.map':
+            handler = cherrypy.request.handler
+
+            def wrapper():
+                params = cherrypy.request.params
+                for name, coercer in list(value.items()):
+                    try:
+                        params[name] = coercer(params[name])
+                    except KeyError:
+                        pass
+                return handler()
+            cherrypy.request.handler = wrapper
+        elif key == 'output':
+            handler = cherrypy.request.handler
+
+            def wrapper():
+                # 'value' is a type (like int or str).
+                return value(handler())
+            cherrypy.request.handler = wrapper
+
+    @cherrypy.config(**{'raw.output': repr})
+    class Raw:
+
+        @cherrypy.expose
+        @cherrypy.config(**{'raw.input.map': {'num': int}})
+        def incr(self, num):
+            return num + 1
+
+    if not six.PY3:
+        thing3 = "thing3: unicode('test', errors='ignore')"
+    else:
+        thing3 = ''
+
+    ioconf = StringIOFromNative("""
+[/]
+neg: -1234
+filename: os.path.join(sys.prefix, "hello.py")
+thing1: cherrypy.lib.httputil.response_codes[404]
+thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
+%s
+complex: 3+2j
+mul: 6*3
+ones: "11"
+twos: "22"
+stradd: %%(ones)s + %%(twos)s + "33"
+
+[/favicon.ico]
+tools.staticfile.filename = %r
+""" % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
+
+    root = Root()
+    root.foo = Foo()
+    root.raw = Raw()
+    app = cherrypy.tree.mount(root, config=ioconf)
+    app.request_class.namespaces['raw'] = raw_namespace
+
+    cherrypy.tree.mount(Another(), '/another')
+    cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove',
+                            'db.scheme': r'sqlite///memory',
+                            })
+
+
+#                             Client-side code                             #
+
+
+class ConfigTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def testConfig(self):
+        tests = [
+            ('/', 'nex', 'None'),
+            ('/', 'foo', 'this'),
+            ('/', 'bar', 'that'),
+            ('/xyz', 'foo', 'this'),
+            ('/foo/', 'foo', 'this2'),
+            ('/foo/', 'bar', 'that'),
+            ('/foo/', 'bax', 'None'),
+            ('/foo/bar', 'baz', "'that2'"),
+            ('/foo/nex', 'baz', 'that2'),
+            # If 'foo' == 'this', then the mount point '/another' leaks into
+            # '/'.
+            ('/another/', 'foo', 'None'),
+        ]
+        for path, key, expected in tests:
+            self.getPage(path + '?key=' + key)
+            self.assertBody(expected)
+
+        expectedconf = {
+            # From CP defaults
+            'tools.log_headers.on': False,
+            'tools.log_tracebacks.on': True,
+            'request.show_tracebacks': True,
+            'log.screen': False,
+            'environment': 'test_suite',
+            'engine.autoreload.on': False,
+            # From global config
+            'luxuryyacht': 'throatwobblermangrove',
+            # From Root._cp_config
+            'bar': 'that',
+            # From Foo._cp_config
+            'baz': 'that2',
+            # From Foo.bar._cp_config
+            'foo': 'this3',
+            'bax': 'this4',
+        }
+        for key, expected in expectedconf.items():
+            self.getPage('/foo/bar?key=' + key)
+            self.assertBody(repr(expected))
+
+    def testUnrepr(self):
+        self.getPage('/repr?key=neg')
+        self.assertBody('-1234')
+
+        self.getPage('/repr?key=filename')
+        self.assertBody(repr(os.path.join(sys.prefix, 'hello.py')))
+
+        self.getPage('/repr?key=thing1')
+        self.assertBody(repr(cherrypy.lib.httputil.response_codes[404]))
+
+        if not getattr(cherrypy.server, 'using_apache', False):
+            # The object ID's won't match up when using Apache, since the
+            # server and client are running in different processes.
+            self.getPage('/repr?key=thing2')
+            from cherrypy.tutorial import thing2
+            self.assertBody(repr(thing2))
+
+        if not six.PY3:
+            self.getPage('/repr?key=thing3')
+            self.assertBody(repr(six.text_type('test')))
+
+        self.getPage('/repr?key=complex')
+        self.assertBody('(3+2j)')
+
+        self.getPage('/repr?key=mul')
+        self.assertBody('18')
+
+        self.getPage('/repr?key=stradd')
+        self.assertBody(repr('112233'))
+
+    def testRespNamespaces(self):
+        self.getPage('/foo/silly')
+        self.assertHeader('X-silly', 'sillyval')
+        self.assertBody('Hello world')
+
+    def testCustomNamespaces(self):
+        self.getPage('/raw/incr?num=12')
+        self.assertBody('13')
+
+        self.getPage('/dbscheme')
+        self.assertBody(r'sqlite///memory')
+
+    def testHandlerToolConfigOverride(self):
+        # Assert that config overrides tool constructor args. Above, we set
+        # the favicon in the page handler to be '../favicon.ico',
+        # but then overrode it in config to be './static/dirback.jpg'.
+        self.getPage('/favicon.ico')
+        self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'),
+                             'rb').read())
+
+    def test_request_body_namespace(self):
+        self.getPage('/plain', method='POST', headers=[
+            ('Content-Type', 'application/x-www-form-urlencoded'),
+            ('Content-Length', '13')],
+            body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')
+        self.assertBody('abc')
+
+
+class VariableSubstitutionTests(unittest.TestCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_config(self):
+        from textwrap import dedent
+
+        # variable substitution with [DEFAULT]
+        conf = dedent("""
+        [DEFAULT]
+        dir = "/some/dir"
+        my.dir = %(dir)s + "/sub"
+
+        [my]
+        my.dir = %(dir)s + "/my/dir"
+        my.dir2 = %(my.dir)s + '/dir2'
+
+        """)
+
+        fp = StringIOFromNative(conf)
+
+        cherrypy.config.update(fp)
+        self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir')
+        self.assertEqual(cherrypy.config['my']
+                         ['my.dir2'], '/some/dir/my/dir/dir2')
+
+
+class CallablesInConfigTest(unittest.TestCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_call_with_literal_dict(self):
+        from textwrap import dedent
+        conf = dedent("""
+        [my]
+        value = dict(**{'foo': 'bar'})
+        """)
+        fp = StringIOFromNative(conf)
+        cherrypy.config.update(fp)
+        self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'})
+
+    def test_call_with_kwargs(self):
+        from textwrap import dedent
+        conf = dedent("""
+        [my]
+        value = dict(foo="buzz", **cherrypy._test_dict)
+        """)
+        test_dict = {
+            'foo': 'bar',
+            'bar': 'foo',
+            'fizz': 'buzz'
+        }
+        cherrypy._test_dict = test_dict
+        fp = StringIOFromNative(conf)
+        cherrypy.config.update(fp)
+        test_dict['foo'] = 'buzz'
+        self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz')
+        self.assertEqual(cherrypy.config['my']['value'], test_dict)
+        del cherrypy._test_dict
diff --git a/libraries/cherrypy/test/test_config_server.py b/libraries/cherrypy/test/test_config_server.py
new file mode 100644
index 00000000..7b183530
--- /dev/null
+++ b/libraries/cherrypy/test/test_config_server.py
@@ -0,0 +1,126 @@
+"""Tests for the CherryPy configuration system."""
+
+import os
+
+import cherrypy
+from cherrypy.test import helper
+
+
+localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+#                             Client-side code                             #
+
+
+class ServerConfigTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return cherrypy.request.wsgi_environ['SERVER_PORT']
+
+            @cherrypy.expose
+            def upload(self, file):
+                return 'Size: %s' % len(file.file.read())
+
+            @cherrypy.expose
+            @cherrypy.config(**{'request.body.maxbytes': 100})
+            def tinyupload(self):
+                return cherrypy.request.body.read()
+
+        cherrypy.tree.mount(Root())
+
+        cherrypy.config.update({
+            'server.socket_host': '0.0.0.0',
+            'server.socket_port': 9876,
+            'server.max_request_body_size': 200,
+            'server.max_request_header_size': 500,
+            'server.socket_timeout': 0.5,
+
+            # Test explicit server.instance
+            'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
+            'server.2.socket_port': 9877,
+
+            # Test non-numeric <servername>
+            # Also test default server.instance = builtin server
+            'server.yetanother.socket_port': 9878,
+        })
+
+    PORT = 9876
+
+    def testBasicConfig(self):
+        self.getPage('/')
+        self.assertBody(str(self.PORT))
+
+    def testAdditionalServers(self):
+        if self.scheme == 'https':
+            return self.skip('not available under ssl')
+        self.PORT = 9877
+        self.getPage('/')
+        self.assertBody(str(self.PORT))
+        self.PORT = 9878
+        self.getPage('/')
+        self.assertBody(str(self.PORT))
+
+    def testMaxRequestSizePerHandler(self):
+        if getattr(cherrypy.server, 'using_apache', False):
+            return self.skip('skipped due to known Apache differences... ')
+
+        self.getPage('/tinyupload', method='POST',
+                     headers=[('Content-Type', 'text/plain'),
+                              ('Content-Length', '100')],
+                     body='x' * 100)
+        self.assertStatus(200)
+        self.assertBody('x' * 100)
+
+        self.getPage('/tinyupload', method='POST',
+                     headers=[('Content-Type', 'text/plain'),
+                              ('Content-Length', '101')],
+                     body='x' * 101)
+        self.assertStatus(413)
+
+    def testMaxRequestSize(self):
+        if getattr(cherrypy.server, 'using_apache', False):
+            return self.skip('skipped due to known Apache differences... ')
+
+        for size in (500, 5000, 50000):
+            self.getPage('/', headers=[('From', 'x' * 500)])
+            self.assertStatus(413)
+
+        # Test for https://github.com/cherrypy/cherrypy/issues/421
+        # (Incorrect border condition in readline of SizeCheckWrapper).
+        # This hangs in rev 891 and earlier.
+        lines256 = 'x' * 248
+        self.getPage('/',
+                     headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
+                              ('From', lines256)])
+
+        # Test upload
+        cd = (
+            'Content-Disposition: form-data; '
+            'name="file"; '
+            'filename="hello.txt"'
+        )
+        body = '\r\n'.join([
+            '--x',
+            cd,
+            'Content-Type: text/plain',
+            '',
+            '%s',
+            '--x--'])
+        partlen = 200 - len(body)
+        b = body % ('x' * partlen)
+        h = [('Content-type', 'multipart/form-data; boundary=x'),
+             ('Content-Length', '%s' % len(b))]
+        self.getPage('/upload', h, 'POST', b)
+        self.assertBody('Size: %d' % partlen)
+
+        b = body % ('x' * 200)
+        h = [('Content-type', 'multipart/form-data; boundary=x'),
+             ('Content-Length', '%s' % len(b))]
+        self.getPage('/upload', h, 'POST', b)
+        self.assertStatus(413)
diff --git a/libraries/cherrypy/test/test_conn.py b/libraries/cherrypy/test/test_conn.py
new file mode 100644
index 00000000..7d60c6fb
--- /dev/null
+++ b/libraries/cherrypy/test/test_conn.py
@@ -0,0 +1,873 @@
+"""Tests for TCP connection handling, including proper and timely close."""
+
+import errno
+import socket
+import sys
+import time
+
+import six
+from six.moves import urllib
+from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected
+
+import pytest
+
+from cheroot.test import webtest
+
+import cherrypy
+from cherrypy._cpcompat import HTTPSConnection, ntob, tonative
+from cherrypy.test import helper
+
+
+timeout = 1
+pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
+
+
+def setup_server():
+
+    def raise500():
+        raise cherrypy.HTTPError(500)
+
+    class Root:
+
+        @cherrypy.expose
+        def index(self):
+            return pov
+        page1 = index
+        page2 = index
+        page3 = index
+
+        @cherrypy.expose
+        def hello(self):
+            return 'Hello, world!'
+
+        @cherrypy.expose
+        def timeout(self, t):
+            return str(cherrypy.server.httpserver.timeout)
+
+        @cherrypy.expose
+        @cherrypy.config(**{'response.stream': True})
+        def stream(self, set_cl=False):
+            if set_cl:
+                cherrypy.response.headers['Content-Length'] = 10
+
+            def content():
+                for x in range(10):
+                    yield str(x)
+
+            return content()
+
+        @cherrypy.expose
+        def error(self, code=500):
+            raise cherrypy.HTTPError(code)
+
+        @cherrypy.expose
+        def upload(self):
+            if not cherrypy.request.method == 'POST':
+                raise AssertionError("'POST' != request.method %r" %
+                                     cherrypy.request.method)
+            return "thanks for '%s'" % cherrypy.request.body.read()
+
+        @cherrypy.expose
+        def custom(self, response_code):
+            cherrypy.response.status = response_code
+            return 'Code = %s' % response_code
+
+        @cherrypy.expose
+        @cherrypy.config(**{'hooks.on_start_resource': raise500})
+        def err_before_read(self):
+            return 'ok'
+
+        @cherrypy.expose
+        def one_megabyte_of_a(self):
+            return ['a' * 1024] * 1024
+
+        @cherrypy.expose
+        # Turn off the encoding tool so it doens't collapse
+        # our response body and reclaculate the Content-Length.
+        @cherrypy.config(**{'tools.encode.on': False})
+        def custom_cl(self, body, cl):
+            cherrypy.response.headers['Content-Length'] = cl
+            if not isinstance(body, list):
+                body = [body]
+            newbody = []
+            for chunk in body:
+                if isinstance(chunk, six.text_type):
+                    chunk = chunk.encode('ISO-8859-1')
+                newbody.append(chunk)
+            return newbody
+
+    cherrypy.tree.mount(Root())
+    cherrypy.config.update({
+        'server.max_request_body_size': 1001,
+        'server.socket_timeout': timeout,
+    })
+
+
+class ConnectionCloseTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_HTTP11(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        self.persistent = True
+
+        # Make the first request and assert there's no "Connection: close".
+        self.getPage('/')
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        self.assertNoHeader('Connection')
+
+        # Make another request on the same connection.
+        self.getPage('/page1')
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        self.assertNoHeader('Connection')
+
+        # Test client-side close.
+        self.getPage('/page2', headers=[('Connection', 'close')])
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        self.assertHeader('Connection', 'close')
+
+        # Make another request on the same connection, which should error.
+        self.assertRaises(NotConnected, self.getPage, '/')
+
+    def test_Streaming_no_len(self):
+        try:
+            self._streaming(set_cl=False)
+        finally:
+            try:
+                self.HTTP_CONN.close()
+            except (TypeError, AttributeError):
+                pass
+
+    def test_Streaming_with_len(self):
+        try:
+            self._streaming(set_cl=True)
+        finally:
+            try:
+                self.HTTP_CONN.close()
+            except (TypeError, AttributeError):
+                pass
+
+    def _streaming(self, set_cl):
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.PROTOCOL = 'HTTP/1.1'
+
+            self.persistent = True
+
+            # Make the first request and assert there's no "Connection: close".
+            self.getPage('/')
+            self.assertStatus('200 OK')
+            self.assertBody(pov)
+            self.assertNoHeader('Connection')
+
+            # Make another, streamed request on the same connection.
+            if set_cl:
+                # When a Content-Length is provided, the content should stream
+                # without closing the connection.
+                self.getPage('/stream?set_cl=Yes')
+                self.assertHeader('Content-Length')
+                self.assertNoHeader('Connection', 'close')
+                self.assertNoHeader('Transfer-Encoding')
+
+                self.assertStatus('200 OK')
+                self.assertBody('0123456789')
+            else:
+                # When no Content-Length response header is provided,
+                # streamed output will either close the connection, or use
+                # chunked encoding, to determine transfer-length.
+                self.getPage('/stream')
+                self.assertNoHeader('Content-Length')
+                self.assertStatus('200 OK')
+                self.assertBody('0123456789')
+
+                chunked_response = False
+                for k, v in self.headers:
+                    if k.lower() == 'transfer-encoding':
+                        if str(v) == 'chunked':
+                            chunked_response = True
+
+                if chunked_response:
+                    self.assertNoHeader('Connection', 'close')
+                else:
+                    self.assertHeader('Connection', 'close')
+
+                    # Make another request on the same connection, which should
+                    # error.
+                    self.assertRaises(NotConnected, self.getPage, '/')
+
+                # Try HEAD. See
+                # https://github.com/cherrypy/cherrypy/issues/864.
+                self.getPage('/stream', method='HEAD')
+                self.assertStatus('200 OK')
+                self.assertBody('')
+                self.assertNoHeader('Transfer-Encoding')
+        else:
+            self.PROTOCOL = 'HTTP/1.0'
+
+            self.persistent = True
+
+            # Make the first request and assert Keep-Alive.
+            self.getPage('/', headers=[('Connection', 'Keep-Alive')])
+            self.assertStatus('200 OK')
+            self.assertBody(pov)
+            self.assertHeader('Connection', 'Keep-Alive')
+
+            # Make another, streamed request on the same connection.
+            if set_cl:
+                # When a Content-Length is provided, the content should
+                # stream without closing the connection.
+                self.getPage('/stream?set_cl=Yes',
+                             headers=[('Connection', 'Keep-Alive')])
+                self.assertHeader('Content-Length')
+                self.assertHeader('Connection', 'Keep-Alive')
+                self.assertNoHeader('Transfer-Encoding')
+                self.assertStatus('200 OK')
+                self.assertBody('0123456789')
+            else:
+                # When a Content-Length is not provided,
+                # the server should close the connection.
+                self.getPage('/stream', headers=[('Connection', 'Keep-Alive')])
+                self.assertStatus('200 OK')
+                self.assertBody('0123456789')
+
+                self.assertNoHeader('Content-Length')
+                self.assertNoHeader('Connection', 'Keep-Alive')
+                self.assertNoHeader('Transfer-Encoding')
+
+                # Make another request on the same connection, which should
+                # error.
+                self.assertRaises(NotConnected, self.getPage, '/')
+
+    def test_HTTP10_KeepAlive(self):
+        self.PROTOCOL = 'HTTP/1.0'
+        if self.scheme == 'https':
+            self.HTTP_CONN = HTTPSConnection
+        else:
+            self.HTTP_CONN = HTTPConnection
+
+        # Test a normal HTTP/1.0 request.
+        self.getPage('/page2')
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        # Apache, for example, may emit a Connection header even for HTTP/1.0
+        # self.assertNoHeader("Connection")
+
+        # Test a keep-alive HTTP/1.0 request.
+        self.persistent = True
+
+        self.getPage('/page3', headers=[('Connection', 'Keep-Alive')])
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        self.assertHeader('Connection', 'Keep-Alive')
+
+        # Remove the keep-alive header again.
+        self.getPage('/page3')
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        # Apache, for example, may emit a Connection header even for HTTP/1.0
+        # self.assertNoHeader("Connection")
+
+
+class PipelineTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_HTTP11_Timeout(self):
+        # If we timeout without sending any data,
+        # the server will close the conn with a 408.
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Connect but send nothing.
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.auto_open = False
+        conn.connect()
+
+        # Wait for our socket timeout
+        time.sleep(timeout * 2)
+
+        # The request should have returned 408 already.
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 408)
+        conn.close()
+
+        # Connect but send half the headers only.
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.auto_open = False
+        conn.connect()
+        conn.send(b'GET /hello HTTP/1.1')
+        conn.send(('Host: %s' % self.HOST).encode('ascii'))
+
+        # Wait for our socket timeout
+        time.sleep(timeout * 2)
+
+        # The conn should have already sent 408.
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 408)
+        conn.close()
+
+    def test_HTTP11_Timeout_after_request(self):
+        # If we timeout after at least one request has succeeded,
+        # the server will close the conn without 408.
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Make an initial request
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 200)
+        self.body = response.read()
+        self.assertBody(str(timeout))
+
+        # Make a second request on the same socket
+        conn._output(b'GET /hello HTTP/1.1')
+        conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
+        conn._send_output()
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 200)
+        self.body = response.read()
+        self.assertBody('Hello, world!')
+
+        # Wait for our socket timeout
+        time.sleep(timeout * 2)
+
+        # Make another request on the same socket, which should error
+        conn._output(b'GET /hello HTTP/1.1')
+        conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
+        conn._send_output()
+        response = conn.response_class(conn.sock, method='GET')
+        try:
+            response.begin()
+        except Exception:
+            if not isinstance(sys.exc_info()[1],
+                              (socket.error, BadStatusLine)):
+                self.fail("Writing to timed out socket didn't fail"
+                          ' as it should have: %s' % sys.exc_info()[1])
+        else:
+            if response.status != 408:
+                self.fail("Writing to timed out socket didn't fail"
+                          ' as it should have: %s' %
+                          response.read())
+
+        conn.close()
+
+        # Make another request on a new socket, which should work
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('GET', '/', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 200)
+        self.body = response.read()
+        self.assertBody(pov)
+
+        # Make another request on the same socket,
+        # but timeout on the headers
+        conn.send(b'GET /hello HTTP/1.1')
+        # Wait for our socket timeout
+        time.sleep(timeout * 2)
+        response = conn.response_class(conn.sock, method='GET')
+        try:
+            response.begin()
+        except Exception:
+            if not isinstance(sys.exc_info()[1],
+                              (socket.error, BadStatusLine)):
+                self.fail("Writing to timed out socket didn't fail"
+                          ' as it should have: %s' % sys.exc_info()[1])
+        else:
+            self.fail("Writing to timed out socket didn't fail"
+                      ' as it should have: %s' %
+                      response.read())
+
+        conn.close()
+
+        # Retry the request on a new connection, which should work
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('GET', '/', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 200)
+        self.body = response.read()
+        self.assertBody(pov)
+        conn.close()
+
+    def test_HTTP11_pipelining(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Test pipelining. httplib doesn't support this directly.
+        self.persistent = True
+        conn = self.HTTP_CONN
+
+        # Put request 1
+        conn.putrequest('GET', '/hello', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+
+        for trial in range(5):
+            # Put next request
+            conn._output(b'GET /hello HTTP/1.1')
+            conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
+            conn._send_output()
+
+            # Retrieve previous response
+            response = conn.response_class(conn.sock, method='GET')
+            # there is a bug in python3 regarding the buffering of
+            # ``conn.sock``. Until that bug get's fixed we will
+            # monkey patch the ``response`` instance.
+            # https://bugs.python.org/issue23377
+            if six.PY3:
+                response.fp = conn.sock.makefile('rb', 0)
+            response.begin()
+            body = response.read(13)
+            self.assertEqual(response.status, 200)
+            self.assertEqual(body, b'Hello, world!')
+
+        # Retrieve final response
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        body = response.read()
+        self.assertEqual(response.status, 200)
+        self.assertEqual(body, b'Hello, world!')
+
+        conn.close()
+
+    def test_100_Continue(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        self.persistent = True
+        conn = self.HTTP_CONN
+
+        # Try a page without an Expect request header first.
+        # Note that httplib's response.begin automatically ignores
+        # 100 Continue responses, so we must manually check for it.
+        try:
+            conn.putrequest('POST', '/upload', skip_host=True)
+            conn.putheader('Host', self.HOST)
+            conn.putheader('Content-Type', 'text/plain')
+            conn.putheader('Content-Length', '4')
+            conn.endheaders()
+            conn.send(ntob("d'oh"))
+            response = conn.response_class(conn.sock, method='POST')
+            version, status, reason = response._read_status()
+            self.assertNotEqual(status, 100)
+        finally:
+            conn.close()
+
+        # Now try a page with an Expect header...
+        try:
+            conn.connect()
+            conn.putrequest('POST', '/upload', skip_host=True)
+            conn.putheader('Host', self.HOST)
+            conn.putheader('Content-Type', 'text/plain')
+            conn.putheader('Content-Length', '17')
+            conn.putheader('Expect', '100-continue')
+            conn.endheaders()
+            response = conn.response_class(conn.sock, method='POST')
+
+            # ...assert and then skip the 100 response
+            version, status, reason = response._read_status()
+            self.assertEqual(status, 100)
+            while True:
+                line = response.fp.readline().strip()
+                if line:
+                    self.fail(
+                        '100 Continue should not output any headers. Got %r' %
+                        line)
+                else:
+                    break
+
+            # ...send the body
+            body = b'I am a small file'
+            conn.send(body)
+
+            # ...get the final response
+            response.begin()
+            self.status, self.headers, self.body = webtest.shb(response)
+            self.assertStatus(200)
+            self.assertBody("thanks for '%s'" % body)
+        finally:
+            conn.close()
+
+
+class ConnectionTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_readall_or_close(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        if self.scheme == 'https':
+            self.HTTP_CONN = HTTPSConnection
+        else:
+            self.HTTP_CONN = HTTPConnection
+
+        # Test a max of 0 (the default) and then reset to what it was above.
+        old_max = cherrypy.server.max_request_body_size
+        for new_max in (0, old_max):
+            cherrypy.server.max_request_body_size = new_max
+
+            self.persistent = True
+            conn = self.HTTP_CONN
+
+            # Get a POST page with an error
+            conn.putrequest('POST', '/err_before_read', skip_host=True)
+            conn.putheader('Host', self.HOST)
+            conn.putheader('Content-Type', 'text/plain')
+            conn.putheader('Content-Length', '1000')
+            conn.putheader('Expect', '100-continue')
+            conn.endheaders()
+            response = conn.response_class(conn.sock, method='POST')
+
+            # ...assert and then skip the 100 response
+            version, status, reason = response._read_status()
+            self.assertEqual(status, 100)
+            while True:
+                skip = response.fp.readline().strip()
+                if not skip:
+                    break
+
+            # ...send the body
+            conn.send(ntob('x' * 1000))
+
+            # ...get the final response
+            response.begin()
+            self.status, self.headers, self.body = webtest.shb(response)
+            self.assertStatus(500)
+
+            # Now try a working page with an Expect header...
+            conn._output(b'POST /upload HTTP/1.1')
+            conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
+            conn._output(b'Content-Type: text/plain')
+            conn._output(b'Content-Length: 17')
+            conn._output(b'Expect: 100-continue')
+            conn._send_output()
+            response = conn.response_class(conn.sock, method='POST')
+
+            # ...assert and then skip the 100 response
+            version, status, reason = response._read_status()
+            self.assertEqual(status, 100)
+            while True:
+                skip = response.fp.readline().strip()
+                if not skip:
+                    break
+
+            # ...send the body
+            body = b'I am a small file'
+            conn.send(body)
+
+            # ...get the final response
+            response.begin()
+            self.status, self.headers, self.body = webtest.shb(response)
+            self.assertStatus(200)
+            self.assertBody("thanks for '%s'" % body)
+            conn.close()
+
+    def test_No_Message_Body(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Set our HTTP_CONN to an instance so it persists between requests.
+        self.persistent = True
+
+        # Make the first request and assert there's no "Connection: close".
+        self.getPage('/')
+        self.assertStatus('200 OK')
+        self.assertBody(pov)
+        self.assertNoHeader('Connection')
+
+        # Make a 204 request on the same connection.
+        self.getPage('/custom/204')
+        self.assertStatus(204)
+        self.assertNoHeader('Content-Length')
+        self.assertBody('')
+        self.assertNoHeader('Connection')
+
+        # Make a 304 request on the same connection.
+        self.getPage('/custom/304')
+        self.assertStatus(304)
+        self.assertNoHeader('Content-Length')
+        self.assertBody('')
+        self.assertNoHeader('Connection')
+
+    def test_Chunked_Encoding(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        if (hasattr(self, 'harness') and
+                'modpython' in self.harness.__class__.__name__.lower()):
+            # mod_python forbids chunked encoding
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Set our HTTP_CONN to an instance so it persists between requests.
+        self.persistent = True
+        conn = self.HTTP_CONN
+
+        # Try a normal chunked request (with extensions)
+        body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
+                    'Content-Type: application/json\r\n'
+                    '\r\n')
+        conn.putrequest('POST', '/upload', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.putheader('Transfer-Encoding', 'chunked')
+        conn.putheader('Trailer', 'Content-Type')
+        # Note that this is somewhat malformed:
+        # we shouldn't be sending Content-Length.
+        # RFC 2616 says the server should ignore it.
+        conn.putheader('Content-Length', '3')
+        conn.endheaders()
+        conn.send(body)
+        response = conn.getresponse()
+        self.status, self.headers, self.body = webtest.shb(response)
+        self.assertStatus('200 OK')
+        self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy')
+
+        # Try a chunked request that exceeds server.max_request_body_size.
+        # Note that the delimiters and trailer are included.
+        body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n')
+        conn.putrequest('POST', '/upload', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.putheader('Transfer-Encoding', 'chunked')
+        conn.putheader('Content-Type', 'text/plain')
+        # Chunked requests don't need a content-length
+        # #        conn.putheader("Content-Length", len(body))
+        conn.endheaders()
+        conn.send(body)
+        response = conn.getresponse()
+        self.status, self.headers, self.body = webtest.shb(response)
+        self.assertStatus(413)
+        conn.close()
+
+    def test_Content_Length_in(self):
+        # Try a non-chunked request where Content-Length exceeds
+        # server.max_request_body_size. Assert error before body send.
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('POST', '/upload', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.putheader('Content-Type', 'text/plain')
+        conn.putheader('Content-Length', '9999')
+        conn.endheaders()
+        response = conn.getresponse()
+        self.status, self.headers, self.body = webtest.shb(response)
+        self.assertStatus(413)
+        self.assertBody('The entity sent with the request exceeds '
+                        'the maximum allowed bytes.')
+        conn.close()
+
+    def test_Content_Length_out_preheaders(self):
+        # Try a non-chunked response where Content-Length is less than
+        # the actual bytes in the response body.
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5',
+                        skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.getresponse()
+        self.status, self.headers, self.body = webtest.shb(response)
+        self.assertStatus(500)
+        self.assertBody(
+            'The requested resource returned more bytes than the '
+            'declared Content-Length.')
+        conn.close()
+
+    def test_Content_Length_out_postheaders(self):
+        # Try a non-chunked response where Content-Length is less than
+        # the actual bytes in the response body.
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest(
+            'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5',
+            skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.getresponse()
+        self.status, self.headers, self.body = webtest.shb(response)
+        self.assertStatus(200)
+        self.assertBody('I too')
+        conn.close()
+
+    def test_598(self):
+        tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/'
+        url = tmpl.format(
+            scheme=self.scheme,
+            host=self.HOST,
+            port=self.PORT,
+        )
+        remote_data_conn = urllib.request.urlopen(url)
+        buf = remote_data_conn.read(512)
+        time.sleep(timeout * 0.6)
+        remaining = (1024 * 1024) - 512
+        while remaining:
+            data = remote_data_conn.read(remaining)
+            if not data:
+                break
+            else:
+                buf += data
+            remaining -= len(data)
+
+        self.assertEqual(len(buf), 1024 * 1024)
+        self.assertEqual(buf, ntob('a' * 1024 * 1024))
+        self.assertEqual(remaining, 0)
+        remote_data_conn.close()
+
+
+def setup_upload_server():
+
+    class Root:
+        @cherrypy.expose
+        def upload(self):
+            if not cherrypy.request.method == 'POST':
+                raise AssertionError("'POST' != request.method %r" %
+                                     cherrypy.request.method)
+            return "thanks for '%s'" % tonative(cherrypy.request.body.read())
+
+    cherrypy.tree.mount(Root())
+    cherrypy.config.update({
+        'server.max_request_body_size': 1001,
+        'server.socket_timeout': 10,
+        'server.accepted_queue_size': 5,
+        'server.accepted_queue_timeout': 0.1,
+    })
+
+
+reset_names = 'ECONNRESET', 'WSAECONNRESET'
+socket_reset_errors = [
+    getattr(errno, name)
+    for name in reset_names
+    if hasattr(errno, name)
+]
+'reset error numbers available on this platform'
+
+socket_reset_errors += [
+    # Python 3.5 raises an http.client.RemoteDisconnected
+    # with this message
+    'Remote end closed connection without response',
+]
+
+
+class LimitedRequestQueueTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_upload_server)
+
+    @pytest.mark.xfail(reason='#1535')
+    def test_queue_full(self):
+        conns = []
+        overflow_conn = None
+
+        try:
+            # Make 15 initial requests and leave them open, which should use
+            # all of wsgiserver's WorkerThreads and fill its Queue.
+            for i in range(15):
+                conn = self.HTTP_CONN(self.HOST, self.PORT)
+                conn.putrequest('POST', '/upload', skip_host=True)
+                conn.putheader('Host', self.HOST)
+                conn.putheader('Content-Type', 'text/plain')
+                conn.putheader('Content-Length', '4')
+                conn.endheaders()
+                conns.append(conn)
+
+            # Now try a 16th conn, which should be closed by the
+            # server immediately.
+            overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
+            # Manually connect since httplib won't let us set a timeout
+            for res in socket.getaddrinfo(self.HOST, self.PORT, 0,
+                                          socket.SOCK_STREAM):
+                af, socktype, proto, canonname, sa = res
+                overflow_conn.sock = socket.socket(af, socktype, proto)
+                overflow_conn.sock.settimeout(5)
+                overflow_conn.sock.connect(sa)
+                break
+
+            overflow_conn.putrequest('GET', '/', skip_host=True)
+            overflow_conn.putheader('Host', self.HOST)
+            overflow_conn.endheaders()
+            response = overflow_conn.response_class(
+                overflow_conn.sock,
+                method='GET',
+            )
+            try:
+                response.begin()
+            except socket.error as exc:
+                if exc.args[0] in socket_reset_errors:
+                    pass  # Expected.
+                else:
+                    tmpl = (
+                        'Overflow conn did not get RST. '
+                        'Got {exc.args!r} instead'
+                    )
+                    raise AssertionError(tmpl.format(**locals()))
+            except BadStatusLine:
+                # This is a special case in OS X. Linux and Windows will
+                # RST correctly.
+                assert sys.platform == 'darwin'
+            else:
+                raise AssertionError('Overflow conn did not get RST ')
+        finally:
+            for conn in conns:
+                conn.send(b'done')
+                response = conn.response_class(conn.sock, method='POST')
+                response.begin()
+                self.body = response.read()
+                self.assertBody("thanks for 'done'")
+                self.assertEqual(response.status, 200)
+                conn.close()
+            if overflow_conn:
+                overflow_conn.close()
+
+
+class BadRequestTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_No_CRLF(self):
+        self.persistent = True
+
+        conn = self.HTTP_CONN
+        conn.send(b'GET /hello HTTP/1.1\n\n')
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.body = response.read()
+        self.assertBody('HTTP requires CRLF terminators')
+        conn.close()
+
+        conn.connect()
+        conn.send(b'GET /hello HTTP/1.1\r\n\n')
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.body = response.read()
+        self.assertBody('HTTP requires CRLF terminators')
+        conn.close()
diff --git a/libraries/cherrypy/test/test_core.py b/libraries/cherrypy/test/test_core.py
new file mode 100644
index 00000000..9834c1f3
--- /dev/null
+++ b/libraries/cherrypy/test/test_core.py
@@ -0,0 +1,823 @@
+# coding: utf-8
+
+"""Basic tests for the CherryPy core: request handling."""
+
+import os
+import sys
+import types
+
+import six
+
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy import _cptools, tools
+from cherrypy.lib import httputil, static
+
+from cherrypy.test._test_decorators import ExposeExamples
+from cherrypy.test import helper
+
+
+localDir = os.path.dirname(__file__)
+favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico')
+
+#                             Client-side code                             #
+
+
+class CoreRequestHandlingTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'hello'
+
+            favicon_ico = tools.staticfile.handler(filename=favicon_path)
+
+            @cherrypy.expose
+            def defct(self, newct):
+                newct = 'text/%s' % newct
+                cherrypy.config.update({'tools.response_headers.on': True,
+                                        'tools.response_headers.headers':
+                                        [('Content-Type', newct)]})
+
+            @cherrypy.expose
+            def baseurl(self, path_info, relative=None):
+                return cherrypy.url(path_info, relative=bool(relative))
+
+        root = Root()
+        root.expose_dec = ExposeExamples()
+
+        class TestType(type):
+
+            """Metaclass which automatically exposes all functions in each
+            subclass, and adds an instance of the subclass as an attribute
+            of root.
+            """
+            def __init__(cls, name, bases, dct):
+                type.__init__(cls, name, bases, dct)
+                for value in six.itervalues(dct):
+                    if isinstance(value, types.FunctionType):
+                        value.exposed = True
+                setattr(root, name.lower(), cls())
+        Test = TestType('Test', (object, ), {})
+
+        @cherrypy.config(**{'tools.trailing_slash.on': False})
+        class URL(Test):
+
+            def index(self, path_info, relative=None):
+                if relative != 'server':
+                    relative = bool(relative)
+                return cherrypy.url(path_info, relative=relative)
+
+            def leaf(self, path_info, relative=None):
+                if relative != 'server':
+                    relative = bool(relative)
+                return cherrypy.url(path_info, relative=relative)
+
+            def qs(self, qs):
+                return cherrypy.url(qs=qs)
+
+        def log_status():
+            Status.statuses.append(cherrypy.response.status)
+        cherrypy.tools.log_status = cherrypy.Tool(
+            'on_end_resource', log_status)
+
+        class Status(Test):
+
+            def index(self):
+                return 'normal'
+
+            def blank(self):
+                cherrypy.response.status = ''
+
+            # According to RFC 2616, new status codes are OK as long as they
+            # are between 100 and 599.
+
+            # Here is an illegal code...
+            def illegal(self):
+                cherrypy.response.status = 781
+                return 'oops'
+
+            # ...and here is an unknown but legal code.
+            def unknown(self):
+                cherrypy.response.status = '431 My custom error'
+                return 'funky'
+
+            # Non-numeric code
+            def bad(self):
+                cherrypy.response.status = 'error'
+                return 'bad news'
+
+            statuses = []
+
+            @cherrypy.config(**{'tools.log_status.on': True})
+            def on_end_resource_stage(self):
+                return repr(self.statuses)
+
+        class Redirect(Test):
+
+            @cherrypy.config(**{
+                'tools.err_redirect.on': True,
+                'tools.err_redirect.url': '/errpage',
+                'tools.err_redirect.internal': False,
+            })
+            class Error:
+                @cherrypy.expose
+                def index(self):
+                    raise NameError('redirect_test')
+
+            error = Error()
+
+            def index(self):
+                return 'child'
+
+            def custom(self, url, code):
+                raise cherrypy.HTTPRedirect(url, code)
+
+            @cherrypy.config(**{'tools.trailing_slash.extra': True})
+            def by_code(self, code):
+                raise cherrypy.HTTPRedirect('somewhere%20else', code)
+
+            def nomodify(self):
+                raise cherrypy.HTTPRedirect('', 304)
+
+            def proxy(self):
+                raise cherrypy.HTTPRedirect('proxy', 305)
+
+            def stringify(self):
+                return str(cherrypy.HTTPRedirect('/'))
+
+            def fragment(self, frag):
+                raise cherrypy.HTTPRedirect('/some/url#%s' % frag)
+
+            def url_with_quote(self):
+                raise cherrypy.HTTPRedirect("/some\"url/that'we/want")
+
+            def url_with_xss(self):
+                raise cherrypy.HTTPRedirect(
+                    "/some<script>alert(1);</script>url/that'we/want")
+
+            def url_with_unicode(self):
+                raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8'))
+
+        def login_redir():
+            if not getattr(cherrypy.request, 'login', None):
+                raise cherrypy.InternalRedirect('/internalredirect/login')
+        tools.login_redir = _cptools.Tool('before_handler', login_redir)
+
+        def redir_custom():
+            raise cherrypy.InternalRedirect('/internalredirect/custom_err')
+
+        class InternalRedirect(Test):
+
+            def index(self):
+                raise cherrypy.InternalRedirect('/')
+
+            @cherrypy.expose
+            @cherrypy.config(**{'hooks.before_error_response': redir_custom})
+            def choke(self):
+                return 3 / 0
+
+            def relative(self, a, b):
+                raise cherrypy.InternalRedirect('cousin?t=6')
+
+            def cousin(self, t):
+                assert cherrypy.request.prev.closed
+                return cherrypy.request.prev.query_string
+
+            def petshop(self, user_id):
+                if user_id == 'parrot':
+                    # Trade it for a slug when redirecting
+                    raise cherrypy.InternalRedirect(
+                        '/image/getImagesByUser?user_id=slug')
+                elif user_id == 'terrier':
+                    # Trade it for a fish when redirecting
+                    raise cherrypy.InternalRedirect(
+                        '/image/getImagesByUser?user_id=fish')
+                else:
+                    # This should pass the user_id through to getImagesByUser
+                    raise cherrypy.InternalRedirect(
+                        '/image/getImagesByUser?user_id=%s' % str(user_id))
+
+            # We support Python 2.3, but the @-deco syntax would look like
+            # this:
+            # @tools.login_redir()
+            def secure(self):
+                return 'Welcome!'
+            secure = tools.login_redir()(secure)
+            # Since calling the tool returns the same function you pass in,
+            # you could skip binding the return value, and just write:
+            # tools.login_redir()(secure)
+
+            def login(self):
+                return 'Please log in'
+
+            def custom_err(self):
+                return 'Something went horribly wrong.'
+
+            @cherrypy.config(**{'hooks.before_request_body': redir_custom})
+            def early_ir(self, arg):
+                return 'whatever'
+
+        class Image(Test):
+
+            def getImagesByUser(self, user_id):
+                return '0 images for %s' % user_id
+
+        class Flatten(Test):
+
+            def as_string(self):
+                return 'content'
+
+            def as_list(self):
+                return ['con', 'tent']
+
+            def as_yield(self):
+                yield b'content'
+
+            @cherrypy.config(**{'tools.flatten.on': True})
+            def as_dblyield(self):
+                yield self.as_yield()
+
+            def as_refyield(self):
+                for chunk in self.as_yield():
+                    yield chunk
+
+        class Ranges(Test):
+
+            def get_ranges(self, bytes):
+                return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
+
+            def slice_file(self):
+                path = os.path.join(os.getcwd(), os.path.dirname(__file__))
+                return static.serve_file(
+                    os.path.join(path, 'static/index.html'))
+
+        class Cookies(Test):
+
+            def single(self, name):
+                cookie = cherrypy.request.cookie[name]
+                # Python2's SimpleCookie.__setitem__ won't take unicode keys.
+                cherrypy.response.cookie[str(name)] = cookie.value
+
+            def multiple(self, names):
+                list(map(self.single, names))
+
+        def append_headers(header_list, debug=False):
+            if debug:
+                cherrypy.log(
+                    'Extending response headers with %s' % repr(header_list),
+                    'TOOLS.APPEND_HEADERS')
+            cherrypy.serving.response.header_list.extend(header_list)
+        cherrypy.tools.append_headers = cherrypy.Tool(
+            'on_end_resource', append_headers)
+
+        class MultiHeader(Test):
+
+            def header_list(self):
+                pass
+            header_list = cherrypy.tools.append_headers(header_list=[
+                (b'WWW-Authenticate', b'Negotiate'),
+                (b'WWW-Authenticate', b'Basic realm="foo"'),
+            ])(header_list)
+
+            def commas(self):
+                cherrypy.response.headers[
+                    'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
+
+        cherrypy.tree.mount(root)
+
+    def testStatus(self):
+        self.getPage('/status/')
+        self.assertBody('normal')
+        self.assertStatus(200)
+
+        self.getPage('/status/blank')
+        self.assertBody('')
+        self.assertStatus(200)
+
+        self.getPage('/status/illegal')
+        self.assertStatus(500)
+        msg = 'Illegal response status from server (781 is out of range).'
+        self.assertErrorPage(500, msg)
+
+        if not getattr(cherrypy.server, 'using_apache', False):
+            self.getPage('/status/unknown')
+            self.assertBody('funky')
+            self.assertStatus(431)
+
+        self.getPage('/status/bad')
+        self.assertStatus(500)
+        msg = "Illegal response status from server ('error' is non-numeric)."
+        self.assertErrorPage(500, msg)
+
+    def test_on_end_resource_status(self):
+        self.getPage('/status/on_end_resource_stage')
+        self.assertBody('[]')
+        self.getPage('/status/on_end_resource_stage')
+        self.assertBody(repr(['200 OK']))
+
+    def testSlashes(self):
+        # Test that requests for index methods without a trailing slash
+        # get redirected to the same URI path with a trailing slash.
+        # Make sure GET params are preserved.
+        self.getPage('/redirect?id=3')
+        self.assertStatus(301)
+        self.assertMatchesBody(
+            '<a href=([\'"])%s/redirect/[?]id=3\\1>'
+            '%s/redirect/[?]id=3</a>' % (self.base(), self.base())
+        )
+
+        if self.prefix():
+            # Corner case: the "trailing slash" redirect could be tricky if
+            # we're using a virtual root and the URI is "/vroot" (no slash).
+            self.getPage('')
+            self.assertStatus(301)
+            self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" %
+                                   (self.base(), self.base()))
+
+        # Test that requests for NON-index methods WITH a trailing slash
+        # get redirected to the same URI path WITHOUT a trailing slash.
+        # Make sure GET params are preserved.
+        self.getPage('/redirect/by_code/?code=307')
+        self.assertStatus(301)
+        self.assertMatchesBody(
+            "<a href=(['\"])%s/redirect/by_code[?]code=307\\1>"
+            '%s/redirect/by_code[?]code=307</a>'
+            % (self.base(), self.base())
+        )
+
+        # If the trailing_slash tool is off, CP should just continue
+        # as if the slashes were correct. But it needs some help
+        # inside cherrypy.url to form correct output.
+        self.getPage('/url?path_info=page1')
+        self.assertBody('%s/url/page1' % self.base())
+        self.getPage('/url/leaf/?path_info=page1')
+        self.assertBody('%s/url/page1' % self.base())
+
+    def testRedirect(self):
+        self.getPage('/redirect/')
+        self.assertBody('child')
+        self.assertStatus(200)
+
+        self.getPage('/redirect/by_code?code=300')
+        self.assertMatchesBody(
+            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
+        self.assertStatus(300)
+
+        self.getPage('/redirect/by_code?code=301')
+        self.assertMatchesBody(
+            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
+        self.assertStatus(301)
+
+        self.getPage('/redirect/by_code?code=302')
+        self.assertMatchesBody(
+            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
+        self.assertStatus(302)
+
+        self.getPage('/redirect/by_code?code=303')
+        self.assertMatchesBody(
+            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
+        self.assertStatus(303)
+
+        self.getPage('/redirect/by_code?code=307')
+        self.assertMatchesBody(
+            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
+        self.assertStatus(307)
+
+        self.getPage('/redirect/nomodify')
+        self.assertBody('')
+        self.assertStatus(304)
+
+        self.getPage('/redirect/proxy')
+        self.assertBody('')
+        self.assertStatus(305)
+
+        # HTTPRedirect on error
+        self.getPage('/redirect/error/')
+        self.assertStatus(('302 Found', '303 See Other'))
+        self.assertInBody('/errpage')
+
+        # Make sure str(HTTPRedirect()) works.
+        self.getPage('/redirect/stringify', protocol='HTTP/1.0')
+        self.assertStatus(200)
+        self.assertBody("(['%s/'], 302)" % self.base())
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.getPage('/redirect/stringify', protocol='HTTP/1.1')
+            self.assertStatus(200)
+            self.assertBody("(['%s/'], 303)" % self.base())
+
+        # check that #fragments are handled properly
+        # http://skrb.org/ietf/http_errata.html#location-fragments
+        frag = 'foo'
+        self.getPage('/redirect/fragment/%s' % frag)
+        self.assertMatchesBody(
+            r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % (
+                frag, frag))
+        loc = self.assertHeader('Location')
+        assert loc.endswith('#%s' % frag)
+        self.assertStatus(('302 Found', '303 See Other'))
+
+        # check injection protection
+        # See https://github.com/cherrypy/cherrypy/issues/1003
+        self.getPage(
+            '/redirect/custom?'
+            'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval')
+        self.assertStatus(303)
+        loc = self.assertHeader('Location')
+        assert 'Set-Cookie' in loc
+        self.assertNoHeader('Set-Cookie')
+
+        def assertValidXHTML():
+            from xml.etree import ElementTree
+            try:
+                ElementTree.fromstring(
+                    '<html><body>%s</body></html>' % self.body,
+                )
+            except ElementTree.ParseError:
+                self._handlewebError(
+                    'automatically generated redirect did not '
+                    'generate well-formed html',
+                )
+
+        # check redirects to URLs generated valid HTML - we check this
+        # by seeing if it appears as valid XHTML.
+        self.getPage('/redirect/by_code?code=303')
+        self.assertStatus(303)
+        assertValidXHTML()
+
+        # do the same with a url containing quote characters.
+        self.getPage('/redirect/url_with_quote')
+        self.assertStatus(303)
+        assertValidXHTML()
+
+    def test_redirect_with_xss(self):
+        """A redirect to a URL with HTML injected should result
+        in page contents escaped."""
+        self.getPage('/redirect/url_with_xss')
+        self.assertStatus(303)
+        assert b'<script>' not in self.body
+        assert b'&lt;script&gt;' in self.body
+
+    def test_redirect_with_unicode(self):
+        """
+        A redirect to a URL with Unicode should return a Location
+        header containing that Unicode URL.
+        """
+        # test disabled due to #1440
+        return
+        self.getPage('/redirect/url_with_unicode')
+        self.assertStatus(303)
+        loc = self.assertHeader('Location')
+        assert ntou('тест', encoding='utf-8') in loc
+
+    def test_InternalRedirect(self):
+        # InternalRedirect
+        self.getPage('/internalredirect/')
+        self.assertBody('hello')
+        self.assertStatus(200)
+
+        # Test passthrough
+        self.getPage(
+            '/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film')
+        self.assertBody('0 images for Sir-not-appearing-in-this-film')
+        self.assertStatus(200)
+
+        # Test args
+        self.getPage('/internalredirect/petshop?user_id=parrot')
+        self.assertBody('0 images for slug')
+        self.assertStatus(200)
+
+        # Test POST
+        self.getPage('/internalredirect/petshop', method='POST',
+                     body='user_id=terrier')
+        self.assertBody('0 images for fish')
+        self.assertStatus(200)
+
+        # Test ir before body read
+        self.getPage('/internalredirect/early_ir', method='POST',
+                     body='arg=aha!')
+        self.assertBody('Something went horribly wrong.')
+        self.assertStatus(200)
+
+        self.getPage('/internalredirect/secure')
+        self.assertBody('Please log in')
+        self.assertStatus(200)
+
+        # Relative path in InternalRedirect.
+        # Also tests request.prev.
+        self.getPage('/internalredirect/relative?a=3&b=5')
+        self.assertBody('a=3&b=5')
+        self.assertStatus(200)
+
+        # InternalRedirect on error
+        self.getPage('/internalredirect/choke')
+        self.assertStatus(200)
+        self.assertBody('Something went horribly wrong.')
+
+    def testFlatten(self):
+        for url in ['/flatten/as_string', '/flatten/as_list',
+                    '/flatten/as_yield', '/flatten/as_dblyield',
+                    '/flatten/as_refyield']:
+            self.getPage(url)
+            self.assertBody('content')
+
+    def testRanges(self):
+        self.getPage('/ranges/get_ranges?bytes=3-6')
+        self.assertBody('[(3, 7)]')
+
+        # Test multiple ranges and a suffix-byte-range-spec, for good measure.
+        self.getPage('/ranges/get_ranges?bytes=2-4,-1')
+        self.assertBody('[(2, 5), (7, 8)]')
+
+        # Test a suffix-byte-range longer than the content
+        # length. Note that in this test, the content length
+        # is 8 bytes.
+        self.getPage('/ranges/get_ranges?bytes=-100')
+        self.assertBody('[(0, 8)]')
+
+        # Get a partial file.
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
+            self.assertStatus(206)
+            self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+            self.assertHeader('Content-Range', 'bytes 2-5/14')
+            self.assertBody('llo,')
+
+            # What happens with overlapping ranges (and out of order, too)?
+            self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')])
+            self.assertStatus(206)
+            ct = self.assertHeader('Content-Type')
+            expected_type = 'multipart/byteranges; boundary='
+            self.assert_(ct.startswith(expected_type))
+            boundary = ct[len(expected_type):]
+            expected_body = ('\r\n--%s\r\n'
+                             'Content-type: text/html\r\n'
+                             'Content-range: bytes 4-6/14\r\n'
+                             '\r\n'
+                             'o, \r\n'
+                             '--%s\r\n'
+                             'Content-type: text/html\r\n'
+                             'Content-range: bytes 2-5/14\r\n'
+                             '\r\n'
+                             'llo,\r\n'
+                             '--%s--\r\n' % (boundary, boundary, boundary))
+            self.assertBody(expected_body)
+            self.assertHeader('Content-Length')
+
+            # Test "416 Requested Range Not Satisfiable"
+            self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')])
+            self.assertStatus(416)
+            # "When this status code is returned for a byte-range request,
+            # the response SHOULD include a Content-Range entity-header
+            # field specifying the current length of the selected resource"
+            self.assertHeader('Content-Range', 'bytes */14')
+        elif cherrypy.server.protocol_version == 'HTTP/1.0':
+            # Test Range behavior with HTTP/1.0 request
+            self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
+            self.assertStatus(200)
+            self.assertBody('Hello, world\r\n')
+
+    def testFavicon(self):
+        # favicon.ico is served by staticfile.
+        icofilename = os.path.join(localDir, '../favicon.ico')
+        icofile = open(icofilename, 'rb')
+        data = icofile.read()
+        icofile.close()
+
+        self.getPage('/favicon.ico')
+        self.assertBody(data)
+
+    def skip_if_bad_cookies(self):
+        """
+        cookies module fails to reject invalid cookies
+        https://github.com/cherrypy/cherrypy/issues/1405
+        """
+        cookies = sys.modules.get('http.cookies')
+        _is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False)
+        if not _is_legal_key(','):
+            return
+        issue = 'http://bugs.python.org/issue26302'
+        tmpl = 'Broken cookies module ({issue})'
+        self.skip(tmpl.format(**locals()))
+
+    def testCookies(self):
+        self.skip_if_bad_cookies()
+
+        self.getPage('/cookies/single?name=First',
+                     [('Cookie', 'First=Dinsdale;')])
+        self.assertHeader('Set-Cookie', 'First=Dinsdale')
+
+        self.getPage('/cookies/multiple?names=First&names=Last',
+                     [('Cookie', 'First=Dinsdale; Last=Piranha;'),
+                      ])
+        self.assertHeader('Set-Cookie', 'First=Dinsdale')
+        self.assertHeader('Set-Cookie', 'Last=Piranha')
+
+        self.getPage('/cookies/single?name=Something-With%2CComma',
+                     [('Cookie', 'Something-With,Comma=some-value')])
+        self.assertStatus(400)
+
+    def testDefaultContentType(self):
+        self.getPage('/')
+        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+        self.getPage('/defct/plain')
+        self.getPage('/')
+        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+        self.getPage('/defct/html')
+
+    def test_multiple_headers(self):
+        self.getPage('/multiheader/header_list')
+        self.assertEqual(
+            [(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
+            [('WWW-Authenticate', 'Negotiate'),
+             ('WWW-Authenticate', 'Basic realm="foo"'),
+             ])
+        self.getPage('/multiheader/commas')
+        self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"')
+
+    def test_cherrypy_url(self):
+        # Input relative to current
+        self.getPage('/url/leaf?path_info=page1')
+        self.assertBody('%s/url/page1' % self.base())
+        self.getPage('/url/?path_info=page1')
+        self.assertBody('%s/url/page1' % self.base())
+        # Other host header
+        host = 'www.mydomain.example'
+        self.getPage('/url/leaf?path_info=page1',
+                     headers=[('Host', host)])
+        self.assertBody('%s://%s/url/page1' % (self.scheme, host))
+
+        # Input is 'absolute'; that is, relative to script_name
+        self.getPage('/url/leaf?path_info=/page1')
+        self.assertBody('%s/page1' % self.base())
+        self.getPage('/url/?path_info=/page1')
+        self.assertBody('%s/page1' % self.base())
+
+        # Single dots
+        self.getPage('/url/leaf?path_info=./page1')
+        self.assertBody('%s/url/page1' % self.base())
+        self.getPage('/url/leaf?path_info=other/./page1')
+        self.assertBody('%s/url/other/page1' % self.base())
+        self.getPage('/url/?path_info=/other/./page1')
+        self.assertBody('%s/other/page1' % self.base())
+        self.getPage('/url/?path_info=/other/././././page1')
+        self.assertBody('%s/other/page1' % self.base())
+
+        # Double dots
+        self.getPage('/url/leaf?path_info=../page1')
+        self.assertBody('%s/page1' % self.base())
+        self.getPage('/url/leaf?path_info=other/../page1')
+        self.assertBody('%s/url/page1' % self.base())
+        self.getPage('/url/leaf?path_info=/other/../page1')
+        self.assertBody('%s/page1' % self.base())
+        self.getPage('/url/leaf?path_info=/other/../../../page1')
+        self.assertBody('%s/page1' % self.base())
+        self.getPage('/url/leaf?path_info=/other/../../../../../page1')
+        self.assertBody('%s/page1' % self.base())
+
+        # qs param is not normalized as a path
+        self.getPage('/url/qs?qs=/other')
+        self.assertBody('%s/url/qs?/other' % self.base())
+        self.getPage('/url/qs?qs=/other/../page1')
+        self.assertBody('%s/url/qs?/other/../page1' % self.base())
+        self.getPage('/url/qs?qs=../page1')
+        self.assertBody('%s/url/qs?../page1' % self.base())
+        self.getPage('/url/qs?qs=../../page1')
+        self.assertBody('%s/url/qs?../../page1' % self.base())
+
+        # Output relative to current path or script_name
+        self.getPage('/url/?path_info=page1&relative=True')
+        self.assertBody('page1')
+        self.getPage('/url/leaf?path_info=/page1&relative=True')
+        self.assertBody('../page1')
+        self.getPage('/url/leaf?path_info=page1&relative=True')
+        self.assertBody('page1')
+        self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
+        self.assertBody('leaf/page1')
+        self.getPage('/url/leaf?path_info=../page1&relative=True')
+        self.assertBody('../page1')
+        self.getPage('/url/?path_info=other/../page1&relative=True')
+        self.assertBody('page1')
+
+        # Output relative to /
+        self.getPage('/baseurl?path_info=ab&relative=True')
+        self.assertBody('ab')
+        # Output relative to /
+        self.getPage('/baseurl?path_info=/ab&relative=True')
+        self.assertBody('ab')
+
+        # absolute-path references ("server-relative")
+        # Input relative to current
+        self.getPage('/url/leaf?path_info=page1&relative=server')
+        self.assertBody('/url/page1')
+        self.getPage('/url/?path_info=page1&relative=server')
+        self.assertBody('/url/page1')
+        # Input is 'absolute'; that is, relative to script_name
+        self.getPage('/url/leaf?path_info=/page1&relative=server')
+        self.assertBody('/page1')
+        self.getPage('/url/?path_info=/page1&relative=server')
+        self.assertBody('/page1')
+
+    def test_expose_decorator(self):
+        # Test @expose
+        self.getPage('/expose_dec/no_call')
+        self.assertStatus(200)
+        self.assertBody('Mr E. R. Bradshaw')
+
+        # Test @expose()
+        self.getPage('/expose_dec/call_empty')
+        self.assertStatus(200)
+        self.assertBody('Mrs. B.J. Smegma')
+
+        # Test @expose("alias")
+        self.getPage('/expose_dec/call_alias')
+        self.assertStatus(200)
+        self.assertBody('Mr Nesbitt')
+        # Does the original name work?
+        self.getPage('/expose_dec/nesbitt')
+        self.assertStatus(200)
+        self.assertBody('Mr Nesbitt')
+
+        # Test @expose(["alias1", "alias2"])
+        self.getPage('/expose_dec/alias1')
+        self.assertStatus(200)
+        self.assertBody('Mr Ken Andrews')
+        self.getPage('/expose_dec/alias2')
+        self.assertStatus(200)
+        self.assertBody('Mr Ken Andrews')
+        # Does the original name work?
+        self.getPage('/expose_dec/andrews')
+        self.assertStatus(200)
+        self.assertBody('Mr Ken Andrews')
+
+        # Test @expose(alias="alias")
+        self.getPage('/expose_dec/alias3')
+        self.assertStatus(200)
+        self.assertBody('Mr. and Mrs. Watson')
+
+
+class ErrorTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        def break_header():
+            # Add a header after finalize that is invalid
+            cherrypy.serving.response.header_list.append((2, 3))
+        cherrypy.tools.break_header = cherrypy.Tool(
+            'on_end_resource', break_header)
+
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'hello'
+
+            @cherrypy.config(**{'tools.break_header.on': True})
+            def start_response_error(self):
+                return 'salud!'
+
+            @cherrypy.expose
+            def stat(self, path):
+                with cherrypy.HTTPError.handle(OSError, 404):
+                    os.stat(path)
+
+        root = Root()
+
+        cherrypy.tree.mount(root)
+
+    def test_start_response_error(self):
+        self.getPage('/start_response_error')
+        self.assertStatus(500)
+        self.assertInBody(
+            'TypeError: response.header_list key 2 is not a byte string.')
+
+    def test_contextmanager(self):
+        self.getPage('/stat/missing')
+        self.assertStatus(404)
+        body_text = self.body.decode('utf-8')
+        assert (
+            'No such file or directory' in body_text or
+            'cannot find the file specified' in body_text
+        )
+
+
+class TestBinding:
+    def test_bind_ephemeral_port(self):
+        """
+        A server configured to bind to port 0 will bind to an ephemeral
+        port and indicate that port number on startup.
+        """
+        cherrypy.config.reset()
+        bind_ephemeral_conf = {
+            'server.socket_port': 0,
+        }
+        cherrypy.config.update(bind_ephemeral_conf)
+        cherrypy.engine.start()
+        assert cherrypy.server.bound_addr != cherrypy.server.bind_addr
+        _host, port = cherrypy.server.bound_addr
+        assert port > 0
+        cherrypy.engine.stop()
+        assert cherrypy.server.bind_addr == cherrypy.server.bound_addr
diff --git a/libraries/cherrypy/test/test_dynamicobjectmapping.py b/libraries/cherrypy/test/test_dynamicobjectmapping.py
new file mode 100644
index 00000000..725a3ce0
--- /dev/null
+++ b/libraries/cherrypy/test/test_dynamicobjectmapping.py
@@ -0,0 +1,424 @@
+import six
+
+import cherrypy
+from cherrypy.test import helper
+
+script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
+
+
+def setup_server():
+    class SubSubRoot:
+
+        @cherrypy.expose
+        def index(self):
+            return 'SubSubRoot index'
+
+        @cherrypy.expose
+        def default(self, *args):
+            return 'SubSubRoot default'
+
+        @cherrypy.expose
+        def handler(self):
+            return 'SubSubRoot handler'
+
+        @cherrypy.expose
+        def dispatch(self):
+            return 'SubSubRoot dispatch'
+
+    subsubnodes = {
+        '1': SubSubRoot(),
+        '2': SubSubRoot(),
+    }
+
+    class SubRoot:
+
+        @cherrypy.expose
+        def index(self):
+            return 'SubRoot index'
+
+        @cherrypy.expose
+        def default(self, *args):
+            return 'SubRoot %s' % (args,)
+
+        @cherrypy.expose
+        def handler(self):
+            return 'SubRoot handler'
+
+        def _cp_dispatch(self, vpath):
+            return subsubnodes.get(vpath[0], None)
+
+    subnodes = {
+        '1': SubRoot(),
+        '2': SubRoot(),
+    }
+
+    class Root:
+
+        @cherrypy.expose
+        def index(self):
+            return 'index'
+
+        @cherrypy.expose
+        def default(self, *args):
+            return 'default %s' % (args,)
+
+        @cherrypy.expose
+        def handler(self):
+            return 'handler'
+
+        def _cp_dispatch(self, vpath):
+            return subnodes.get(vpath[0])
+
+    # -------------------------------------------------------------------------
+    # DynamicNodeAndMethodDispatcher example.
+    # This example exposes a fairly naive HTTP api
+    class User(object):
+
+        def __init__(self, id, name):
+            self.id = id
+            self.name = name
+
+        def __unicode__(self):
+            return six.text_type(self.name)
+
+        def __str__(self):
+            return str(self.name)
+
+    user_lookup = {
+        1: User(1, 'foo'),
+        2: User(2, 'bar'),
+    }
+
+    def make_user(name, id=None):
+        if not id:
+            id = max(*list(user_lookup.keys())) + 1
+        user_lookup[id] = User(id, name)
+        return id
+
+    @cherrypy.expose
+    class UserContainerNode(object):
+
+        def POST(self, name):
+            """
+            Allow the creation of a new Object
+            """
+            return 'POST %d' % make_user(name)
+
+        def GET(self):
+            return six.text_type(sorted(user_lookup.keys()))
+
+        def dynamic_dispatch(self, vpath):
+            try:
+                id = int(vpath[0])
+            except (ValueError, IndexError):
+                return None
+            return UserInstanceNode(id)
+
+    @cherrypy.expose
+    class UserInstanceNode(object):
+
+        def __init__(self, id):
+            self.id = id
+            self.user = user_lookup.get(id, None)
+
+            # For all but PUT methods there MUST be a valid user identified
+            # by self.id
+            if not self.user and cherrypy.request.method != 'PUT':
+                raise cherrypy.HTTPError(404)
+
+        def GET(self, *args, **kwargs):
+            """
+            Return the appropriate representation of the instance.
+            """
+            return six.text_type(self.user)
+
+        def POST(self, name):
+            """
+            Update the fields of the user instance.
+            """
+            self.user.name = name
+            return 'POST %d' % self.user.id
+
+        def PUT(self, name):
+            """
+            Create a new user with the specified id, or edit it if it already
+            exists
+            """
+            if self.user:
+                # Edit the current user
+                self.user.name = name
+                return 'PUT %d' % self.user.id
+            else:
+                # Make a new user with said attributes.
+                return 'PUT %d' % make_user(name, self.id)
+
+        def DELETE(self):
+            """
+            Delete the user specified at the id.
+            """
+            id = self.user.id
+            del user_lookup[self.user.id]
+            del self.user
+            return 'DELETE %d' % id
+
+    class ABHandler:
+
+        class CustomDispatch:
+
+            @cherrypy.expose
+            def index(self, a, b):
+                return 'custom'
+
+        def _cp_dispatch(self, vpath):
+            """Make sure that if we don't pop anything from vpath,
+            processing still works.
+            """
+            return self.CustomDispatch()
+
+        @cherrypy.expose
+        def index(self, a, b=None):
+            body = ['a:' + str(a)]
+            if b is not None:
+                body.append(',b:' + str(b))
+            return ''.join(body)
+
+        @cherrypy.expose
+        def delete(self, a, b):
+            return 'deleting ' + str(a) + ' and ' + str(b)
+
+    class IndexOnly:
+
+        def _cp_dispatch(self, vpath):
+            """Make sure that popping ALL of vpath still shows the index
+            handler.
+            """
+            while vpath:
+                vpath.pop()
+            return self
+
+        @cherrypy.expose
+        def index(self):
+            return 'IndexOnly index'
+
+    class DecoratedPopArgs:
+
+        """Test _cp_dispatch with @cherrypy.popargs."""
+
+        @cherrypy.expose
+        def index(self):
+            return 'no params'
+
+        @cherrypy.expose
+        def hi(self):
+            return "hi was not interpreted as 'a' param"
+    DecoratedPopArgs = cherrypy.popargs(
+        'a', 'b', handler=ABHandler())(DecoratedPopArgs)
+
+    class NonDecoratedPopArgs:
+
+        """Test _cp_dispatch = cherrypy.popargs()"""
+
+        _cp_dispatch = cherrypy.popargs('a')
+
+        @cherrypy.expose
+        def index(self, a):
+            return 'index: ' + str(a)
+
+    class ParameterizedHandler:
+
+        """Special handler created for each request"""
+
+        def __init__(self, a):
+            self.a = a
+
+        @cherrypy.expose
+        def index(self):
+            if 'a' in cherrypy.request.params:
+                raise Exception(
+                    'Parameterized handler argument ended up in '
+                    'request.params')
+            return self.a
+
+    class ParameterizedPopArgs:
+
+        """Test cherrypy.popargs() with a function call handler"""
+    ParameterizedPopArgs = cherrypy.popargs(
+        'a', handler=ParameterizedHandler)(ParameterizedPopArgs)
+
+    Root.decorated = DecoratedPopArgs()
+    Root.undecorated = NonDecoratedPopArgs()
+    Root.index_only = IndexOnly()
+    Root.parameter_test = ParameterizedPopArgs()
+
+    Root.users = UserContainerNode()
+
+    md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
+    for url in script_names:
+        conf = {
+            '/': {
+                'user': (url or '/').split('/')[-2],
+            },
+            '/users': {
+                'request.dispatch': md
+            },
+        }
+        cherrypy.tree.mount(Root(), url, conf)
+
+
+class DynamicObjectMappingTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def testObjectMapping(self):
+        for url in script_names:
+            self.script_name = url
+
+            self.getPage('/')
+            self.assertBody('index')
+
+            self.getPage('/handler')
+            self.assertBody('handler')
+
+            # Dynamic dispatch will succeed here for the subnodes
+            # so the subroot gets called
+            self.getPage('/1/')
+            self.assertBody('SubRoot index')
+
+            self.getPage('/2/')
+            self.assertBody('SubRoot index')
+
+            self.getPage('/1/handler')
+            self.assertBody('SubRoot handler')
+
+            self.getPage('/2/handler')
+            self.assertBody('SubRoot handler')
+
+            # Dynamic dispatch will fail here for the subnodes
+            # so the default gets called
+            self.getPage('/asdf/')
+            self.assertBody("default ('asdf',)")
+
+            self.getPage('/asdf/asdf')
+            self.assertBody("default ('asdf', 'asdf')")
+
+            self.getPage('/asdf/handler')
+            self.assertBody("default ('asdf', 'handler')")
+
+            # Dynamic dispatch will succeed here for the subsubnodes
+            # so the subsubroot gets called
+            self.getPage('/1/1/')
+            self.assertBody('SubSubRoot index')
+
+            self.getPage('/2/2/')
+            self.assertBody('SubSubRoot index')
+
+            self.getPage('/1/1/handler')
+            self.assertBody('SubSubRoot handler')
+
+            self.getPage('/2/2/handler')
+            self.assertBody('SubSubRoot handler')
+
+            self.getPage('/2/2/dispatch')
+            self.assertBody('SubSubRoot dispatch')
+
+            # The exposed dispatch will not be called as a dispatch
+            # method.
+            self.getPage('/2/2/foo/foo')
+            self.assertBody('SubSubRoot default')
+
+            # Dynamic dispatch will fail here for the subsubnodes
+            # so the SubRoot gets called
+            self.getPage('/1/asdf/')
+            self.assertBody("SubRoot ('asdf',)")
+
+            self.getPage('/1/asdf/asdf')
+            self.assertBody("SubRoot ('asdf', 'asdf')")
+
+            self.getPage('/1/asdf/handler')
+            self.assertBody("SubRoot ('asdf', 'handler')")
+
+    def testMethodDispatch(self):
+        # GET acts like a container
+        self.getPage('/users')
+        self.assertBody('[1, 2]')
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        # POST to the container URI allows creation
+        self.getPage('/users', method='POST', body='name=baz')
+        self.assertBody('POST 3')
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        # POST to a specific instanct URI results in a 404
+        # as the resource does not exit.
+        self.getPage('/users/5', method='POST', body='name=baz')
+        self.assertStatus(404)
+
+        # PUT to a specific instanct URI results in creation
+        self.getPage('/users/5', method='PUT', body='name=boris')
+        self.assertBody('PUT 5')
+        self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT')
+
+        # GET acts like a container
+        self.getPage('/users')
+        self.assertBody('[1, 2, 3, 5]')
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        test_cases = (
+            (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'),
+            (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'),
+            (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'),
+            (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'),
+        )
+        for id, name, updatedname, headers in test_cases:
+            self.getPage('/users/%d' % id)
+            self.assertBody(name)
+            self.assertHeader('Allow', headers)
+
+            # Make sure POSTs update already existings resources
+            self.getPage('/users/%d' %
+                         id, method='POST', body='name=%s' % updatedname)
+            self.assertBody('POST %d' % id)
+            self.assertHeader('Allow', headers)
+
+            # Make sure PUTs Update already existing resources.
+            self.getPage('/users/%d' %
+                         id, method='PUT', body='name=%s' % updatedname)
+            self.assertBody('PUT %d' % id)
+            self.assertHeader('Allow', headers)
+
+            # Make sure DELETES Remove already existing resources.
+            self.getPage('/users/%d' % id, method='DELETE')
+            self.assertBody('DELETE %d' % id)
+            self.assertHeader('Allow', headers)
+
+        # GET acts like a container
+        self.getPage('/users')
+        self.assertBody('[]')
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+    def testVpathDispatch(self):
+        self.getPage('/decorated/')
+        self.assertBody('no params')
+
+        self.getPage('/decorated/hi')
+        self.assertBody("hi was not interpreted as 'a' param")
+
+        self.getPage('/decorated/yo/')
+        self.assertBody('a:yo')
+
+        self.getPage('/decorated/yo/there/')
+        self.assertBody('a:yo,b:there')
+
+        self.getPage('/decorated/yo/there/delete')
+        self.assertBody('deleting yo and there')
+
+        self.getPage('/decorated/yo/there/handled_by_dispatch/')
+        self.assertBody('custom')
+
+        self.getPage('/undecorated/blah/')
+        self.assertBody('index: blah')
+
+        self.getPage('/index_only/a/b/c/d/e/f/g/')
+        self.assertBody('IndexOnly index')
+
+        self.getPage('/parameter_test/argument2/')
+        self.assertBody('argument2')
diff --git a/libraries/cherrypy/test/test_encoding.py b/libraries/cherrypy/test/test_encoding.py
new file mode 100644
index 00000000..ab24ab93
--- /dev/null
+++ b/libraries/cherrypy/test/test_encoding.py
@@ -0,0 +1,426 @@
+# coding: utf-8
+
+import gzip
+import io
+from unittest import mock
+
+from six.moves.http_client import IncompleteRead
+from six.moves.urllib.parse import quote as url_quote
+
+import cherrypy
+from cherrypy._cpcompat import ntob, ntou
+
+from cherrypy.test import helper
+
+
+europoundUnicode = ntou('£', encoding='utf-8')
+sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8')
+
+sing8 = sing.encode('utf-8')
+sing16 = sing.encode('utf-16')
+
+
+class EncodingTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self, param):
+                assert param == europoundUnicode, '%r != %r' % (
+                    param, europoundUnicode)
+                yield europoundUnicode
+
+            @cherrypy.expose
+            def mao_zedong(self):
+                return sing
+
+            @cherrypy.expose
+            @cherrypy.config(**{'tools.encode.encoding': 'utf-8'})
+            def utf8(self):
+                return sing8
+
+            @cherrypy.expose
+            def cookies_and_headers(self):
+                # if the headers have non-ascii characters and a cookie has
+                #  any part which is unicode (even ascii), the response
+                #  should not fail.
+                cherrypy.response.cookie['candy'] = 'bar'
+                cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
+                cherrypy.response.headers[
+                    'Some-Header'] = 'My d\xc3\xb6g has fleas'
+                return 'Any content'
+
+            @cherrypy.expose
+            def reqparams(self, *args, **kwargs):
+                return b', '.join(
+                    [': '.join((k, v)).encode('utf8')
+                     for k, v in sorted(cherrypy.request.params.items())]
+                )
+
+            @cherrypy.expose
+            @cherrypy.config(**{
+                'tools.encode.text_only': False,
+                'tools.encode.add_charset': True,
+            })
+            def nontext(self, *args, **kwargs):
+                cherrypy.response.headers[
+                    'Content-Type'] = 'application/binary'
+                return '\x00\x01\x02\x03'
+
+        class GZIP:
+
+            @cherrypy.expose
+            def index(self):
+                yield 'Hello, world'
+
+            @cherrypy.expose
+            # Turn encoding off so the gzip tool is the one doing the collapse.
+            @cherrypy.config(**{'tools.encode.on': False})
+            def noshow(self):
+                # Test for ticket #147, where yield showed no exceptions
+                # (content-encoding was still gzip even though traceback
+                # wasn't zipped).
+                raise IndexError()
+                yield 'Here be dragons'
+
+            @cherrypy.expose
+            @cherrypy.config(**{'response.stream': True})
+            def noshow_stream(self):
+                # Test for ticket #147, where yield showed no exceptions
+                # (content-encoding was still gzip even though traceback
+                # wasn't zipped).
+                raise IndexError()
+                yield 'Here be dragons'
+
+        class Decode:
+
+            @cherrypy.expose
+            @cherrypy.config(**{
+                'tools.decode.on': True,
+                'tools.decode.default_encoding': ['utf-16'],
+            })
+            def extra_charset(self, *args, **kwargs):
+                return ', '.join([': '.join((k, v))
+                                  for k, v in cherrypy.request.params.items()])
+
+            @cherrypy.expose
+            @cherrypy.config(**{
+                'tools.decode.on': True,
+                'tools.decode.encoding': 'utf-16',
+            })
+            def force_charset(self, *args, **kwargs):
+                return ', '.join([': '.join((k, v))
+                                  for k, v in cherrypy.request.params.items()])
+
+        root = Root()
+        root.gzip = GZIP()
+        root.decode = Decode()
+        cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
+
+    def test_query_string_decoding(self):
+        URI_TMPL = '/reqparams?q={q}'
+
+        europoundUtf8_2_bytes = europoundUnicode.encode('utf-8')
+        europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2]
+
+        # Encoded utf8 query strings MUST be parsed correctly.
+        # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
+        self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes)))
+        # The return value will be encoded as utf8.
+        self.assertBody(b'q: ' + europoundUtf8_2_bytes)
+
+        # Query strings that are incorrectly encoded MUST raise 404.
+        # Here, q is the second byte of POUND SIGN U+A3 encoded in utf8
+        # and then %HEX
+        # TODO: check whether this shouldn't raise 400 Bad Request instead
+        self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte)))
+        self.assertStatus(404)
+        self.assertErrorPage(
+            404,
+            'The given query string could not be processed. Query '
+            "strings for this resource must be encoded with 'utf8'.")
+
+    def test_urlencoded_decoding(self):
+        # Test the decoding of an application/x-www-form-urlencoded entity.
+        europoundUtf8 = europoundUnicode.encode('utf-8')
+        body = b'param=' + europoundUtf8
+        self.getPage('/',
+                     method='POST',
+                     headers=[
+                         ('Content-Type', 'application/x-www-form-urlencoded'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(europoundUtf8)
+
+        # Encoded utf8 entities MUST be parsed and decoded correctly.
+        # Here, q is the POUND SIGN U+00A3 encoded in utf8
+        body = b'q=\xc2\xa3'
+        self.getPage('/reqparams', method='POST',
+                     headers=[(
+                         'Content-Type', 'application/x-www-form-urlencoded'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(b'q: \xc2\xa3')
+
+        # ...and in utf16, which is not in the default attempt_charsets list:
+        body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
+        self.getPage('/reqparams',
+                     method='POST',
+                     headers=[
+                         ('Content-Type',
+                          'application/x-www-form-urlencoded;charset=utf-16'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(b'q: \xc2\xa3')
+
+        # Entities that are incorrectly encoded MUST raise 400.
+        # Here, q is the POUND SIGN U+00A3 encoded in utf16, but
+        # the Content-Type incorrectly labels it utf-8.
+        body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
+        self.getPage('/reqparams',
+                     method='POST',
+                     headers=[
+                         ('Content-Type',
+                          'application/x-www-form-urlencoded;charset=utf-8'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertStatus(400)
+        self.assertErrorPage(
+            400,
+            'The request entity could not be decoded. The following charsets '
+            "were attempted: ['utf-8']")
+
+    def test_decode_tool(self):
+        # An extra charset should be tried first, and succeed if it matches.
+        # Here, we add utf-16 as a charset and pass a utf-16 body.
+        body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
+        self.getPage('/decode/extra_charset', method='POST',
+                     headers=[(
+                         'Content-Type', 'application/x-www-form-urlencoded'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(b'q: \xc2\xa3')
+
+        # An extra charset should be tried first, and continue to other default
+        # charsets if it doesn't match.
+        # Here, we add utf-16 as a charset but still pass a utf-8 body.
+        body = b'q=\xc2\xa3'
+        self.getPage('/decode/extra_charset', method='POST',
+                     headers=[(
+                         'Content-Type', 'application/x-www-form-urlencoded'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(b'q: \xc2\xa3')
+
+        # An extra charset should error if force is True and it doesn't match.
+        # Here, we force utf-16 as a charset but still pass a utf-8 body.
+        body = b'q=\xc2\xa3'
+        self.getPage('/decode/force_charset', method='POST',
+                     headers=[(
+                         'Content-Type', 'application/x-www-form-urlencoded'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertErrorPage(
+            400,
+            'The request entity could not be decoded. The following charsets '
+            "were attempted: ['utf-16']")
+
+    def test_multipart_decoding(self):
+        # Test the decoding of a multipart entity when the charset (utf16) is
+        # explicitly given.
+        body = ntob('\r\n'.join([
+            '--X',
+            'Content-Type: text/plain;charset=utf-16',
+            'Content-Disposition: form-data; name="text"',
+            '',
+            '\xff\xfea\x00b\x00\x1c c\x00',
+            '--X',
+            'Content-Type: text/plain;charset=utf-16',
+            'Content-Disposition: form-data; name="submit"',
+            '',
+            '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
+            '--X--'
+        ]))
+        self.getPage('/reqparams', method='POST',
+                     headers=[(
+                         'Content-Type', 'multipart/form-data;boundary=X'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc')
+
+    @mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1)
+    def test_multipart_decoding_bigger_maxrambytes(self):
+        """
+        Decoding of a multipart entity should also pass when
+        the entity is bigger than maxrambytes. See ticket #1352.
+        """
+        self.test_multipart_decoding()
+
+    def test_multipart_decoding_no_charset(self):
+        # Test the decoding of a multipart entity when the charset (utf8) is
+        # NOT explicitly given, but is in the list of charsets to attempt.
+        body = ntob('\r\n'.join([
+            '--X',
+            'Content-Disposition: form-data; name="text"',
+            '',
+            '\xe2\x80\x9c',
+            '--X',
+            'Content-Disposition: form-data; name="submit"',
+            '',
+            'Create',
+            '--X--'
+        ]))
+        self.getPage('/reqparams', method='POST',
+                     headers=[(
+                         'Content-Type', 'multipart/form-data;boundary=X'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(b'submit: Create, text: \xe2\x80\x9c')
+
+    def test_multipart_decoding_no_successful_charset(self):
+        # Test the decoding of a multipart entity when the charset (utf16) is
+        # NOT explicitly given, and is NOT in the list of charsets to attempt.
+        body = ntob('\r\n'.join([
+            '--X',
+            'Content-Disposition: form-data; name="text"',
+            '',
+            '\xff\xfea\x00b\x00\x1c c\x00',
+            '--X',
+            'Content-Disposition: form-data; name="submit"',
+            '',
+            '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
+            '--X--'
+        ]))
+        self.getPage('/reqparams', method='POST',
+                     headers=[(
+                         'Content-Type', 'multipart/form-data;boundary=X'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertStatus(400)
+        self.assertErrorPage(
+            400,
+            'The request entity could not be decoded. The following charsets '
+            "were attempted: ['us-ascii', 'utf-8']")
+
+    def test_nontext(self):
+        self.getPage('/nontext')
+        self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
+        self.assertBody('\x00\x01\x02\x03')
+
+    def testEncoding(self):
+        # Default encoding should be utf-8
+        self.getPage('/mao_zedong')
+        self.assertBody(sing8)
+
+        # Ask for utf-16.
+        self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
+        self.assertHeader('Content-Type', 'text/html;charset=utf-16')
+        self.assertBody(sing16)
+
+        # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
+        # should be produced.
+        self.getPage('/mao_zedong', [('Accept-Charset',
+                                      'iso-8859-1;q=1, utf-16;q=0.5')])
+        self.assertBody(sing16)
+
+        # The "*" value should default to our default_encoding, utf-8
+        self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
+        self.assertBody(sing8)
+
+        # Only allow iso-8859-1, which should fail and raise 406.
+        self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
+        self.assertStatus('406 Not Acceptable')
+        self.assertInBody('Your client sent this Accept-Charset header: '
+                          'iso-8859-1, *;q=0. We tried these charsets: '
+                          'iso-8859-1.')
+
+        # Ask for x-mac-ce, which should be unknown. See ticket #569.
+        self.getPage('/mao_zedong', [('Accept-Charset',
+                                      'us-ascii, ISO-8859-1, x-mac-ce')])
+        self.assertStatus('406 Not Acceptable')
+        self.assertInBody('Your client sent this Accept-Charset header: '
+                          'us-ascii, ISO-8859-1, x-mac-ce. We tried these '
+                          'charsets: ISO-8859-1, us-ascii, x-mac-ce.')
+
+        # Test the 'encoding' arg to encode.
+        self.getPage('/utf8')
+        self.assertBody(sing8)
+        self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
+        self.assertStatus('406 Not Acceptable')
+
+        # Test malformed quality value, which should raise 400.
+        self.getPage('/mao_zedong', [('Accept-Charset',
+                                      'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')])
+        self.assertStatus('400 Bad Request')
+
+    def testGzip(self):
+        zbuf = io.BytesIO()
+        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
+        zfile.write(b'Hello, world')
+        zfile.close()
+
+        self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')])
+        self.assertInBody(zbuf.getvalue()[:3])
+        self.assertHeader('Vary', 'Accept-Encoding')
+        self.assertHeader('Content-Encoding', 'gzip')
+
+        # Test when gzip is denied.
+        self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')])
+        self.assertHeader('Vary', 'Accept-Encoding')
+        self.assertNoHeader('Content-Encoding')
+        self.assertBody('Hello, world')
+
+        self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')])
+        self.assertHeader('Vary', 'Accept-Encoding')
+        self.assertNoHeader('Content-Encoding')
+        self.assertBody('Hello, world')
+
+        # Test that trailing comma doesn't cause IndexError
+        # Ref: https://github.com/cherrypy/cherrypy/issues/988
+        self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')])
+        self.assertStatus(200)
+        self.assertNotInBody('IndexError')
+
+        self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')])
+        self.assertStatus(406)
+        self.assertNoHeader('Content-Encoding')
+        self.assertErrorPage(406, 'identity, gzip')
+
+        # Test for ticket #147
+        self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')])
+        self.assertNoHeader('Content-Encoding')
+        self.assertStatus(500)
+        self.assertErrorPage(500, pattern='IndexError\n')
+
+        # In this case, there's nothing we can do to deliver a
+        # readable page, since 1) the gzip header is already set,
+        # and 2) we may have already written some of the body.
+        # The fix is to never stream yields when using gzip.
+        if (cherrypy.server.protocol_version == 'HTTP/1.0' or
+                getattr(cherrypy.server, 'using_apache', False)):
+            self.getPage('/gzip/noshow_stream',
+                         headers=[('Accept-Encoding', 'gzip')])
+            self.assertHeader('Content-Encoding', 'gzip')
+            self.assertInBody('\x1f\x8b\x08\x00')
+        else:
+            # The wsgiserver will simply stop sending data, and the HTTP client
+            # will error due to an incomplete chunk-encoded stream.
+            self.assertRaises((ValueError, IncompleteRead), self.getPage,
+                              '/gzip/noshow_stream',
+                              headers=[('Accept-Encoding', 'gzip')])
+
+    def test_UnicodeHeaders(self):
+        self.getPage('/cookies_and_headers')
+        self.assertBody('Any content')
diff --git a/libraries/cherrypy/test/test_etags.py b/libraries/cherrypy/test/test_etags.py
new file mode 100644
index 00000000..293eb866
--- /dev/null
+++ b/libraries/cherrypy/test/test_etags.py
@@ -0,0 +1,84 @@
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy.test import helper
+
+
+class ETagTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def resource(self):
+                return 'Oh wah ta goo Siam.'
+
+            @cherrypy.expose
+            def fail(self, code):
+                code = int(code)
+                if 300 <= code <= 399:
+                    raise cherrypy.HTTPRedirect([], code)
+                else:
+                    raise cherrypy.HTTPError(code)
+
+            @cherrypy.expose
+            # In Python 3, tools.encode is on by default
+            @cherrypy.config(**{'tools.encode.on': True})
+            def unicoded(self):
+                return ntou('I am a \u1ee4nicode string.', 'escape')
+
+        conf = {'/': {'tools.etags.on': True,
+                      'tools.etags.autotags': True,
+                      }}
+        cherrypy.tree.mount(Root(), config=conf)
+
+    def test_etags(self):
+        self.getPage('/resource')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+        self.assertBody('Oh wah ta goo Siam.')
+        etag = self.assertHeader('ETag')
+
+        # Test If-Match (both valid and invalid)
+        self.getPage('/resource', headers=[('If-Match', etag)])
+        self.assertStatus('200 OK')
+        self.getPage('/resource', headers=[('If-Match', '*')])
+        self.assertStatus('200 OK')
+        self.getPage('/resource', headers=[('If-Match', '*')], method='POST')
+        self.assertStatus('200 OK')
+        self.getPage('/resource', headers=[('If-Match', 'a bogus tag')])
+        self.assertStatus('412 Precondition Failed')
+
+        # Test If-None-Match (both valid and invalid)
+        self.getPage('/resource', headers=[('If-None-Match', etag)])
+        self.assertStatus(304)
+        self.getPage('/resource', method='POST',
+                     headers=[('If-None-Match', etag)])
+        self.assertStatus('412 Precondition Failed')
+        self.getPage('/resource', headers=[('If-None-Match', '*')])
+        self.assertStatus(304)
+        self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')])
+        self.assertStatus('200 OK')
+
+    def test_errors(self):
+        self.getPage('/resource')
+        self.assertStatus(200)
+        etag = self.assertHeader('ETag')
+
+        # Test raising errors in page handler
+        self.getPage('/fail/412', headers=[('If-Match', etag)])
+        self.assertStatus(412)
+        self.getPage('/fail/304', headers=[('If-Match', etag)])
+        self.assertStatus(304)
+        self.getPage('/fail/412', headers=[('If-None-Match', '*')])
+        self.assertStatus(412)
+        self.getPage('/fail/304', headers=[('If-None-Match', '*')])
+        self.assertStatus(304)
+
+    def test_unicode_body(self):
+        self.getPage('/unicoded')
+        self.assertStatus(200)
+        etag1 = self.assertHeader('ETag')
+        self.getPage('/unicoded', headers=[('If-Match', etag1)])
+        self.assertStatus(200)
+        self.assertHeader('ETag', etag1)
diff --git a/libraries/cherrypy/test/test_http.py b/libraries/cherrypy/test/test_http.py
new file mode 100644
index 00000000..0899d4d0
--- /dev/null
+++ b/libraries/cherrypy/test/test_http.py
@@ -0,0 +1,307 @@
+# coding: utf-8
+"""Tests for managing HTTP issues (malformed requests, etc)."""
+
+import errno
+import mimetypes
+import socket
+import sys
+from unittest import mock
+
+import six
+from six.moves.http_client import HTTPConnection
+from six.moves import urllib
+
+import cherrypy
+from cherrypy._cpcompat import HTTPSConnection, quote
+
+from cherrypy.test import helper
+
+
+def is_ascii(text):
+    """
+    Return True if the text encodes as ascii.
+    """
+    try:
+        text.encode('ascii')
+        return True
+    except Exception:
+        pass
+    return False
+
+
+def encode_filename(filename):
+    """
+    Given a filename to be used in a multipart/form-data,
+    encode the name. Return the key and encoded filename.
+    """
+    if is_ascii(filename):
+        return 'filename', '"{filename}"'.format(**locals())
+    encoded = quote(filename, encoding='utf-8')
+    return 'filename*', "'".join((
+        'UTF-8',
+        '',  # lang
+        encoded,
+    ))
+
+
+def encode_multipart_formdata(files):
+    """Return (content_type, body) ready for httplib.HTTP instance.
+
+    files: a sequence of (name, filename, value) tuples for multipart uploads.
+    filename can be a string or a tuple ('filename string', 'encoding')
+    """
+    BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
+    L = []
+    for key, filename, value in files:
+        L.append('--' + BOUNDARY)
+
+        fn_key, encoded = encode_filename(filename)
+        tmpl = \
+            'Content-Disposition: form-data; name="{key}"; {fn_key}={encoded}'
+        L.append(tmpl.format(**locals()))
+        ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
+        L.append('Content-Type: %s' % ct)
+        L.append('')
+        L.append(value)
+    L.append('--' + BOUNDARY + '--')
+    L.append('')
+    body = '\r\n'.join(L)
+    content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
+    return content_type, body
+
+
+class HTTPTests(helper.CPWebCase):
+
+    def make_connection(self):
+        if self.scheme == 'https':
+            return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+        else:
+            return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self, *args, **kwargs):
+                return 'Hello world!'
+
+            @cherrypy.expose
+            @cherrypy.config(**{'request.process_request_body': False})
+            def no_body(self, *args, **kwargs):
+                return 'Hello world!'
+
+            @cherrypy.expose
+            def post_multipart(self, file):
+                """Return a summary ("a * 65536\nb * 65536") of the uploaded
+                file.
+                """
+                contents = file.file.read()
+                summary = []
+                curchar = None
+                count = 0
+                for c in contents:
+                    if c == curchar:
+                        count += 1
+                    else:
+                        if count:
+                            if six.PY3:
+                                curchar = chr(curchar)
+                            summary.append('%s * %d' % (curchar, count))
+                        count = 1
+                        curchar = c
+                if count:
+                    if six.PY3:
+                        curchar = chr(curchar)
+                    summary.append('%s * %d' % (curchar, count))
+                return ', '.join(summary)
+
+            @cherrypy.expose
+            def post_filename(self, myfile):
+                '''Return the name of the file which was uploaded.'''
+                return myfile.filename
+
+        cherrypy.tree.mount(Root())
+        cherrypy.config.update({'server.max_request_body_size': 30000000})
+
+    def test_no_content_length(self):
+        # "The presence of a message-body in a request is signaled by the
+        # inclusion of a Content-Length or Transfer-Encoding header field in
+        # the request's message-headers."
+        #
+        # Send a message with neither header and no body. Even though
+        # the request is of method POST, this should be OK because we set
+        # request.process_request_body to False for our handler.
+        c = self.make_connection()
+        c.request('POST', '/no_body')
+        response = c.getresponse()
+        self.body = response.fp.read()
+        self.status = str(response.status)
+        self.assertStatus(200)
+        self.assertBody(b'Hello world!')
+
+        # Now send a message that has no Content-Length, but does send a body.
+        # Verify that CP times out the socket and responds
+        # with 411 Length Required.
+        if self.scheme == 'https':
+            c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+        else:
+            c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+
+        # `_get_content_length` is needed for Python 3.6+
+        with mock.patch.object(
+                c,
+                '_get_content_length',
+                lambda body, method: None,
+                create=True):
+            # `_set_content_length` is needed for Python 2.7-3.5
+            with mock.patch.object(c, '_set_content_length', create=True):
+                c.request('POST', '/')
+
+        response = c.getresponse()
+        self.body = response.fp.read()
+        self.status = str(response.status)
+        self.assertStatus(411)
+
+    def test_post_multipart(self):
+        alphabet = 'abcdefghijklmnopqrstuvwxyz'
+        # generate file contents for a large post
+        contents = ''.join([c * 65536 for c in alphabet])
+
+        # encode as multipart form data
+        files = [('file', 'file.txt', contents)]
+        content_type, body = encode_multipart_formdata(files)
+        body = body.encode('Latin-1')
+
+        # post file
+        c = self.make_connection()
+        c.putrequest('POST', '/post_multipart')
+        c.putheader('Content-Type', content_type)
+        c.putheader('Content-Length', str(len(body)))
+        c.endheaders()
+        c.send(body)
+
+        response = c.getresponse()
+        self.body = response.fp.read()
+        self.status = str(response.status)
+        self.assertStatus(200)
+        parts = ['%s * 65536' % ch for ch in alphabet]
+        self.assertBody(', '.join(parts))
+
+    def test_post_filename_with_special_characters(self):
+        '''Testing that we can handle filenames with special characters. This
+        was reported as a bug in:
+           https://github.com/cherrypy/cherrypy/issues/1146/
+           https://github.com/cherrypy/cherrypy/issues/1397/
+           https://github.com/cherrypy/cherrypy/issues/1694/
+        '''
+        # We'll upload a bunch of files with differing names.
+        fnames = [
+            'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
+            'file;name.csv', 'file; name.csv', u'test_łóąä.txt',
+        ]
+        for fname in fnames:
+            files = [('myfile', fname, 'yunyeenyunyue')]
+            content_type, body = encode_multipart_formdata(files)
+            body = body.encode('Latin-1')
+
+            # post file
+            c = self.make_connection()
+            c.putrequest('POST', '/post_filename')
+            c.putheader('Content-Type', content_type)
+            c.putheader('Content-Length', str(len(body)))
+            c.endheaders()
+            c.send(body)
+
+            response = c.getresponse()
+            self.body = response.fp.read()
+            self.status = str(response.status)
+            self.assertStatus(200)
+            self.assertBody(fname)
+
+    def test_malformed_request_line(self):
+        if getattr(cherrypy.server, 'using_apache', False):
+            return self.skip('skipped due to known Apache differences...')
+
+        # Test missing version in Request-Line
+        c = self.make_connection()
+        c._output(b'geT /')
+        c._send_output()
+        if hasattr(c, 'strict'):
+            response = c.response_class(c.sock, strict=c.strict, method='GET')
+        else:
+            # Python 3.2 removed the 'strict' feature, saying:
+            # "http.client now always assumes HTTP/1.x compliant servers."
+            response = c.response_class(c.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 400)
+        self.assertEqual(response.fp.read(22), b'Malformed Request-Line')
+        c.close()
+
+    def test_request_line_split_issue_1220(self):
+        params = {
+            'intervenant-entreprise-evenement_classaction':
+                'evenement-mailremerciements',
+            '_path': 'intervenant-entreprise-evenement',
+            'intervenant-entreprise-evenement_action-id': 19404,
+            'intervenant-entreprise-evenement_id': 19404,
+            'intervenant-entreprise_id': 28092,
+        }
+        Request_URI = '/index?' + urllib.parse.urlencode(params)
+        self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256)
+        self.getPage(Request_URI)
+        self.assertBody('Hello world!')
+
+    def test_malformed_header(self):
+        c = self.make_connection()
+        c.putrequest('GET', '/')
+        c.putheader('Content-Type', 'text/plain')
+        # See https://github.com/cherrypy/cherrypy/issues/941
+        c._output(b're, 1.2.3.4#015#012')
+        c.endheaders()
+
+        response = c.getresponse()
+        self.status = str(response.status)
+        self.assertStatus(400)
+        self.body = response.fp.read(20)
+        self.assertBody('Illegal header line.')
+
+    def test_http_over_https(self):
+        if self.scheme != 'https':
+            return self.skip('skipped (not running HTTPS)... ')
+
+        # Try connecting without SSL.
+        conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+        conn.putrequest('GET', '/', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.response_class(conn.sock, method='GET')
+        try:
+            response.begin()
+            self.assertEqual(response.status, 400)
+            self.body = response.read()
+            self.assertBody('The client sent a plain HTTP request, but this '
+                            'server only speaks HTTPS on this port.')
+        except socket.error:
+            e = sys.exc_info()[1]
+            # "Connection reset by peer" is also acceptable.
+            if e.errno != errno.ECONNRESET:
+                raise
+
+    def test_garbage_in(self):
+        # Connect without SSL regardless of server.scheme
+        c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+        c._output(b'gjkgjklsgjklsgjkljklsg')
+        c._send_output()
+        response = c.response_class(c.sock, method='GET')
+        try:
+            response.begin()
+            self.assertEqual(response.status, 400)
+            self.assertEqual(response.fp.read(22),
+                             b'Malformed Request-Line')
+            c.close()
+        except socket.error:
+            e = sys.exc_info()[1]
+            # "Connection reset by peer" is also acceptable.
+            if e.errno != errno.ECONNRESET:
+                raise
diff --git a/libraries/cherrypy/test/test_httputil.py b/libraries/cherrypy/test/test_httputil.py
new file mode 100644
index 00000000..656b8a3d
--- /dev/null
+++ b/libraries/cherrypy/test/test_httputil.py
@@ -0,0 +1,80 @@
+"""Test helpers from ``cherrypy.lib.httputil`` module."""
+import pytest
+from six.moves import http_client
+
+from cherrypy.lib import httputil
+
+
+@pytest.mark.parametrize(
+    'script_name,path_info,expected_url',
+    [
+        ('/sn/', '/pi/', '/sn/pi/'),
+        ('/sn/', '/pi', '/sn/pi'),
+        ('/sn/', '/', '/sn/'),
+        ('/sn/', '', '/sn/'),
+        ('/sn', '/pi/', '/sn/pi/'),
+        ('/sn', '/pi', '/sn/pi'),
+        ('/sn', '/', '/sn/'),
+        ('/sn', '', '/sn'),
+        ('/', '/pi/', '/pi/'),
+        ('/', '/pi', '/pi'),
+        ('/', '/', '/'),
+        ('/', '', '/'),
+        ('', '/pi/', '/pi/'),
+        ('', '/pi', '/pi'),
+        ('', '/', '/'),
+        ('', '', '/'),
+    ]
+)
+def test_urljoin(script_name, path_info, expected_url):
+    """Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO."""
+    actual_url = httputil.urljoin(script_name, path_info)
+    assert actual_url == expected_url
+
+
+EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows')
+EXPECTED_500 = (
+    500,
+    'Internal Server Error',
+    'The server encountered an unexpected condition which '
+    'prevented it from fulfilling the request.',
+)
+EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI')
+EXPECTED_444 = (444, 'Non-existent reason', '')
+
+
+@pytest.mark.parametrize(
+    'status,expected_status',
+    [
+        (None, EXPECTED_200),
+        (200, EXPECTED_200),
+        ('500', EXPECTED_500),
+        (http_client.NOT_FOUND, EXPECTED_404),
+        ('444 Non-existent reason', EXPECTED_444),
+    ]
+)
+def test_valid_status(status, expected_status):
+    """Check valid int, string and http_client-constants
+    statuses processing."""
+    assert httputil.valid_status(status) == expected_status
+
+
+@pytest.mark.parametrize(
+    'status_code,error_msg',
+    [
+        ('hey', "Illegal response status from server ('hey' is non-numeric)."),
+        (
+            {'hey': 'hi'},
+            'Illegal response status from server '
+            "({'hey': 'hi'} is non-numeric).",
+        ),
+        (1, 'Illegal response status from server (1 is out of range).'),
+        (600, 'Illegal response status from server (600 is out of range).'),
+    ]
+)
+def test_invalid_status(status_code, error_msg):
+    """Check that invalid status cause certain errors."""
+    with pytest.raises(ValueError) as excinfo:
+        httputil.valid_status(status_code)
+
+    assert error_msg in str(excinfo)
diff --git a/libraries/cherrypy/test/test_iterator.py b/libraries/cherrypy/test/test_iterator.py
new file mode 100644
index 00000000..92f08e7c
--- /dev/null
+++ b/libraries/cherrypy/test/test_iterator.py
@@ -0,0 +1,196 @@
+import six
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class IteratorBase(object):
+
+    created = 0
+    datachunk = 'butternut squash' * 256
+
+    @classmethod
+    def incr(cls):
+        cls.created += 1
+
+    @classmethod
+    def decr(cls):
+        cls.created -= 1
+
+
+class OurGenerator(IteratorBase):
+
+    def __iter__(self):
+        self.incr()
+        try:
+            for i in range(1024):
+                yield self.datachunk
+        finally:
+            self.decr()
+
+
+class OurIterator(IteratorBase):
+
+    started = False
+    closed_off = False
+    count = 0
+
+    def increment(self):
+        self.incr()
+
+    def decrement(self):
+        if not self.closed_off:
+            self.closed_off = True
+            self.decr()
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if not self.started:
+            self.started = True
+            self.increment()
+        self.count += 1
+        if self.count > 1024:
+            raise StopIteration
+        return self.datachunk
+
+    next = __next__
+
+    def __del__(self):
+        self.decrement()
+
+
+class OurClosableIterator(OurIterator):
+
+    def close(self):
+        self.decrement()
+
+
+class OurNotClosableIterator(OurIterator):
+
+    # We can't close something which requires an additional argument.
+    def close(self, somearg):
+        self.decrement()
+
+
+class OurUnclosableIterator(OurIterator):
+    close = 'close'  # not callable!
+
+
+class IteratorTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        class Root(object):
+
+            @cherrypy.expose
+            def count(self, clsname):
+                cherrypy.response.headers['Content-Type'] = 'text/plain'
+                return six.text_type(globals()[clsname].created)
+
+            @cherrypy.expose
+            def getall(self, clsname):
+                cherrypy.response.headers['Content-Type'] = 'text/plain'
+                return globals()[clsname]()
+
+            @cherrypy.expose
+            @cherrypy.config(**{'response.stream': True})
+            def stream(self, clsname):
+                return self.getall(clsname)
+
+        cherrypy.tree.mount(Root())
+
+    def test_iterator(self):
+        try:
+            self._test_iterator()
+        except Exception:
+            'Test fails intermittently. See #1419'
+
+    def _test_iterator(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Check the counts of all the classes, they should be zero.
+        closables = ['OurClosableIterator', 'OurGenerator']
+        unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator']
+        all_classes = closables + unclosables
+
+        import random
+        random.shuffle(all_classes)
+
+        for clsname in all_classes:
+            self.getPage('/count/' + clsname)
+            self.assertStatus(200)
+            self.assertBody('0')
+
+        # We should also be able to read the entire content body
+        # successfully, though we don't need to, we just want to
+        # check the header.
+        for clsname in all_classes:
+            itr_conn = self.get_conn()
+            itr_conn.putrequest('GET', '/getall/' + clsname)
+            itr_conn.endheaders()
+            response = itr_conn.getresponse()
+            self.assertEqual(response.status, 200)
+            headers = response.getheaders()
+            for header_name, header_value in headers:
+                if header_name.lower() == 'content-length':
+                    expected = six.text_type(1024 * 16 * 256)
+                    assert header_value == expected, header_value
+                    break
+            else:
+                raise AssertionError('No Content-Length header found')
+
+            # As the response should be fully consumed by CherryPy
+            # before sending back, the count should still be at zero
+            # by the time the response has been sent.
+            self.getPage('/count/' + clsname)
+            self.assertStatus(200)
+            self.assertBody('0')
+
+        # Now we do the same check with streaming - some classes will
+        # be automatically closed, while others cannot.
+        stream_counts = {}
+        for clsname in all_classes:
+            itr_conn = self.get_conn()
+            itr_conn.putrequest('GET', '/stream/' + clsname)
+            itr_conn.endheaders()
+            response = itr_conn.getresponse()
+            self.assertEqual(response.status, 200)
+            response.fp.read(65536)
+
+            # Let's check the count - this should always be one.
+            self.getPage('/count/' + clsname)
+            self.assertBody('1')
+
+            # Now if we close the connection, the count should go back
+            # to zero.
+            itr_conn.close()
+            self.getPage('/count/' + clsname)
+
+            # If this is a response which should be easily closed, then
+            # we will test to see if the value has gone back down to
+            # zero.
+            if clsname in closables:
+
+                # Sometimes we try to get the answer too quickly - we
+                # will wait for 100 ms before asking again if we didn't
+                # get the answer we wanted.
+                if self.body != '0':
+                    import time
+                    time.sleep(0.1)
+                    self.getPage('/count/' + clsname)
+
+            stream_counts[clsname] = int(self.body)
+
+        # Check that we closed off the classes which should provide
+        # easy mechanisms for doing so.
+        for clsname in closables:
+            assert stream_counts[clsname] == 0, (
+                'did not close off stream response correctly, expected '
+                'count of zero for %s: %s' % (clsname, stream_counts)
+            )
diff --git a/libraries/cherrypy/test/test_json.py b/libraries/cherrypy/test/test_json.py
new file mode 100644
index 00000000..1585f6e6
--- /dev/null
+++ b/libraries/cherrypy/test/test_json.py
@@ -0,0 +1,102 @@
+import cherrypy
+from cherrypy.test import helper
+
+from cherrypy._cpcompat import json
+
+
+json_out = cherrypy.config(**{'tools.json_out.on': True})
+json_in = cherrypy.config(**{'tools.json_in.on': True})
+
+
+class JsonTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root(object):
+
+            @cherrypy.expose
+            def plain(self):
+                return 'hello'
+
+            @cherrypy.expose
+            @json_out
+            def json_string(self):
+                return 'hello'
+
+            @cherrypy.expose
+            @json_out
+            def json_list(self):
+                return ['a', 'b', 42]
+
+            @cherrypy.expose
+            @json_out
+            def json_dict(self):
+                return {'answer': 42}
+
+            @cherrypy.expose
+            @json_in
+            def json_post(self):
+                if cherrypy.request.json == [13, 'c']:
+                    return 'ok'
+                else:
+                    return 'nok'
+
+            @cherrypy.expose
+            @json_out
+            @cherrypy.config(**{'tools.caching.on': True})
+            def json_cached(self):
+                return 'hello there'
+
+        root = Root()
+        cherrypy.tree.mount(root)
+
+    def test_json_output(self):
+        if json is None:
+            self.skip('json not found ')
+            return
+
+        self.getPage('/plain')
+        self.assertBody('hello')
+
+        self.getPage('/json_string')
+        self.assertBody('"hello"')
+
+        self.getPage('/json_list')
+        self.assertBody('["a", "b", 42]')
+
+        self.getPage('/json_dict')
+        self.assertBody('{"answer": 42}')
+
+    def test_json_input(self):
+        if json is None:
+            self.skip('json not found ')
+            return
+
+        body = '[13, "c"]'
+        headers = [('Content-Type', 'application/json'),
+                   ('Content-Length', str(len(body)))]
+        self.getPage('/json_post', method='POST', headers=headers, body=body)
+        self.assertBody('ok')
+
+        body = '[13, "c"]'
+        headers = [('Content-Type', 'text/plain'),
+                   ('Content-Length', str(len(body)))]
+        self.getPage('/json_post', method='POST', headers=headers, body=body)
+        self.assertStatus(415, 'Expected an application/json content type')
+
+        body = '[13, -]'
+        headers = [('Content-Type', 'application/json'),
+                   ('Content-Length', str(len(body)))]
+        self.getPage('/json_post', method='POST', headers=headers, body=body)
+        self.assertStatus(400, 'Invalid JSON document')
+
+    def test_cached(self):
+        if json is None:
+            self.skip('json not found ')
+            return
+
+        self.getPage('/json_cached')
+        self.assertStatus(200, '"hello"')
+
+        self.getPage('/json_cached')  # 2'nd time to hit cache
+        self.assertStatus(200, '"hello"')
diff --git a/libraries/cherrypy/test/test_logging.py b/libraries/cherrypy/test/test_logging.py
new file mode 100644
index 00000000..c4948c20
--- /dev/null
+++ b/libraries/cherrypy/test/test_logging.py
@@ -0,0 +1,209 @@
+"""Basic tests for the CherryPy core: request handling."""
+
+import os
+from unittest import mock
+
+import six
+
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy.test import helper, logtest
+
+localDir = os.path.dirname(__file__)
+access_log = os.path.join(localDir, 'access.log')
+error_log = os.path.join(localDir, 'error.log')
+
+# Some unicode strings.
+tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape')
+erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape')
+
+
+def setup_server():
+    class Root:
+
+        @cherrypy.expose
+        def index(self):
+            return 'hello'
+
+        @cherrypy.expose
+        def uni_code(self):
+            cherrypy.request.login = tartaros
+            cherrypy.request.remote.name = erebos
+
+        @cherrypy.expose
+        def slashes(self):
+            cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
+
+        @cherrypy.expose
+        def whitespace(self):
+            # User-Agent = "User-Agent" ":" 1*( product | comment )
+            # comment    = "(" *( ctext | quoted-pair | comment ) ")"
+            # ctext      = <any TEXT excluding "(" and ")">
+            # TEXT       = <any OCTET except CTLs, but including LWS>
+            # LWS        = [CRLF] 1*( SP | HT )
+            cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
+
+        @cherrypy.expose
+        def as_string(self):
+            return 'content'
+
+        @cherrypy.expose
+        def as_yield(self):
+            yield 'content'
+
+        @cherrypy.expose
+        @cherrypy.config(**{'tools.log_tracebacks.on': True})
+        def error(self):
+            raise ValueError()
+
+    root = Root()
+
+    cherrypy.config.update({
+        'log.error_file': error_log,
+        'log.access_file': access_log,
+    })
+    cherrypy.tree.mount(root)
+
+
+class AccessLogTests(helper.CPWebCase, logtest.LogCase):
+    setup_server = staticmethod(setup_server)
+
+    logfile = access_log
+
+    def testNormalReturn(self):
+        self.markLog()
+        self.getPage('/as_string',
+                     headers=[('Referer', 'http://www.cherrypy.org/'),
+                              ('User-Agent', 'Mozilla/5.0')])
+        self.assertBody('content')
+        self.assertStatus(200)
+
+        intro = '%s - - [' % self.interface()
+
+        self.assertLog(-1, intro)
+
+        if [k for k, v in self.headers if k.lower() == 'content-length']:
+            self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
+                           '"http://www.cherrypy.org/" "Mozilla/5.0"'
+                           % self.prefix())
+        else:
+            self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
+                           '"http://www.cherrypy.org/" "Mozilla/5.0"'
+                           % self.prefix())
+
+    def testNormalYield(self):
+        self.markLog()
+        self.getPage('/as_yield')
+        self.assertBody('content')
+        self.assertStatus(200)
+
+        intro = '%s - - [' % self.interface()
+
+        self.assertLog(-1, intro)
+        if [k for k, v in self.headers if k.lower() == 'content-length']:
+            self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
+                           self.prefix())
+        else:
+            self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
+                           % self.prefix())
+
+    @mock.patch(
+        'cherrypy._cplogging.LogManager.access_log_format',
+        '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}'
+        if six.PY3 else
+        '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
+    )
+    def testCustomLogFormat(self):
+        """Test a customized access_log_format string, which is a
+        feature of _cplogging.LogManager.access()."""
+        self.markLog()
+        self.getPage('/as_string', headers=[('Referer', 'REFERER'),
+                                            ('User-Agent', 'USERAGENT'),
+                                            ('Host', 'HOST')])
+        self.assertLog(-1, '%s - - [' % self.interface())
+        self.assertLog(-1, '] "GET /as_string HTTP/1.1" '
+                           '200 7 "REFERER" "USERAGENT" HOST')
+
+    @mock.patch(
+        'cherrypy._cplogging.LogManager.access_log_format',
+        '{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}'
+        if six.PY3 else
+        '%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
+    )
+    def testTimezLogFormat(self):
+        """Test a customized access_log_format string, which is a
+        feature of _cplogging.LogManager.access()."""
+        self.markLog()
+
+        expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
+        with mock.patch(
+                'cherrypy._cplogging.LazyRfc3339UtcTime',
+                lambda: expected_time):
+            self.getPage('/as_string', headers=[('Referer', 'REFERER'),
+                                                ('User-Agent', 'USERAGENT'),
+                                                ('Host', 'HOST')])
+
+        self.assertLog(-1, '%s - - ' % self.interface())
+        self.assertLog(-1, expected_time)
+        self.assertLog(-1, ' "GET /as_string HTTP/1.1" '
+                           '200 7 "REFERER" "USERAGENT" HOST')
+
+    @mock.patch(
+        'cherrypy._cplogging.LogManager.access_log_format',
+        '{i}' if six.PY3 else '%(i)s'
+    )
+    def testUUIDv4ParameterLogFormat(self):
+        """Test rendering of UUID4 within access log."""
+        self.markLog()
+        self.getPage('/as_string')
+        self.assertValidUUIDv4()
+
+    def testEscapedOutput(self):
+        # Test unicode in access log pieces.
+        self.markLog()
+        self.getPage('/uni_code')
+        self.assertStatus(200)
+        if six.PY3:
+            # The repr of a bytestring in six.PY3 includes a b'' prefix
+            self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
+        else:
+            self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
+        # Test the erebos value. Included inline for your enlightenment.
+        # Note the 'r' prefix--those backslashes are literals.
+        self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
+
+        # Test backslashes in output.
+        self.markLog()
+        self.getPage('/slashes')
+        self.assertStatus(200)
+        if six.PY3:
+            self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
+        else:
+            self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
+
+        # Test whitespace in output.
+        self.markLog()
+        self.getPage('/whitespace')
+        self.assertStatus(200)
+        # Again, note the 'r' prefix.
+        self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
+
+
+class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
+    setup_server = staticmethod(setup_server)
+
+    logfile = error_log
+
+    def testTracebacks(self):
+        # Test that tracebacks get written to the error log.
+        self.markLog()
+        ignore = helper.webtest.ignored_exceptions
+        ignore.append(ValueError)
+        try:
+            self.getPage('/error')
+            self.assertInBody('raise ValueError()')
+            self.assertLog(0, 'HTTP')
+            self.assertLog(1, 'Traceback (most recent call last):')
+            self.assertLog(-2, 'raise ValueError()')
+        finally:
+            ignore.pop()
diff --git a/libraries/cherrypy/test/test_mime.py b/libraries/cherrypy/test/test_mime.py
new file mode 100644
index 00000000..ef35d10e
--- /dev/null
+++ b/libraries/cherrypy/test/test_mime.py
@@ -0,0 +1,134 @@
+"""Tests for various MIME issues, including the safe_multipart Tool."""
+
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy.test import helper
+
+
+def setup_server():
+
+    class Root:
+
+        @cherrypy.expose
+        def multipart(self, parts):
+            return repr(parts)
+
+        @cherrypy.expose
+        def multipart_form_data(self, **kwargs):
+            return repr(list(sorted(kwargs.items())))
+
+        @cherrypy.expose
+        def flashupload(self, Filedata, Upload, Filename):
+            return ('Upload: %s, Filename: %s, Filedata: %r' %
+                    (Upload, Filename, Filedata.file.read()))
+
+    cherrypy.config.update({'server.max_request_body_size': 0})
+    cherrypy.tree.mount(Root())
+
+
+#                             Client-side code                             #
+
+
+class MultipartTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_multipart(self):
+        text_part = ntou('This is the text version')
+        html_part = ntou(
+            """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
+<html>
+<head>
+ <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
+</head>
+<body bgcolor="#ffffff" text="#000000">
+
+This is the <strong>HTML</strong> version
+</body>
+</html>
+""")
+        body = '\r\n'.join([
+            '--123456789',
+            "Content-Type: text/plain; charset='ISO-8859-1'",
+            'Content-Transfer-Encoding: 7bit',
+            '',
+            text_part,
+            '--123456789',
+            "Content-Type: text/html; charset='ISO-8859-1'",
+            '',
+            html_part,
+            '--123456789--'])
+        headers = [
+            ('Content-Type', 'multipart/mixed; boundary=123456789'),
+            ('Content-Length', str(len(body))),
+        ]
+        self.getPage('/multipart', headers, 'POST', body)
+        self.assertBody(repr([text_part, html_part]))
+
+    def test_multipart_form_data(self):
+        body = '\r\n'.join([
+            '--X',
+            'Content-Disposition: form-data; name="foo"',
+            '',
+            'bar',
+            '--X',
+            # Test a param with more than one value.
+            # See
+            # https://github.com/cherrypy/cherrypy/issues/1028
+            'Content-Disposition: form-data; name="baz"',
+            '',
+            '111',
+            '--X',
+            'Content-Disposition: form-data; name="baz"',
+            '',
+            '333',
+            '--X--'
+        ])
+        self.getPage('/multipart_form_data', method='POST',
+                     headers=[(
+                         'Content-Type', 'multipart/form-data;boundary=X'),
+                         ('Content-Length', str(len(body))),
+                     ],
+                     body=body),
+        self.assertBody(
+            repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
+
+
+class SafeMultipartHandlingTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_Flash_Upload(self):
+        headers = [
+            ('Accept', 'text/*'),
+            ('Content-Type', 'multipart/form-data; '
+             'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
+            ('User-Agent', 'Shockwave Flash'),
+            ('Host', 'www.example.com:54583'),
+            ('Content-Length', '499'),
+            ('Connection', 'Keep-Alive'),
+            ('Cache-Control', 'no-cache'),
+        ]
+        filedata = (b'<?xml version="1.0" encoding="UTF-8"?>\r\n'
+                    b'<projectDescription>\r\n'
+                    b'</projectDescription>\r\n')
+        body = (
+            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+            b'Content-Disposition: form-data; name="Filename"\r\n'
+            b'\r\n'
+            b'.project\r\n'
+            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+            b'Content-Disposition: form-data; '
+            b'name="Filedata"; filename=".project"\r\n'
+            b'Content-Type: application/octet-stream\r\n'
+            b'\r\n' +
+            filedata +
+            b'\r\n'
+            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
+            b'Content-Disposition: form-data; name="Upload"\r\n'
+            b'\r\n'
+            b'Submit Query\r\n'
+            # Flash apps omit the trailing \r\n on the last line:
+            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
+        )
+        self.getPage('/flashupload', headers, 'POST', body)
+        self.assertBody('Upload: Submit Query, Filename: .project, '
+                        'Filedata: %r' % filedata)
diff --git a/libraries/cherrypy/test/test_misc_tools.py b/libraries/cherrypy/test/test_misc_tools.py
new file mode 100644
index 00000000..fb85b8f8
--- /dev/null
+++ b/libraries/cherrypy/test/test_misc_tools.py
@@ -0,0 +1,210 @@
+import os
+
+import cherrypy
+from cherrypy import tools
+from cherrypy.test import helper
+
+
+localDir = os.path.dirname(__file__)
+logfile = os.path.join(localDir, 'test_misc_tools.log')
+
+
+def setup_server():
+    class Root:
+
+        @cherrypy.expose
+        def index(self):
+            yield 'Hello, world'
+        h = [('Content-Language', 'en-GB'), ('Content-Type', 'text/plain')]
+        tools.response_headers(headers=h)(index)
+
+        @cherrypy.expose
+        @cherrypy.config(**{
+            'tools.response_headers.on': True,
+            'tools.response_headers.headers': [
+                ('Content-Language', 'fr'),
+                ('Content-Type', 'text/plain'),
+            ],
+            'tools.log_hooks.on': True,
+        })
+        def other(self):
+            return 'salut'
+
+    @cherrypy.config(**{'tools.accept.on': True})
+    class Accept:
+
+        @cherrypy.expose
+        def index(self):
+            return '<a href="feed">Atom feed</a>'
+
+        @cherrypy.expose
+        @tools.accept(media='application/atom+xml')
+        def feed(self):
+            return """<?xml version="1.0" encoding="utf-8"?>
+<feed xmlns="http://www.w3.org/2005/Atom">
+    <title>Unknown Blog</title>
+</feed>"""
+
+        @cherrypy.expose
+        def select(self):
+            # We could also write this: mtype = cherrypy.lib.accept.accept(...)
+            mtype = tools.accept.callable(['text/html', 'text/plain'])
+            if mtype == 'text/html':
+                return '<h2>Page Title</h2>'
+            else:
+                return 'PAGE TITLE'
+
+    class Referer:
+
+        @cherrypy.expose
+        def accept(self):
+            return 'Accepted!'
+        reject = accept
+
+    class AutoVary:
+
+        @cherrypy.expose
+        def index(self):
+            # Read a header directly with 'get'
+            cherrypy.request.headers.get('Accept-Encoding')
+            # Read a header directly with '__getitem__'
+            cherrypy.request.headers['Host']
+            # Read a header directly with '__contains__'
+            'If-Modified-Since' in cherrypy.request.headers
+            # Read a header directly
+            'Range' in cherrypy.request.headers
+            # Call a lib function
+            tools.accept.callable(['text/html', 'text/plain'])
+            return 'Hello, world!'
+
+    conf = {'/referer': {'tools.referer.on': True,
+                         'tools.referer.pattern': r'http://[^/]*example\.com',
+                         },
+            '/referer/reject': {'tools.referer.accept': False,
+                                'tools.referer.accept_missing': True,
+                                },
+            '/autovary': {'tools.autovary.on': True},
+            }
+
+    root = Root()
+    root.referer = Referer()
+    root.accept = Accept()
+    root.autovary = AutoVary()
+    cherrypy.tree.mount(root, config=conf)
+    cherrypy.config.update({'log.error_file': logfile})
+
+
+class ResponseHeadersTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def testResponseHeadersDecorator(self):
+        self.getPage('/')
+        self.assertHeader('Content-Language', 'en-GB')
+        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+
+    def testResponseHeaders(self):
+        self.getPage('/other')
+        self.assertHeader('Content-Language', 'fr')
+        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+
+
+class RefererTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def testReferer(self):
+        self.getPage('/referer/accept')
+        self.assertErrorPage(403, 'Forbidden Referer header.')
+
+        self.getPage('/referer/accept',
+                     headers=[('Referer', 'http://www.example.com/')])
+        self.assertStatus(200)
+        self.assertBody('Accepted!')
+
+        # Reject
+        self.getPage('/referer/reject')
+        self.assertStatus(200)
+        self.assertBody('Accepted!')
+
+        self.getPage('/referer/reject',
+                     headers=[('Referer', 'http://www.example.com/')])
+        self.assertErrorPage(403, 'Forbidden Referer header.')
+
+
+class AcceptTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def test_Accept_Tool(self):
+        # Test with no header provided
+        self.getPage('/accept/feed')
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+
+        # Specify exact media type
+        self.getPage('/accept/feed',
+                     headers=[('Accept', 'application/atom+xml')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+
+        # Specify matching media range
+        self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+
+        # Specify all media ranges
+        self.getPage('/accept/feed', headers=[('Accept', '*/*')])
+        self.assertStatus(200)
+        self.assertInBody('<title>Unknown Blog</title>')
+
+        # Specify unacceptable media types
+        self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
+        self.assertErrorPage(406,
+                             'Your client sent this Accept header: text/html. '
+                             'But this resource only emits these media types: '
+                             'application/atom+xml.')
+
+        # Test resource where tool is 'on' but media is None (not set).
+        self.getPage('/accept/')
+        self.assertStatus(200)
+        self.assertBody('<a href="feed">Atom feed</a>')
+
+    def test_accept_selection(self):
+        # Try both our expected media types
+        self.getPage('/accept/select', [('Accept', 'text/html')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        self.getPage('/accept/select', [('Accept', 'text/plain')])
+        self.assertStatus(200)
+        self.assertBody('PAGE TITLE')
+        self.getPage('/accept/select',
+                     [('Accept', 'text/plain, text/*;q=0.5')])
+        self.assertStatus(200)
+        self.assertBody('PAGE TITLE')
+
+        # text/* and */* should prefer text/html since it comes first
+        # in our 'media' argument to tools.accept
+        self.getPage('/accept/select', [('Accept', 'text/*')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+        self.getPage('/accept/select', [('Accept', '*/*')])
+        self.assertStatus(200)
+        self.assertBody('<h2>Page Title</h2>')
+
+        # Try unacceptable media types
+        self.getPage('/accept/select', [('Accept', 'application/xml')])
+        self.assertErrorPage(
+            406,
+            'Your client sent this Accept header: application/xml. '
+            'But this resource only emits these media types: '
+            'text/html, text/plain.')
+
+
+class AutoVaryTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def testAutoVary(self):
+        self.getPage('/autovary/')
+        self.assertHeader(
+            'Vary',
+            'Accept, Accept-Charset, Accept-Encoding, '
+            'Host, If-Modified-Since, Range'
+        )
diff --git a/libraries/cherrypy/test/test_native.py b/libraries/cherrypy/test/test_native.py
new file mode 100644
index 00000000..caebc3f4
--- /dev/null
+++ b/libraries/cherrypy/test/test_native.py
@@ -0,0 +1,35 @@
+"""Test the native server."""
+
+import pytest
+from requests_toolbelt import sessions
+
+import cherrypy._cpnative_server
+
+
+pytestmark = pytest.mark.skipif(
+    'sys.platform == "win32"',
+    reason='tests fail on Windows',
+)
+
+
+@pytest.fixture
+def cp_native_server(request):
+    """A native server."""
+    class Root(object):
+        @cherrypy.expose
+        def index(self):
+            return 'Hello World!'
+
+    cls = cherrypy._cpnative_server.CPHTTPServer
+    cherrypy.server.httpserver = cls(cherrypy.server)
+
+    cherrypy.tree.mount(Root(), '/')
+    cherrypy.engine.start()
+    request.addfinalizer(cherrypy.engine.stop)
+    url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals())
+    return sessions.BaseUrlSession(url)
+
+
+def test_basic_request(cp_native_server):
+    """A request to a native server should succeed."""
+    cp_native_server.get('/')
diff --git a/libraries/cherrypy/test/test_objectmapping.py b/libraries/cherrypy/test/test_objectmapping.py
new file mode 100644
index 00000000..98402b8b
--- /dev/null
+++ b/libraries/cherrypy/test/test_objectmapping.py
@@ -0,0 +1,430 @@
+import sys
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy._cptree import Application
+from cherrypy.test import helper
+
+script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
+
+
+class ObjectMappingTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self, name='world'):
+                return name
+
+            @cherrypy.expose
+            def foobar(self):
+                return 'bar'
+
+            @cherrypy.expose
+            def default(self, *params, **kwargs):
+                return 'default:' + repr(params)
+
+            @cherrypy.expose
+            def other(self):
+                return 'other'
+
+            @cherrypy.expose
+            def extra(self, *p):
+                return repr(p)
+
+            @cherrypy.expose
+            def redirect(self):
+                raise cherrypy.HTTPRedirect('dir1/', 302)
+
+            def notExposed(self):
+                return 'not exposed'
+
+            @cherrypy.expose
+            def confvalue(self):
+                return cherrypy.request.config.get('user')
+
+            @cherrypy.expose
+            def redirect_via_url(self, path):
+                raise cherrypy.HTTPRedirect(cherrypy.url(path))
+
+            @cherrypy.expose
+            def translate_html(self):
+                return 'OK'
+
+        @cherrypy.expose
+        def mapped_func(self, ID=None):
+            return 'ID is %s' % ID
+        setattr(Root, 'Von B\xfclow', mapped_func)
+
+        class Exposing:
+
+            @cherrypy.expose
+            def base(self):
+                return 'expose works!'
+            cherrypy.expose(base, '1')
+            cherrypy.expose(base, '2')
+
+        class ExposingNewStyle(object):
+
+            @cherrypy.expose
+            def base(self):
+                return 'expose works!'
+            cherrypy.expose(base, '1')
+            cherrypy.expose(base, '2')
+
+        class Dir1:
+
+            @cherrypy.expose
+            def index(self):
+                return 'index for dir1'
+
+            @cherrypy.expose
+            @cherrypy.config(**{'tools.trailing_slash.extra': True})
+            def myMethod(self):
+                return 'myMethod from dir1, path_info is:' + repr(
+                    cherrypy.request.path_info)
+
+            @cherrypy.expose
+            def default(self, *params):
+                return 'default for dir1, param is:' + repr(params)
+
+        class Dir2:
+
+            @cherrypy.expose
+            def index(self):
+                return 'index for dir2, path is:' + cherrypy.request.path_info
+
+            @cherrypy.expose
+            def script_name(self):
+                return cherrypy.tree.script_name()
+
+            @cherrypy.expose
+            def cherrypy_url(self):
+                return cherrypy.url('/extra')
+
+            @cherrypy.expose
+            def posparam(self, *vpath):
+                return '/'.join(vpath)
+
+        class Dir3:
+
+            def default(self):
+                return 'default for dir3, not exposed'
+
+        class Dir4:
+
+            def index(self):
+                return 'index for dir4, not exposed'
+
+        class DefNoIndex:
+
+            @cherrypy.expose
+            def default(self, *args):
+                raise cherrypy.HTTPRedirect('contact')
+
+        # MethodDispatcher code
+        @cherrypy.expose
+        class ByMethod:
+
+            def __init__(self, *things):
+                self.things = list(things)
+
+            def GET(self):
+                return repr(self.things)
+
+            def POST(self, thing):
+                self.things.append(thing)
+
+        class Collection:
+            default = ByMethod('a', 'bit')
+
+        Root.exposing = Exposing()
+        Root.exposingnew = ExposingNewStyle()
+        Root.dir1 = Dir1()
+        Root.dir1.dir2 = Dir2()
+        Root.dir1.dir2.dir3 = Dir3()
+        Root.dir1.dir2.dir3.dir4 = Dir4()
+        Root.defnoindex = DefNoIndex()
+        Root.bymethod = ByMethod('another')
+        Root.collection = Collection()
+
+        d = cherrypy.dispatch.MethodDispatcher()
+        for url in script_names:
+            conf = {'/': {'user': (url or '/').split('/')[-2]},
+                    '/bymethod': {'request.dispatch': d},
+                    '/collection': {'request.dispatch': d},
+                    }
+            cherrypy.tree.mount(Root(), url, conf)
+
+        class Isolated:
+
+            @cherrypy.expose
+            def index(self):
+                return 'made it!'
+
+        cherrypy.tree.mount(Isolated(), '/isolated')
+
+        @cherrypy.expose
+        class AnotherApp:
+
+            def GET(self):
+                return 'milk'
+
+        cherrypy.tree.mount(AnotherApp(), '/app',
+                            {'/': {'request.dispatch': d}})
+
+    def testObjectMapping(self):
+        for url in script_names:
+            self.script_name = url
+
+            self.getPage('/')
+            self.assertBody('world')
+
+            self.getPage('/dir1/myMethod')
+            self.assertBody(
+                "myMethod from dir1, path_info is:'/dir1/myMethod'")
+
+            self.getPage('/this/method/does/not/exist')
+            self.assertBody(
+                "default:('this', 'method', 'does', 'not', 'exist')")
+
+            self.getPage('/extra/too/much')
+            self.assertBody("('too', 'much')")
+
+            self.getPage('/other')
+            self.assertBody('other')
+
+            self.getPage('/notExposed')
+            self.assertBody("default:('notExposed',)")
+
+            self.getPage('/dir1/dir2/')
+            self.assertBody('index for dir2, path is:/dir1/dir2/')
+
+            # Test omitted trailing slash (should be redirected by default).
+            self.getPage('/dir1/dir2')
+            self.assertStatus(301)
+            self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
+
+            # Test extra trailing slash (should be redirected if configured).
+            self.getPage('/dir1/myMethod/')
+            self.assertStatus(301)
+            self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
+
+            # Test that default method must be exposed in order to match.
+            self.getPage('/dir1/dir2/dir3/dir4/index')
+            self.assertBody(
+                "default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
+
+            # Test *vpath when default() is defined but not index()
+            # This also tests HTTPRedirect with default.
+            self.getPage('/defnoindex')
+            self.assertStatus((302, 303))
+            self.assertHeader('Location', '%s/contact' % self.base())
+            self.getPage('/defnoindex/')
+            self.assertStatus((302, 303))
+            self.assertHeader('Location', '%s/defnoindex/contact' %
+                              self.base())
+            self.getPage('/defnoindex/page')
+            self.assertStatus((302, 303))
+            self.assertHeader('Location', '%s/defnoindex/contact' %
+                              self.base())
+
+            self.getPage('/redirect')
+            self.assertStatus('302 Found')
+            self.assertHeader('Location', '%s/dir1/' % self.base())
+
+            if not getattr(cherrypy.server, 'using_apache', False):
+                # Test that we can use URL's which aren't all valid Python
+                # identifiers
+                # This should also test the %XX-unquoting of URL's.
+                self.getPage('/Von%20B%fclow?ID=14')
+                self.assertBody('ID is 14')
+
+                # Test that %2F in the path doesn't get unquoted too early;
+                # that is, it should not be used to separate path components.
+                # See ticket #393.
+                self.getPage('/page%2Fname')
+                self.assertBody("default:('page/name',)")
+
+            self.getPage('/dir1/dir2/script_name')
+            self.assertBody(url)
+            self.getPage('/dir1/dir2/cherrypy_url')
+            self.assertBody('%s/extra' % self.base())
+
+            # Test that configs don't overwrite each other from different apps
+            self.getPage('/confvalue')
+            self.assertBody((url or '/').split('/')[-2])
+
+        self.script_name = ''
+
+        # Test absoluteURI's in the Request-Line
+        self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
+        self.assertBody('world')
+
+        self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
+                     (self.interface(), self.PORT))
+        self.assertBody("default:('abs',)")
+
+        self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
+        self.assertBody("default:('rel',)")
+
+        # Test that the "isolated" app doesn't leak url's into the root app.
+        # If it did leak, Root.default() would answer with
+        #   "default:('isolated', 'doesnt', 'exist')".
+        self.getPage('/isolated/')
+        self.assertStatus('200 OK')
+        self.assertBody('made it!')
+        self.getPage('/isolated/doesnt/exist')
+        self.assertStatus('404 Not Found')
+
+        # Make sure /foobar maps to Root.foobar and not to the app
+        # mounted at /foo. See
+        # https://github.com/cherrypy/cherrypy/issues/573
+        self.getPage('/foobar')
+        self.assertBody('bar')
+
+    def test_translate(self):
+        self.getPage('/translate_html')
+        self.assertStatus('200 OK')
+        self.assertBody('OK')
+
+        self.getPage('/translate.html')
+        self.assertStatus('200 OK')
+        self.assertBody('OK')
+
+        self.getPage('/translate-html')
+        self.assertStatus('200 OK')
+        self.assertBody('OK')
+
+    def test_redir_using_url(self):
+        for url in script_names:
+            self.script_name = url
+
+            # Test the absolute path to the parent (leading slash)
+            self.getPage('/redirect_via_url?path=./')
+            self.assertStatus(('302 Found', '303 See Other'))
+            self.assertHeader('Location', '%s/' % self.base())
+
+            # Test the relative path to the parent (no leading slash)
+            self.getPage('/redirect_via_url?path=./')
+            self.assertStatus(('302 Found', '303 See Other'))
+            self.assertHeader('Location', '%s/' % self.base())
+
+            # Test the absolute path to the parent (leading slash)
+            self.getPage('/redirect_via_url/?path=./')
+            self.assertStatus(('302 Found', '303 See Other'))
+            self.assertHeader('Location', '%s/' % self.base())
+
+            # Test the relative path to the parent (no leading slash)
+            self.getPage('/redirect_via_url/?path=./')
+            self.assertStatus(('302 Found', '303 See Other'))
+            self.assertHeader('Location', '%s/' % self.base())
+
+    def testPositionalParams(self):
+        self.getPage('/dir1/dir2/posparam/18/24/hut/hike')
+        self.assertBody('18/24/hut/hike')
+
+        # intermediate index methods should not receive posparams;
+        # only the "final" index method should do so.
+        self.getPage('/dir1/dir2/5/3/sir')
+        self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
+
+        # test that extra positional args raises an 404 Not Found
+        # See https://github.com/cherrypy/cherrypy/issues/733.
+        self.getPage('/dir1/dir2/script_name/extra/stuff')
+        self.assertStatus(404)
+
+    def testExpose(self):
+        # Test the cherrypy.expose function/decorator
+        self.getPage('/exposing/base')
+        self.assertBody('expose works!')
+
+        self.getPage('/exposing/1')
+        self.assertBody('expose works!')
+
+        self.getPage('/exposing/2')
+        self.assertBody('expose works!')
+
+        self.getPage('/exposingnew/base')
+        self.assertBody('expose works!')
+
+        self.getPage('/exposingnew/1')
+        self.assertBody('expose works!')
+
+        self.getPage('/exposingnew/2')
+        self.assertBody('expose works!')
+
+    def testMethodDispatch(self):
+        self.getPage('/bymethod')
+        self.assertBody("['another']")
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        self.getPage('/bymethod', method='HEAD')
+        self.assertBody('')
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        self.getPage('/bymethod', method='POST', body='thing=one')
+        self.assertBody('')
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        self.getPage('/bymethod')
+        self.assertBody(repr(['another', ntou('one')]))
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        self.getPage('/bymethod', method='PUT')
+        self.assertErrorPage(405)
+        self.assertHeader('Allow', 'GET, HEAD, POST')
+
+        # Test default with posparams
+        self.getPage('/collection/silly', method='POST')
+        self.getPage('/collection', method='GET')
+        self.assertBody("['a', 'bit', 'silly']")
+
+        # Test custom dispatcher set on app root (see #737).
+        self.getPage('/app')
+        self.assertBody('milk')
+
+    def testTreeMounting(self):
+        class Root(object):
+
+            @cherrypy.expose
+            def hello(self):
+                return 'Hello world!'
+
+        # When mounting an application instance,
+        # we can't specify a different script name in the call to mount.
+        a = Application(Root(), '/somewhere')
+        self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
+
+        # When mounting an application instance...
+        a = Application(Root(), '/somewhere')
+        # ...we MUST allow in identical script name in the call to mount...
+        cherrypy.tree.mount(a, '/somewhere')
+        self.getPage('/somewhere/hello')
+        self.assertStatus(200)
+        # ...and MUST allow a missing script_name.
+        del cherrypy.tree.apps['/somewhere']
+        cherrypy.tree.mount(a)
+        self.getPage('/somewhere/hello')
+        self.assertStatus(200)
+
+        # In addition, we MUST be able to create an Application using
+        # script_name == None for access to the wsgi_environ.
+        a = Application(Root(), script_name=None)
+        # However, this does not apply to tree.mount
+        self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
+
+    def testKeywords(self):
+        if sys.version_info < (3,):
+            return self.skip('skipped (Python 3 only)')
+        exec("""class Root(object):
+    @cherrypy.expose
+    def hello(self, *, name='world'):
+        return 'Hello %s!' % name
+cherrypy.tree.mount(Application(Root(), '/keywords'))""")
+
+        self.getPage('/keywords/hello')
+        self.assertStatus(200)
+        self.getPage('/keywords/hello/extra')
+        self.assertStatus(404)
diff --git a/libraries/cherrypy/test/test_params.py b/libraries/cherrypy/test/test_params.py
new file mode 100644
index 00000000..73b4cb4c
--- /dev/null
+++ b/libraries/cherrypy/test/test_params.py
@@ -0,0 +1,61 @@
+import sys
+import textwrap
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class ParamsTest(helper.CPWebCase):
+    @staticmethod
+    def setup_server():
+        class Root:
+            @cherrypy.expose
+            @cherrypy.tools.json_out()
+            @cherrypy.tools.params()
+            def resource(self, limit=None, sort=None):
+                return type(limit).__name__
+            # for testing on Py 2
+            resource.__annotations__ = {'limit': int}
+        conf = {'/': {'tools.params.on': True}}
+        cherrypy.tree.mount(Root(), config=conf)
+
+    def test_pass(self):
+        self.getPage('/resource')
+        self.assertStatus(200)
+        self.assertBody('"NoneType"')
+
+        self.getPage('/resource?limit=0')
+        self.assertStatus(200)
+        self.assertBody('"int"')
+
+    def test_error(self):
+        self.getPage('/resource?limit=')
+        self.assertStatus(400)
+        self.assertInBody('invalid literal for int')
+
+        cherrypy.config['tools.params.error'] = 422
+        self.getPage('/resource?limit=')
+        self.assertStatus(422)
+        self.assertInBody('invalid literal for int')
+
+        cherrypy.config['tools.params.exception'] = TypeError
+        self.getPage('/resource?limit=')
+        self.assertStatus(500)
+
+    def test_syntax(self):
+        if sys.version_info < (3,):
+            return self.skip('skipped (Python 3 only)')
+        code = textwrap.dedent("""
+            class Root:
+                @cherrypy.expose
+                @cherrypy.tools.params()
+                def resource(self, limit: int):
+                    return type(limit).__name__
+            conf = {'/': {'tools.params.on': True}}
+            cherrypy.tree.mount(Root(), config=conf)
+            """)
+        exec(code)
+
+        self.getPage('/resource?limit=0')
+        self.assertStatus(200)
+        self.assertBody('int')
diff --git a/libraries/cherrypy/test/test_plugins.py b/libraries/cherrypy/test/test_plugins.py
new file mode 100644
index 00000000..4d3aa6b1
--- /dev/null
+++ b/libraries/cherrypy/test/test_plugins.py
@@ -0,0 +1,14 @@
+from cherrypy.process import plugins
+
+
+__metaclass__ = type
+
+
+class TestAutoreloader:
+    def test_file_for_file_module_when_None(self):
+        """No error when module.__file__ is None.
+        """
+        class test_module:
+            __file__ = None
+
+        assert plugins.Autoreloader._file_for_file_module(test_module) is None
diff --git a/libraries/cherrypy/test/test_proxy.py b/libraries/cherrypy/test/test_proxy.py
new file mode 100644
index 00000000..4d34440a
--- /dev/null
+++ b/libraries/cherrypy/test/test_proxy.py
@@ -0,0 +1,154 @@
+import cherrypy
+from cherrypy.test import helper
+
+script_names = ['', '/path/to/myapp']
+
+
+class ProxyTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        # Set up site
+        cherrypy.config.update({
+            'tools.proxy.on': True,
+            'tools.proxy.base': 'www.mydomain.test',
+        })
+
+        # Set up application
+
+        class Root:
+
+            def __init__(self, sn):
+                # Calculate a URL outside of any requests.
+                self.thisnewpage = cherrypy.url(
+                    '/this/new/page', script_name=sn)
+
+            @cherrypy.expose
+            def pageurl(self):
+                return self.thisnewpage
+
+            @cherrypy.expose
+            def index(self):
+                raise cherrypy.HTTPRedirect('dummy')
+
+            @cherrypy.expose
+            def remoteip(self):
+                return cherrypy.request.remote.ip
+
+            @cherrypy.expose
+            @cherrypy.config(**{
+                'tools.proxy.local': 'X-Host',
+                'tools.trailing_slash.extra': True,
+            })
+            def xhost(self):
+                raise cherrypy.HTTPRedirect('blah')
+
+            @cherrypy.expose
+            def base(self):
+                return cherrypy.request.base
+
+            @cherrypy.expose
+            @cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'})
+            def ssl(self):
+                return cherrypy.request.base
+
+            @cherrypy.expose
+            def newurl(self):
+                return ("Browse to <a href='%s'>this page</a>."
+                        % cherrypy.url('/this/new/page'))
+
+            @cherrypy.expose
+            @cherrypy.config(**{
+                'tools.proxy.base': None,
+            })
+            def base_no_base(self):
+                return cherrypy.request.base
+
+        for sn in script_names:
+            cherrypy.tree.mount(Root(sn), sn)
+
+    def testProxy(self):
+        self.getPage('/')
+        self.assertHeader('Location',
+                          '%s://www.mydomain.test%s/dummy' %
+                          (self.scheme, self.prefix()))
+
+        # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
+        self.getPage(
+            '/', headers=[('X-Forwarded-Host', 'http://www.example.test')])
+        self.assertHeader('Location', 'http://www.example.test/dummy')
+        self.getPage('/', headers=[('X-Forwarded-Host', 'www.example.test')])
+        self.assertHeader('Location', '%s://www.example.test/dummy' %
+                          self.scheme)
+        # Test multiple X-Forwarded-Host headers
+        self.getPage('/', headers=[
+            ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
+        ])
+        self.assertHeader('Location', 'http://www.example.test/dummy')
+
+        # Test X-Forwarded-For (Apache2)
+        self.getPage('/remoteip',
+                     headers=[('X-Forwarded-For', '192.168.0.20')])
+        self.assertBody('192.168.0.20')
+        # Fix bug #1268
+        self.getPage('/remoteip',
+                     headers=[
+                         ('X-Forwarded-For', '67.15.36.43, 192.168.0.20')
+                     ])
+        self.assertBody('67.15.36.43')
+
+        # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
+        self.getPage('/xhost', headers=[('X-Host', 'www.example.test')])
+        self.assertHeader('Location', '%s://www.example.test/blah' %
+                          self.scheme)
+
+        # Test X-Forwarded-Proto (lighttpd)
+        self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')])
+        self.assertBody('https://www.mydomain.test')
+
+        # Test X-Forwarded-Ssl (webfaction?)
+        self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')])
+        self.assertBody('https://www.mydomain.test')
+
+        # Test cherrypy.url()
+        for sn in script_names:
+            # Test the value inside requests
+            self.getPage(sn + '/newurl')
+            self.assertBody(
+                "Browse to <a href='%s://www.mydomain.test" % self.scheme +
+                sn + "/this/new/page'>this page</a>.")
+            self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host',
+                                                   'http://www.example.test')])
+            self.assertBody("Browse to <a href='http://www.example.test" +
+                            sn + "/this/new/page'>this page</a>.")
+
+            # Test the value outside requests
+            port = ''
+            if self.scheme == 'http' and self.PORT != 80:
+                port = ':%s' % self.PORT
+            elif self.scheme == 'https' and self.PORT != 443:
+                port = ':%s' % self.PORT
+            host = self.HOST
+            if host in ('0.0.0.0', '::'):
+                import socket
+                host = socket.gethostname()
+            expected = ('%s://%s%s%s/this/new/page'
+                        % (self.scheme, host, port, sn))
+            self.getPage(sn + '/pageurl')
+            self.assertBody(expected)
+
+        # Test trailing slash (see
+        # https://github.com/cherrypy/cherrypy/issues/562).
+        self.getPage('/xhost/', headers=[('X-Host', 'www.example.test')])
+        self.assertHeader('Location', '%s://www.example.test/xhost'
+                          % self.scheme)
+
+    def test_no_base_port_in_host(self):
+        """
+        If no base is indicated, and the host header is used to resolve
+        the base, it should rely on the host header for the port also.
+        """
+        headers = {'Host': 'localhost:8080'}.items()
+        self.getPage('/base_no_base', headers=headers)
+        self.assertBody('http://localhost:8080')
diff --git a/libraries/cherrypy/test/test_refleaks.py b/libraries/cherrypy/test/test_refleaks.py
new file mode 100644
index 00000000..c2fe9e66
--- /dev/null
+++ b/libraries/cherrypy/test/test_refleaks.py
@@ -0,0 +1,66 @@
+"""Tests for refleaks."""
+
+import itertools
+import platform
+import threading
+
+from six.moves.http_client import HTTPConnection
+
+import cherrypy
+from cherrypy._cpcompat import HTTPSConnection
+from cherrypy.test import helper
+
+
+data = object()
+
+
+class ReferenceTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        class Root:
+
+            @cherrypy.expose
+            def index(self, *args, **kwargs):
+                cherrypy.request.thing = data
+                return 'Hello world!'
+
+        cherrypy.tree.mount(Root())
+
+    def test_threadlocal_garbage(self):
+        if platform.system() == 'Darwin':
+            self.skip('queue issues; see #1474')
+        success = itertools.count()
+
+        def getpage():
+            host = '%s:%s' % (self.interface(), self.PORT)
+            if self.scheme == 'https':
+                c = HTTPSConnection(host)
+            else:
+                c = HTTPConnection(host)
+            try:
+                c.putrequest('GET', '/')
+                c.endheaders()
+                response = c.getresponse()
+                body = response.read()
+                self.assertEqual(response.status, 200)
+                self.assertEqual(body, b'Hello world!')
+            finally:
+                c.close()
+            next(success)
+
+        ITERATIONS = 25
+
+        ts = [
+            threading.Thread(target=getpage)
+            for _ in range(ITERATIONS)
+        ]
+
+        for t in ts:
+            t.start()
+
+        for t in ts:
+            t.join()
+
+        self.assertEqual(next(success), ITERATIONS)
diff --git a/libraries/cherrypy/test/test_request_obj.py b/libraries/cherrypy/test/test_request_obj.py
new file mode 100644
index 00000000..6b93e13d
--- /dev/null
+++ b/libraries/cherrypy/test/test_request_obj.py
@@ -0,0 +1,932 @@
+"""Basic tests for the cherrypy.Request object."""
+
+from functools import wraps
+import os
+import sys
+import types
+import uuid
+
+import six
+from six.moves.http_client import IncompleteRead
+
+import cherrypy
+from cherrypy._cpcompat import ntou
+from cherrypy.lib import httputil
+from cherrypy.test import helper
+
+localDir = os.path.dirname(__file__)
+
+defined_http_methods = ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE',
+                        'TRACE', 'PROPFIND', 'PATCH')
+
+
+#                             Client-side code                             #
+
+
+class RequestObjectTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'hello'
+
+            @cherrypy.expose
+            def scheme(self):
+                return cherrypy.request.scheme
+
+            @cherrypy.expose
+            def created_example_com_3128(self):
+                """Handle CONNECT method."""
+                cherrypy.response.status = 204
+
+            @cherrypy.expose
+            def body_example_com_3128(self):
+                """Handle CONNECT method."""
+                return (
+                    cherrypy.request.method
+                    + 'ed to '
+                    + cherrypy.request.path_info
+                )
+
+            @cherrypy.expose
+            def request_uuid4(self):
+                return [
+                    str(cherrypy.request.unique_id),
+                    ' ',
+                    str(cherrypy.request.unique_id),
+                ]
+
+        root = Root()
+
+        class TestType(type):
+            """Metaclass which automatically exposes all functions in each
+            subclass, and adds an instance of the subclass as an attribute
+            of root.
+            """
+            def __init__(cls, name, bases, dct):
+                type.__init__(cls, name, bases, dct)
+                for value in dct.values():
+                    if isinstance(value, types.FunctionType):
+                        value.exposed = True
+                setattr(root, name.lower(), cls())
+        Test = TestType('Test', (object,), {})
+
+        class PathInfo(Test):
+
+            def default(self, *args):
+                return cherrypy.request.path_info
+
+        class Params(Test):
+
+            def index(self, thing):
+                return repr(thing)
+
+            def ismap(self, x, y):
+                return 'Coordinates: %s, %s' % (x, y)
+
+            @cherrypy.config(**{'request.query_string_encoding': 'latin1'})
+            def default(self, *args, **kwargs):
+                return 'args: %s kwargs: %s' % (args, sorted(kwargs.items()))
+
+        @cherrypy.expose
+        class ParamErrorsCallable(object):
+
+            def __call__(self):
+                return 'data'
+
+        def handler_dec(f):
+            @wraps(f)
+            def wrapper(handler, *args, **kwargs):
+                return f(handler, *args, **kwargs)
+            return wrapper
+
+        class ParamErrors(Test):
+
+            @cherrypy.expose
+            def one_positional(self, param1):
+                return 'data'
+
+            @cherrypy.expose
+            def one_positional_args(self, param1, *args):
+                return 'data'
+
+            @cherrypy.expose
+            def one_positional_args_kwargs(self, param1, *args, **kwargs):
+                return 'data'
+
+            @cherrypy.expose
+            def one_positional_kwargs(self, param1, **kwargs):
+                return 'data'
+
+            @cherrypy.expose
+            def no_positional(self):
+                return 'data'
+
+            @cherrypy.expose
+            def no_positional_args(self, *args):
+                return 'data'
+
+            @cherrypy.expose
+            def no_positional_args_kwargs(self, *args, **kwargs):
+                return 'data'
+
+            @cherrypy.expose
+            def no_positional_kwargs(self, **kwargs):
+                return 'data'
+
+            callable_object = ParamErrorsCallable()
+
+            @cherrypy.expose
+            def raise_type_error(self, **kwargs):
+                raise TypeError('Client Error')
+
+            @cherrypy.expose
+            def raise_type_error_with_default_param(self, x, y=None):
+                return '%d' % 'a'  # throw an exception
+
+            @cherrypy.expose
+            @handler_dec
+            def raise_type_error_decorated(self, *args, **kwargs):
+                raise TypeError('Client Error')
+
+        def callable_error_page(status, **kwargs):
+            return "Error %s - Well, I'm very sorry but you haven't paid!" % (
+                status)
+
+        @cherrypy.config(**{'tools.log_tracebacks.on': True})
+        class Error(Test):
+
+            def reason_phrase(self):
+                raise cherrypy.HTTPError("410 Gone fishin'")
+
+            @cherrypy.config(**{
+                'error_page.404': os.path.join(localDir, 'static/index.html'),
+                'error_page.401': callable_error_page,
+            })
+            def custom(self, err='404'):
+                raise cherrypy.HTTPError(
+                    int(err), 'No, <b>really</b>, not found!')
+
+            @cherrypy.config(**{
+                'error_page.default': callable_error_page,
+            })
+            def custom_default(self):
+                return 1 + 'a'  # raise an unexpected error
+
+            @cherrypy.config(**{'error_page.404': 'nonexistent.html'})
+            def noexist(self):
+                raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!')
+
+            def page_method(self):
+                raise ValueError()
+
+            def page_yield(self):
+                yield 'howdy'
+                raise ValueError()
+
+            @cherrypy.config(**{'response.stream': True})
+            def page_streamed(self):
+                yield 'word up'
+                raise ValueError()
+                yield 'very oops'
+
+            @cherrypy.config(**{'request.show_tracebacks': False})
+            def cause_err_in_finalize(self):
+                # Since status must start with an int, this should error.
+                cherrypy.response.status = 'ZOO OK'
+
+            @cherrypy.config(**{'request.throw_errors': True})
+            def rethrow(self):
+                """Test that an error raised here will be thrown out to
+                the server.
+                """
+                raise ValueError()
+
+        class Expect(Test):
+
+            def expectation_failed(self):
+                expect = cherrypy.request.headers.elements('Expect')
+                if expect and expect[0].value != '100-continue':
+                    raise cherrypy.HTTPError(400)
+                raise cherrypy.HTTPError(417, 'Expectation Failed')
+
+        class Headers(Test):
+
+            def default(self, headername):
+                """Spit back out the value for the requested header."""
+                return cherrypy.request.headers[headername]
+
+            def doubledheaders(self):
+                # From https://github.com/cherrypy/cherrypy/issues/165:
+                # "header field names should not be case sensitive sayes the
+                # rfc. if i set a headerfield in complete lowercase i end up
+                # with two header fields, one in lowercase, the other in
+                # mixed-case."
+
+                # Set the most common headers
+                hMap = cherrypy.response.headers
+                hMap['content-type'] = 'text/html'
+                hMap['content-length'] = 18
+                hMap['server'] = 'CherryPy headertest'
+                hMap['location'] = ('%s://%s:%s/headers/'
+                                    % (cherrypy.request.local.ip,
+                                       cherrypy.request.local.port,
+                                       cherrypy.request.scheme))
+
+                # Set a rare header for fun
+                hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
+
+                return 'double header test'
+
+            def ifmatch(self):
+                val = cherrypy.request.headers['If-Match']
+                assert isinstance(val, six.text_type)
+                cherrypy.response.headers['ETag'] = val
+                return val
+
+        class HeaderElements(Test):
+
+            def get_elements(self, headername):
+                e = cherrypy.request.headers.elements(headername)
+                return '\n'.join([six.text_type(x) for x in e])
+
+        class Method(Test):
+
+            def index(self):
+                m = cherrypy.request.method
+                if m in defined_http_methods or m == 'CONNECT':
+                    return m
+
+                if m == 'LINK':
+                    raise cherrypy.HTTPError(405)
+                else:
+                    raise cherrypy.HTTPError(501)
+
+            def parameterized(self, data):
+                return data
+
+            def request_body(self):
+                # This should be a file object (temp file),
+                # which CP will just pipe back out if we tell it to.
+                return cherrypy.request.body
+
+            def reachable(self):
+                return 'success'
+
+        class Divorce(Test):
+
+            """HTTP Method handlers shouldn't collide with normal method names.
+            For example, a GET-handler shouldn't collide with a method named
+            'get'.
+
+            If you build HTTP method dispatching into CherryPy, rewrite this
+            class to use your new dispatch mechanism and make sure that:
+                "GET /divorce HTTP/1.1" maps to divorce.index() and
+                "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
+            """
+
+            documents = {}
+
+            @cherrypy.expose
+            def index(self):
+                yield '<h1>Choose your document</h1>\n'
+                yield '<ul>\n'
+                for id, contents in self.documents.items():
+                    yield (
+                        "    <li><a href='/divorce/get?ID=%s'>%s</a>:"
+                        ' %s</li>\n' % (id, id, contents))
+                yield '</ul>'
+
+            @cherrypy.expose
+            def get(self, ID):
+                return ('Divorce document %s: %s' %
+                        (ID, self.documents.get(ID, 'empty')))
+
+        class ThreadLocal(Test):
+
+            def index(self):
+                existing = repr(getattr(cherrypy.request, 'asdf', None))
+                cherrypy.request.asdf = 'rassfrassin'
+                return existing
+
+        appconf = {
+            '/method': {
+                'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND',
+                                                'PATCH')
+            },
+        }
+        cherrypy.tree.mount(root, config=appconf)
+
+    def test_scheme(self):
+        self.getPage('/scheme')
+        self.assertBody(self.scheme)
+
+    def test_per_request_uuid4(self):
+        self.getPage('/request_uuid4')
+        first_uuid4, _, second_uuid4 = self.body.decode().partition(' ')
+        assert (
+            uuid.UUID(first_uuid4, version=4)
+            == uuid.UUID(second_uuid4, version=4)
+        )
+
+        self.getPage('/request_uuid4')
+        third_uuid4, _, _ = self.body.decode().partition(' ')
+        assert (
+            uuid.UUID(first_uuid4, version=4)
+            != uuid.UUID(third_uuid4, version=4)
+        )
+
+    def testRelativeURIPathInfo(self):
+        self.getPage('/pathinfo/foo/bar')
+        self.assertBody('/pathinfo/foo/bar')
+
+    def testAbsoluteURIPathInfo(self):
+        # http://cherrypy.org/ticket/1061
+        self.getPage('http://localhost/pathinfo/foo/bar')
+        self.assertBody('/pathinfo/foo/bar')
+
+    def testParams(self):
+        self.getPage('/params/?thing=a')
+        self.assertBody(repr(ntou('a')))
+
+        self.getPage('/params/?thing=a&thing=b&thing=c')
+        self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')]))
+
+        # Test friendly error message when given params are not accepted.
+        cherrypy.config.update({'request.show_mismatched_params': True})
+        self.getPage('/params/?notathing=meeting')
+        self.assertInBody('Missing parameters: thing')
+        self.getPage('/params/?thing=meeting&notathing=meeting')
+        self.assertInBody('Unexpected query string parameters: notathing')
+
+        # Test ability to turn off friendly error messages
+        cherrypy.config.update({'request.show_mismatched_params': False})
+        self.getPage('/params/?notathing=meeting')
+        self.assertInBody('Not Found')
+        self.getPage('/params/?thing=meeting&notathing=meeting')
+        self.assertInBody('Not Found')
+
+        # Test "% HEX HEX"-encoded URL, param keys, and values
+        self.getPage('/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville')
+        self.assertBody('args: %s kwargs: %s' %
+                        (('\xd4 \xe3', 'cheese'),
+                         [('Gruy\xe8re', ntou('Bulgn\xe9ville'))]))
+
+        # Make sure that encoded = and & get parsed correctly
+        self.getPage(
+            '/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2')
+        self.assertBody('args: %s kwargs: %s' %
+                        (('code',),
+                         [('url', ntou('http://cherrypy.org/index?a=1&b=2'))]))
+
+        # Test coordinates sent by <img ismap>
+        self.getPage('/params/ismap?223,114')
+        self.assertBody('Coordinates: 223, 114')
+
+        # Test "name[key]" dict-like params
+        self.getPage('/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz')
+        self.assertBody('args: %s kwargs: %s' %
+                        (('dictlike',),
+                         [('a[1]', ntou('1')), ('a[2]', ntou('2')),
+                          ('b', ntou('foo')), ('b[bar]', ntou('baz'))]))
+
+    def testParamErrors(self):
+
+        # test that all of the handlers work when given
+        # the correct parameters in order to ensure that the
+        # errors below aren't coming from some other source.
+        for uri in (
+                '/paramerrors/one_positional?param1=foo',
+                '/paramerrors/one_positional_args?param1=foo',
+                '/paramerrors/one_positional_args/foo',
+                '/paramerrors/one_positional_args/foo/bar/baz',
+                '/paramerrors/one_positional_args_kwargs?'
+                'param1=foo&param2=bar',
+                '/paramerrors/one_positional_args_kwargs/foo?'
+                'param2=bar&param3=baz',
+                '/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
+                'param2=bar&param3=baz',
+                '/paramerrors/one_positional_kwargs?'
+                'param1=foo&param2=bar&param3=baz',
+                '/paramerrors/one_positional_kwargs/foo?'
+                'param4=foo&param2=bar&param3=baz',
+                '/paramerrors/no_positional',
+                '/paramerrors/no_positional_args/foo',
+                '/paramerrors/no_positional_args/foo/bar/baz',
+                '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
+                '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
+                '/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
+                'param2=bar&param3=baz',
+                '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
+                '/paramerrors/callable_object',
+        ):
+            self.getPage(uri)
+            self.assertStatus(200)
+
+        error_msgs = [
+            'Missing parameters',
+            'Nothing matches the given URI',
+            'Multiple values for parameters',
+            'Unexpected query string parameters',
+            'Unexpected body parameters',
+            'Invalid path in Request-URI',
+            'Illegal #fragment in Request-URI',
+        ]
+
+        # uri should be tested for valid absolute path, the status must be 400.
+        for uri, error_idx in (
+            ('invalid/path/without/leading/slash', 5),
+            ('/valid/path#invalid=fragment', 6),
+        ):
+            self.getPage(uri)
+            self.assertStatus(400)
+            self.assertInBody(error_msgs[error_idx])
+
+        # query string parameters are part of the URI, so if they are wrong
+        # for a particular handler, the status MUST be a 404.
+        for uri, msg in (
+            ('/paramerrors/one_positional', error_msgs[0]),
+            ('/paramerrors/one_positional?foo=foo', error_msgs[0]),
+            ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
+            ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
+            ('/paramerrors/one_positional/foo?param1=foo&param2=foo',
+             error_msgs[2]),
+            ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
+             error_msgs[2]),
+            ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
+             error_msgs[3]),
+            ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
+             'param1=bar&param3=baz',
+             error_msgs[2]),
+            ('/paramerrors/one_positional_kwargs/foo?'
+             'param1=foo&param2=bar&param3=baz',
+             error_msgs[2]),
+            ('/paramerrors/no_positional/boo', error_msgs[1]),
+            ('/paramerrors/no_positional?param1=foo', error_msgs[3]),
+            ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
+            ('/paramerrors/no_positional_kwargs/boo?param1=foo',
+             error_msgs[1]),
+            ('/paramerrors/callable_object?param1=foo', error_msgs[3]),
+            ('/paramerrors/callable_object/boo', error_msgs[1]),
+        ):
+            for show_mismatched_params in (True, False):
+                cherrypy.config.update(
+                    {'request.show_mismatched_params': show_mismatched_params})
+                self.getPage(uri)
+                self.assertStatus(404)
+                if show_mismatched_params:
+                    self.assertInBody(msg)
+                else:
+                    self.assertInBody('Not Found')
+
+        # if body parameters are wrong, a 400 must be returned.
+        for uri, body, msg in (
+                ('/paramerrors/one_positional/foo',
+                 'param1=foo', error_msgs[2]),
+                ('/paramerrors/one_positional/foo',
+                 'param1=foo&param2=foo', error_msgs[2]),
+                ('/paramerrors/one_positional_args/foo',
+                 'param1=foo&param2=foo', error_msgs[2]),
+                ('/paramerrors/one_positional_args/foo/bar/baz',
+                 'param2=foo', error_msgs[4]),
+                ('/paramerrors/one_positional_args_kwargs/foo/bar/baz',
+                 'param1=bar&param3=baz', error_msgs[2]),
+                ('/paramerrors/one_positional_kwargs/foo',
+                 'param1=foo&param2=bar&param3=baz', error_msgs[2]),
+                ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
+                ('/paramerrors/no_positional_args/boo',
+                 'param1=foo', error_msgs[4]),
+                ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
+        ):
+            for show_mismatched_params in (True, False):
+                cherrypy.config.update(
+                    {'request.show_mismatched_params': show_mismatched_params})
+                self.getPage(uri, method='POST', body=body)
+                self.assertStatus(400)
+                if show_mismatched_params:
+                    self.assertInBody(msg)
+                else:
+                    self.assertInBody('400 Bad')
+
+        # even if body parameters are wrong, if we get the uri wrong, then
+        # it's a 404
+        for uri, body, msg in (
+                ('/paramerrors/one_positional?param2=foo',
+                 'param1=foo', error_msgs[3]),
+                ('/paramerrors/one_positional/foo/bar',
+                 'param2=foo', error_msgs[1]),
+                ('/paramerrors/one_positional_args/foo/bar?param2=foo',
+                 'param3=foo', error_msgs[3]),
+                ('/paramerrors/one_positional_kwargs/foo/bar',
+                 'param2=bar&param3=baz', error_msgs[1]),
+                ('/paramerrors/no_positional?param1=foo',
+                 'param2=foo', error_msgs[3]),
+                ('/paramerrors/no_positional_args/boo?param2=foo',
+                 'param1=foo', error_msgs[3]),
+                ('/paramerrors/callable_object?param2=bar',
+                 'param1=foo', error_msgs[3]),
+        ):
+            for show_mismatched_params in (True, False):
+                cherrypy.config.update(
+                    {'request.show_mismatched_params': show_mismatched_params})
+                self.getPage(uri, method='POST', body=body)
+                self.assertStatus(404)
+                if show_mismatched_params:
+                    self.assertInBody(msg)
+                else:
+                    self.assertInBody('Not Found')
+
+        # In the case that a handler raises a TypeError we should
+        # let that type error through.
+        for uri in (
+                '/paramerrors/raise_type_error',
+                '/paramerrors/raise_type_error_with_default_param?x=0',
+                '/paramerrors/raise_type_error_with_default_param?x=0&y=0',
+                '/paramerrors/raise_type_error_decorated',
+        ):
+            self.getPage(uri, method='GET')
+            self.assertStatus(500)
+            self.assertTrue('Client Error', self.body)
+
+    def testErrorHandling(self):
+        self.getPage('/error/missing')
+        self.assertStatus(404)
+        self.assertErrorPage(404, "The path '/error/missing' was not found.")
+
+        ignore = helper.webtest.ignored_exceptions
+        ignore.append(ValueError)
+        try:
+            valerr = '\n    raise ValueError()\nValueError'
+            self.getPage('/error/page_method')
+            self.assertErrorPage(500, pattern=valerr)
+
+            self.getPage('/error/page_yield')
+            self.assertErrorPage(500, pattern=valerr)
+
+            if (cherrypy.server.protocol_version == 'HTTP/1.0' or
+                    getattr(cherrypy.server, 'using_apache', False)):
+                self.getPage('/error/page_streamed')
+                # Because this error is raised after the response body has
+                # started, the status should not change to an error status.
+                self.assertStatus(200)
+                self.assertBody('word up')
+            else:
+                # Under HTTP/1.1, the chunked transfer-coding is used.
+                # The HTTP client will choke when the output is incomplete.
+                self.assertRaises((ValueError, IncompleteRead), self.getPage,
+                                  '/error/page_streamed')
+
+            # No traceback should be present
+            self.getPage('/error/cause_err_in_finalize')
+            msg = "Illegal response status from server ('ZOO' is non-numeric)."
+            self.assertErrorPage(500, msg, None)
+        finally:
+            ignore.pop()
+
+        # Test HTTPError with a reason-phrase in the status arg.
+        self.getPage('/error/reason_phrase')
+        self.assertStatus("410 Gone fishin'")
+
+        # Test custom error page for a specific error.
+        self.getPage('/error/custom')
+        self.assertStatus(404)
+        self.assertBody('Hello, world\r\n' + (' ' * 499))
+
+        # Test custom error page for a specific error.
+        self.getPage('/error/custom?err=401')
+        self.assertStatus(401)
+        self.assertBody(
+            'Error 401 Unauthorized - '
+            "Well, I'm very sorry but you haven't paid!")
+
+        # Test default custom error page.
+        self.getPage('/error/custom_default')
+        self.assertStatus(500)
+        self.assertBody(
+            'Error 500 Internal Server Error - '
+            "Well, I'm very sorry but you haven't paid!".ljust(513))
+
+        # Test error in custom error page (ticket #305).
+        # Note that the message is escaped for HTML (ticket #310).
+        self.getPage('/error/noexist')
+        self.assertStatus(404)
+        if sys.version_info >= (3, 3):
+            exc_name = 'FileNotFoundError'
+        else:
+            exc_name = 'IOError'
+        msg = ('No, &lt;b&gt;really&lt;/b&gt;, not found!<br />'
+               'In addition, the custom error page failed:\n<br />'
+               '%s: [Errno 2] '
+               "No such file or directory: 'nonexistent.html'") % (exc_name,)
+        self.assertInBody(msg)
+
+        if getattr(cherrypy.server, 'using_apache', False):
+            pass
+        else:
+            # Test throw_errors (ticket #186).
+            self.getPage('/error/rethrow')
+            self.assertInBody('raise ValueError()')
+
+    def testExpect(self):
+        e = ('Expect', '100-continue')
+        self.getPage('/headerelements/get_elements?headername=Expect', [e])
+        self.assertBody('100-continue')
+
+        self.getPage('/expect/expectation_failed', [e])
+        self.assertStatus(417)
+
+    def testHeaderElements(self):
+        # Accept-* header elements should be sorted, with most preferred first.
+        h = [('Accept', 'audio/*; q=0.2, audio/basic')]
+        self.getPage('/headerelements/get_elements?headername=Accept', h)
+        self.assertStatus(200)
+        self.assertBody('audio/basic\n'
+                        'audio/*;q=0.2')
+
+        h = [
+            ('Accept',
+             'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
+        ]
+        self.getPage('/headerelements/get_elements?headername=Accept', h)
+        self.assertStatus(200)
+        self.assertBody('text/x-c\n'
+                        'text/html\n'
+                        'text/x-dvi;q=0.8\n'
+                        'text/plain;q=0.5')
+
+        # Test that more specific media ranges get priority.
+        h = [('Accept', 'text/*, text/html, text/html;level=1, */*')]
+        self.getPage('/headerelements/get_elements?headername=Accept', h)
+        self.assertStatus(200)
+        self.assertBody('text/html;level=1\n'
+                        'text/html\n'
+                        'text/*\n'
+                        '*/*')
+
+        # Test Accept-Charset
+        h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')]
+        self.getPage(
+            '/headerelements/get_elements?headername=Accept-Charset', h)
+        self.assertStatus('200 OK')
+        self.assertBody('iso-8859-5\n'
+                        'unicode-1-1;q=0.8')
+
+        # Test Accept-Encoding
+        h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')]
+        self.getPage(
+            '/headerelements/get_elements?headername=Accept-Encoding', h)
+        self.assertStatus('200 OK')
+        self.assertBody('gzip;q=1.0\n'
+                        'identity;q=0.5\n'
+                        '*;q=0')
+
+        # Test Accept-Language
+        h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')]
+        self.getPage(
+            '/headerelements/get_elements?headername=Accept-Language', h)
+        self.assertStatus('200 OK')
+        self.assertBody('da\n'
+                        'en-gb;q=0.8\n'
+                        'en;q=0.7')
+
+        # Test malformed header parsing. See
+        # https://github.com/cherrypy/cherrypy/issues/763.
+        self.getPage('/headerelements/get_elements?headername=Content-Type',
+                     # Note the illegal trailing ";"
+                     headers=[('Content-Type', 'text/html; charset=utf-8;')])
+        self.assertStatus(200)
+        self.assertBody('text/html;charset=utf-8')
+
+    def test_repeated_headers(self):
+        # Test that two request headers are collapsed into one.
+        # See https://github.com/cherrypy/cherrypy/issues/542.
+        self.getPage('/headers/Accept-Charset',
+                     headers=[('Accept-Charset', 'iso-8859-5'),
+                              ('Accept-Charset', 'unicode-1-1;q=0.8')])
+        self.assertBody('iso-8859-5, unicode-1-1;q=0.8')
+
+        # Tests that each header only appears once, regardless of case.
+        self.getPage('/headers/doubledheaders')
+        self.assertBody('double header test')
+        hnames = [name.title() for name, val in self.headers]
+        for key in ['Content-Length', 'Content-Type', 'Date',
+                    'Expires', 'Location', 'Server']:
+            self.assertEqual(hnames.count(key), 1, self.headers)
+
+    def test_encoded_headers(self):
+        # First, make sure the innards work like expected.
+        self.assertEqual(
+            httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), ntou('f\xfcr'))
+
+        if cherrypy.server.protocol_version == 'HTTP/1.1':
+            # Test RFC-2047-encoded request and response header values
+            u = ntou('\u212bngstr\xf6m', 'escape')
+            c = ntou('=E2=84=ABngstr=C3=B6m')
+            self.getPage('/headers/ifmatch',
+                         [('If-Match', ntou('=?utf-8?q?%s?=') % c)])
+            # The body should be utf-8 encoded.
+            self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m')
+            # But the Etag header should be RFC-2047 encoded (binary)
+            self.assertHeader('ETag', ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?='))
+
+            # Test a *LONG* RFC-2047-encoded request and response header value
+            self.getPage('/headers/ifmatch',
+                         [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))])
+            self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10)
+            # Note: this is different output for Python3, but it decodes fine.
+            etag = self.assertHeader(
+                'ETag',
+                '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
+                '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
+                '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
+                '4oSrbmdzdHLDtm0=?=')
+            self.assertEqual(httputil.decode_TEXT(etag), u * 10)
+
+    def test_header_presence(self):
+        # If we don't pass a Content-Type header, it should not be present
+        # in cherrypy.request.headers
+        self.getPage('/headers/Content-Type',
+                     headers=[])
+        self.assertStatus(500)
+
+        # If Content-Type is present in the request, it should be present in
+        # cherrypy.request.headers
+        self.getPage('/headers/Content-Type',
+                     headers=[('Content-type', 'application/json')])
+        self.assertBody('application/json')
+
+    def test_basic_HTTPMethods(self):
+        helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND',
+                                              'PATCH')
+
+        # Test that all defined HTTP methods work.
+        for m in defined_http_methods:
+            self.getPage('/method/', method=m)
+
+            # HEAD requests should not return any body.
+            if m == 'HEAD':
+                self.assertBody('')
+            elif m == 'TRACE':
+                # Some HTTP servers (like modpy) have their own TRACE support
+                self.assertEqual(self.body[:5], b'TRACE')
+            else:
+                self.assertBody(m)
+
+        # test of PATCH requests
+        # Request a PATCH method with a form-urlencoded body
+        self.getPage('/method/parameterized', method='PATCH',
+                     body='data=on+top+of+other+things')
+        self.assertBody('on top of other things')
+
+        # Request a PATCH method with a file body
+        b = 'one thing on top of another'
+        h = [('Content-Type', 'text/plain'),
+             ('Content-Length', str(len(b)))]
+        self.getPage('/method/request_body', headers=h, method='PATCH', body=b)
+        self.assertStatus(200)
+        self.assertBody(b)
+
+        # Request a PATCH method with a file body but no Content-Type.
+        # See https://github.com/cherrypy/cherrypy/issues/790.
+        b = b'one thing on top of another'
+        self.persistent = True
+        try:
+            conn = self.HTTP_CONN
+            conn.putrequest('PATCH', '/method/request_body', skip_host=True)
+            conn.putheader('Host', self.HOST)
+            conn.putheader('Content-Length', str(len(b)))
+            conn.endheaders()
+            conn.send(b)
+            response = conn.response_class(conn.sock, method='PATCH')
+            response.begin()
+            self.assertEqual(response.status, 200)
+            self.body = response.read()
+            self.assertBody(b)
+        finally:
+            self.persistent = False
+
+        # Request a PATCH method with no body whatsoever (not an empty one).
+        # See https://github.com/cherrypy/cherrypy/issues/650.
+        # Provide a C-T or webtest will provide one (and a C-L) for us.
+        h = [('Content-Type', 'text/plain')]
+        self.getPage('/method/reachable', headers=h, method='PATCH')
+        self.assertStatus(411)
+
+        # HTTP PUT tests
+        # Request a PUT method with a form-urlencoded body
+        self.getPage('/method/parameterized', method='PUT',
+                     body='data=on+top+of+other+things')
+        self.assertBody('on top of other things')
+
+        # Request a PUT method with a file body
+        b = 'one thing on top of another'
+        h = [('Content-Type', 'text/plain'),
+             ('Content-Length', str(len(b)))]
+        self.getPage('/method/request_body', headers=h, method='PUT', body=b)
+        self.assertStatus(200)
+        self.assertBody(b)
+
+        # Request a PUT method with a file body but no Content-Type.
+        # See https://github.com/cherrypy/cherrypy/issues/790.
+        b = b'one thing on top of another'
+        self.persistent = True
+        try:
+            conn = self.HTTP_CONN
+            conn.putrequest('PUT', '/method/request_body', skip_host=True)
+            conn.putheader('Host', self.HOST)
+            conn.putheader('Content-Length', str(len(b)))
+            conn.endheaders()
+            conn.send(b)
+            response = conn.response_class(conn.sock, method='PUT')
+            response.begin()
+            self.assertEqual(response.status, 200)
+            self.body = response.read()
+            self.assertBody(b)
+        finally:
+            self.persistent = False
+
+        # Request a PUT method with no body whatsoever (not an empty one).
+        # See https://github.com/cherrypy/cherrypy/issues/650.
+        # Provide a C-T or webtest will provide one (and a C-L) for us.
+        h = [('Content-Type', 'text/plain')]
+        self.getPage('/method/reachable', headers=h, method='PUT')
+        self.assertStatus(411)
+
+        # Request a custom method with a request body
+        b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n'
+             '<propfind xmlns="DAV:"><prop><getlastmodified/>'
+             '</prop></propfind>')
+        h = [('Content-Type', 'text/xml'),
+             ('Content-Length', str(len(b)))]
+        self.getPage('/method/request_body', headers=h,
+                     method='PROPFIND', body=b)
+        self.assertStatus(200)
+        self.assertBody(b)
+
+        # Request a disallowed method
+        self.getPage('/method/', method='LINK')
+        self.assertStatus(405)
+
+        # Request an unknown method
+        self.getPage('/method/', method='SEARCH')
+        self.assertStatus(501)
+
+        # For method dispatchers: make sure that an HTTP method doesn't
+        # collide with a virtual path atom. If you build HTTP-method
+        # dispatching into the core, rewrite these handlers to use
+        # your dispatch idioms.
+        self.getPage('/divorce/get?ID=13')
+        self.assertBody('Divorce document 13: empty')
+        self.assertStatus(200)
+        self.getPage('/divorce/', method='GET')
+        self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>')
+        self.assertStatus(200)
+
+    def test_CONNECT_method(self):
+        self.persistent = True
+        try:
+            conn = self.HTTP_CONN
+            conn.request('CONNECT', 'created.example.com:3128')
+            response = conn.response_class(conn.sock, method='CONNECT')
+            response.begin()
+            self.assertEqual(response.status, 204)
+        finally:
+            self.persistent = False
+
+        self.persistent = True
+        try:
+            conn = self.HTTP_CONN
+            conn.request('CONNECT', 'body.example.com:3128')
+            response = conn.response_class(conn.sock, method='CONNECT')
+            response.begin()
+            self.assertEqual(response.status, 200)
+            self.body = response.read()
+            self.assertBody(b'CONNECTed to /body.example.com:3128')
+        finally:
+            self.persistent = False
+
+    def test_CONNECT_method_invalid_authority(self):
+        for request_target in ['example.com', 'http://example.com:33',
+                               '/path/', 'path/', '/?q=f', '#f']:
+            self.persistent = True
+            try:
+                conn = self.HTTP_CONN
+                conn.request('CONNECT', request_target)
+                response = conn.response_class(conn.sock, method='CONNECT')
+                response.begin()
+                self.assertEqual(response.status, 400)
+                self.body = response.read()
+                self.assertBody(b'Invalid path in Request-URI: request-target '
+                                b'must match authority-form.')
+            finally:
+                self.persistent = False
+
+    def testEmptyThreadlocals(self):
+        results = []
+        for x in range(20):
+            self.getPage('/threadlocal/')
+            results.append(self.body)
+        self.assertEqual(results, [b'None'] * 20)
diff --git a/libraries/cherrypy/test/test_routes.py b/libraries/cherrypy/test/test_routes.py
new file mode 100644
index 00000000..cc714765
--- /dev/null
+++ b/libraries/cherrypy/test/test_routes.py
@@ -0,0 +1,80 @@
+"""Test Routes dispatcher."""
+import os
+import importlib
+
+import pytest
+
+import cherrypy
+from cherrypy.test import helper
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+class RoutesDispatchTest(helper.CPWebCase):
+    """Routes dispatcher test suite."""
+
+    @staticmethod
+    def setup_server():
+        """Set up cherrypy test instance."""
+        try:
+            importlib.import_module('routes')
+        except ImportError:
+            pytest.skip('Install routes to test RoutesDispatcher code')
+
+        class Dummy:
+
+            def index(self):
+                return 'I said good day!'
+
+        class City:
+
+            def __init__(self, name):
+                self.name = name
+                self.population = 10000
+
+            @cherrypy.config(**{
+                'tools.response_headers.on': True,
+                'tools.response_headers.headers': [
+                    ('Content-Language', 'en-GB'),
+                ],
+            })
+            def index(self, **kwargs):
+                return 'Welcome to %s, pop. %s' % (self.name, self.population)
+
+            def update(self, **kwargs):
+                self.population = kwargs['pop']
+                return 'OK'
+
+        d = cherrypy.dispatch.RoutesDispatcher()
+        d.connect(action='index', name='hounslow', route='/hounslow',
+                  controller=City('Hounslow'))
+        d.connect(
+            name='surbiton', route='/surbiton', controller=City('Surbiton'),
+            action='index', conditions=dict(method=['GET']))
+        d.mapper.connect('/surbiton', controller='surbiton',
+                         action='update', conditions=dict(method=['POST']))
+        d.connect('main', ':action', controller=Dummy())
+
+        conf = {'/': {'request.dispatch': d}}
+        cherrypy.tree.mount(root=None, config=conf)
+
+    def test_Routes_Dispatch(self):
+        """Check that routes package based URI dispatching works correctly."""
+        self.getPage('/hounslow')
+        self.assertStatus('200 OK')
+        self.assertBody('Welcome to Hounslow, pop. 10000')
+
+        self.getPage('/foo')
+        self.assertStatus('404 Not Found')
+
+        self.getPage('/surbiton')
+        self.assertStatus('200 OK')
+        self.assertBody('Welcome to Surbiton, pop. 10000')
+
+        self.getPage('/surbiton', method='POST', body='pop=1327')
+        self.assertStatus('200 OK')
+        self.assertBody('OK')
+        self.getPage('/surbiton')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Language', 'en-GB')
+        self.assertBody('Welcome to Surbiton, pop. 1327')
diff --git a/libraries/cherrypy/test/test_session.py b/libraries/cherrypy/test/test_session.py
new file mode 100644
index 00000000..0083c97c
--- /dev/null
+++ b/libraries/cherrypy/test/test_session.py
@@ -0,0 +1,512 @@
+import os
+import threading
+import time
+import socket
+import importlib
+
+from six.moves.http_client import HTTPConnection
+
+import pytest
+from path import Path
+
+import cherrypy
+from cherrypy._cpcompat import (
+    json_decode,
+    HTTPSConnection,
+)
+from cherrypy.lib import sessions
+from cherrypy.lib import reprconf
+from cherrypy.lib.httputil import response_codes
+from cherrypy.test import helper
+
+localDir = os.path.dirname(__file__)
+
+
+def http_methods_allowed(methods=['GET', 'HEAD']):
+    method = cherrypy.request.method.upper()
+    if method not in methods:
+        cherrypy.response.headers['Allow'] = ', '.join(methods)
+        raise cherrypy.HTTPError(405)
+
+
+cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed)
+
+
+def setup_server():
+
+    @cherrypy.config(**{
+        'tools.sessions.on': True,
+        'tools.sessions.storage_class': sessions.RamSession,
+        'tools.sessions.storage_path': localDir,
+        'tools.sessions.timeout': (1.0 / 60),
+        'tools.sessions.clean_freq': (1.0 / 60),
+    })
+    class Root:
+
+        @cherrypy.expose
+        def clear(self):
+            cherrypy.session.cache.clear()
+
+        @cherrypy.expose
+        def data(self):
+            cherrypy.session['aha'] = 'foo'
+            return repr(cherrypy.session._data)
+
+        @cherrypy.expose
+        def testGen(self):
+            counter = cherrypy.session.get('counter', 0) + 1
+            cherrypy.session['counter'] = counter
+            yield str(counter)
+
+        @cherrypy.expose
+        def testStr(self):
+            counter = cherrypy.session.get('counter', 0) + 1
+            cherrypy.session['counter'] = counter
+            return str(counter)
+
+        @cherrypy.expose
+        @cherrypy.config(**{'tools.sessions.on': False})
+        def set_session_cls(self, new_cls_name):
+            new_cls = reprconf.attributes(new_cls_name)
+            cfg = {'tools.sessions.storage_class': new_cls}
+            self.__class__._cp_config.update(cfg)
+            if hasattr(cherrypy, 'session'):
+                del cherrypy.session
+            if new_cls.clean_thread:
+                new_cls.clean_thread.stop()
+                new_cls.clean_thread.unsubscribe()
+                del new_cls.clean_thread
+
+        @cherrypy.expose
+        def index(self):
+            sess = cherrypy.session
+            c = sess.get('counter', 0) + 1
+            time.sleep(0.01)
+            sess['counter'] = c
+            return str(c)
+
+        @cherrypy.expose
+        def keyin(self, key):
+            return str(key in cherrypy.session)
+
+        @cherrypy.expose
+        def delete(self):
+            cherrypy.session.delete()
+            sessions.expire()
+            return 'done'
+
+        @cherrypy.expose
+        def delkey(self, key):
+            del cherrypy.session[key]
+            return 'OK'
+
+        @cherrypy.expose
+        def redir_target(self):
+            return self._cp_config['tools.sessions.storage_class'].__name__
+
+        @cherrypy.expose
+        def iredir(self):
+            raise cherrypy.InternalRedirect('/redir_target')
+
+        @cherrypy.expose
+        @cherrypy.config(**{
+            'tools.allow.on': True,
+            'tools.allow.methods': ['GET'],
+        })
+        def restricted(self):
+            return cherrypy.request.method
+
+        @cherrypy.expose
+        def regen(self):
+            cherrypy.tools.sessions.regenerate()
+            return 'logged in'
+
+        @cherrypy.expose
+        def length(self):
+            return str(len(cherrypy.session))
+
+        @cherrypy.expose
+        @cherrypy.config(**{
+            'tools.sessions.path': '/session_cookie',
+            'tools.sessions.name': 'temp',
+            'tools.sessions.persistent': False,
+        })
+        def session_cookie(self):
+            # Must load() to start the clean thread.
+            cherrypy.session.load()
+            return cherrypy.session.id
+
+    cherrypy.tree.mount(Root())
+
+
+class SessionTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def tearDown(self):
+        # Clean up sessions.
+        for fname in os.listdir(localDir):
+            if fname.startswith(sessions.FileSession.SESSION_PREFIX):
+                path = Path(localDir) / fname
+                path.remove_p()
+
+    @pytest.mark.xfail(reason='#1534')
+    def test_0_Session(self):
+        self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
+        self.getPage('/clear')
+
+        # Test that a normal request gets the same id in the cookies.
+        # Note: this wouldn't work if /data didn't load the session.
+        self.getPage('/data')
+        self.assertBody("{'aha': 'foo'}")
+        c = self.cookies[0]
+        self.getPage('/data', self.cookies)
+        self.assertEqual(self.cookies[0], c)
+
+        self.getPage('/testStr')
+        self.assertBody('1')
+        cookie_parts = dict([p.strip().split('=')
+                             for p in self.cookies[0][1].split(';')])
+        # Assert there is an 'expires' param
+        self.assertEqual(set(cookie_parts.keys()),
+                         set(['session_id', 'expires', 'Path']))
+        self.getPage('/testGen', self.cookies)
+        self.assertBody('2')
+        self.getPage('/testStr', self.cookies)
+        self.assertBody('3')
+        self.getPage('/data', self.cookies)
+        self.assertDictEqual(json_decode(self.body),
+                             {'counter': 3, 'aha': 'foo'})
+        self.getPage('/length', self.cookies)
+        self.assertBody('2')
+        self.getPage('/delkey?key=counter', self.cookies)
+        self.assertStatus(200)
+
+        self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
+        self.getPage('/testStr')
+        self.assertBody('1')
+        self.getPage('/testGen', self.cookies)
+        self.assertBody('2')
+        self.getPage('/testStr', self.cookies)
+        self.assertBody('3')
+        self.getPage('/delkey?key=counter', self.cookies)
+        self.assertStatus(200)
+
+        # Wait for the session.timeout (1 second)
+        time.sleep(2)
+        self.getPage('/')
+        self.assertBody('1')
+        self.getPage('/length', self.cookies)
+        self.assertBody('1')
+
+        # Test session __contains__
+        self.getPage('/keyin?key=counter', self.cookies)
+        self.assertBody('True')
+        cookieset1 = self.cookies
+
+        # Make a new session and test __len__ again
+        self.getPage('/')
+        self.getPage('/length', self.cookies)
+        self.assertBody('2')
+
+        # Test session delete
+        self.getPage('/delete', self.cookies)
+        self.assertBody('done')
+        self.getPage('/delete', cookieset1)
+        self.assertBody('done')
+
+        def f():
+            return [
+                x
+                for x in os.listdir(localDir)
+                if x.startswith('session-')
+            ]
+        self.assertEqual(f(), [])
+
+        # Wait for the cleanup thread to delete remaining session files
+        self.getPage('/')
+        self.assertNotEqual(f(), [])
+        time.sleep(2)
+        self.assertEqual(f(), [])
+
+    def test_1_Ram_Concurrency(self):
+        self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
+        self._test_Concurrency()
+
+    @pytest.mark.xfail(reason='#1306')
+    def test_2_File_Concurrency(self):
+        self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
+        self._test_Concurrency()
+
+    def _test_Concurrency(self):
+        client_thread_count = 5
+        request_count = 30
+
+        # Get initial cookie
+        self.getPage('/')
+        self.assertBody('1')
+        cookies = self.cookies
+
+        data_dict = {}
+        errors = []
+
+        def request(index):
+            if self.scheme == 'https':
+                c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
+            else:
+                c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
+            for i in range(request_count):
+                c.putrequest('GET', '/')
+                for k, v in cookies:
+                    c.putheader(k, v)
+                c.endheaders()
+                response = c.getresponse()
+                body = response.read()
+                if response.status != 200 or not body.isdigit():
+                    errors.append((response.status, body))
+                else:
+                    data_dict[index] = max(data_dict[index], int(body))
+                # Uncomment the following line to prove threads overlap.
+                # sys.stdout.write("%d " % index)
+
+        # Start <request_count> requests from each of
+        # <client_thread_count> concurrent clients
+        ts = []
+        for c in range(client_thread_count):
+            data_dict[c] = 0
+            t = threading.Thread(target=request, args=(c,))
+            ts.append(t)
+            t.start()
+
+        for t in ts:
+            t.join()
+
+        hitcount = max(data_dict.values())
+        expected = 1 + (client_thread_count * request_count)
+
+        for e in errors:
+            print(e)
+        self.assertEqual(hitcount, expected)
+
+    def test_3_Redirect(self):
+        # Start a new session
+        self.getPage('/testStr')
+        self.getPage('/iredir', self.cookies)
+        self.assertBody('FileSession')
+
+    def test_4_File_deletion(self):
+        # Start a new session
+        self.getPage('/testStr')
+        # Delete the session file manually and retry.
+        id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
+        path = os.path.join(localDir, 'session-' + id)
+        os.unlink(path)
+        self.getPage('/testStr', self.cookies)
+
+    def test_5_Error_paths(self):
+        self.getPage('/unknown/page')
+        self.assertErrorPage(404, "The path '/unknown/page' was not found.")
+
+        # Note: this path is *not* the same as above. The above
+        # takes a normal route through the session code; this one
+        # skips the session code's before_handler and only calls
+        # before_finalize (save) and on_end (close). So the session
+        # code has to survive calling save/close without init.
+        self.getPage('/restricted', self.cookies, method='POST')
+        self.assertErrorPage(405, response_codes[405][1])
+
+    def test_6_regenerate(self):
+        self.getPage('/testStr')
+        # grab the cookie ID
+        id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
+        self.getPage('/regen')
+        self.assertBody('logged in')
+        id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
+        self.assertNotEqual(id1, id2)
+
+        self.getPage('/testStr')
+        # grab the cookie ID
+        id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
+        self.getPage('/testStr',
+                     headers=[
+                         ('Cookie',
+                          'session_id=maliciousid; '
+                          'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
+        id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
+        self.assertNotEqual(id1, id2)
+        self.assertNotEqual(id2, 'maliciousid')
+
+    def test_7_session_cookies(self):
+        self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
+        self.getPage('/clear')
+        self.getPage('/session_cookie')
+        # grab the cookie ID
+        cookie_parts = dict([p.strip().split('=')
+                            for p in self.cookies[0][1].split(';')])
+        # Assert there is no 'expires' param
+        self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
+        id1 = cookie_parts['temp']
+        self.assertEqual(list(sessions.RamSession.cache), [id1])
+
+        # Send another request in the same "browser session".
+        self.getPage('/session_cookie', self.cookies)
+        cookie_parts = dict([p.strip().split('=')
+                            for p in self.cookies[0][1].split(';')])
+        # Assert there is no 'expires' param
+        self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
+        self.assertBody(id1)
+        self.assertEqual(list(sessions.RamSession.cache), [id1])
+
+        # Simulate a browser close by just not sending the cookies
+        self.getPage('/session_cookie')
+        # grab the cookie ID
+        cookie_parts = dict([p.strip().split('=')
+                            for p in self.cookies[0][1].split(';')])
+        # Assert there is no 'expires' param
+        self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
+        # Assert a new id has been generated...
+        id2 = cookie_parts['temp']
+        self.assertNotEqual(id1, id2)
+        self.assertEqual(set(sessions.RamSession.cache.keys()),
+                         set([id1, id2]))
+
+        # Wait for the session.timeout on both sessions
+        time.sleep(2.5)
+        cache = list(sessions.RamSession.cache)
+        if cache:
+            if cache == [id2]:
+                self.fail('The second session did not time out.')
+            else:
+                self.fail('Unknown session id in cache: %r', cache)
+
+    def test_8_Ram_Cleanup(self):
+        def lock():
+            s1 = sessions.RamSession()
+            s1.acquire_lock()
+            time.sleep(1)
+            s1.release_lock()
+
+        t = threading.Thread(target=lock)
+        t.start()
+        start = time.time()
+        while not sessions.RamSession.locks and time.time() - start < 5:
+            time.sleep(0.01)
+        assert len(sessions.RamSession.locks) == 1, 'Lock not acquired'
+        s2 = sessions.RamSession()
+        s2.clean_up()
+        msg = 'Clean up should not remove active lock'
+        assert len(sessions.RamSession.locks) == 1, msg
+        t.join()
+
+
+try:
+    importlib.import_module('memcache')
+
+    host, port = '127.0.0.1', 11211
+    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
+                                  socket.SOCK_STREAM):
+        af, socktype, proto, canonname, sa = res
+        s = None
+        try:
+            s = socket.socket(af, socktype, proto)
+            # See http://groups.google.com/group/cherrypy-users/
+            #        browse_frm/thread/bbfe5eb39c904fe0
+            s.settimeout(1.0)
+            s.connect((host, port))
+            s.close()
+        except socket.error:
+            if s:
+                s.close()
+            raise
+        break
+except (ImportError, socket.error):
+    class MemcachedSessionTest(helper.CPWebCase):
+        setup_server = staticmethod(setup_server)
+
+        def test(self):
+            return self.skip('memcached not reachable ')
+else:
+    class MemcachedSessionTest(helper.CPWebCase):
+        setup_server = staticmethod(setup_server)
+
+        def test_0_Session(self):
+            self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession')
+
+            self.getPage('/testStr')
+            self.assertBody('1')
+            self.getPage('/testGen', self.cookies)
+            self.assertBody('2')
+            self.getPage('/testStr', self.cookies)
+            self.assertBody('3')
+            self.getPage('/length', self.cookies)
+            self.assertErrorPage(500)
+            self.assertInBody('NotImplementedError')
+            self.getPage('/delkey?key=counter', self.cookies)
+            self.assertStatus(200)
+
+            # Wait for the session.timeout (1 second)
+            time.sleep(1.25)
+            self.getPage('/')
+            self.assertBody('1')
+
+            # Test session __contains__
+            self.getPage('/keyin?key=counter', self.cookies)
+            self.assertBody('True')
+
+            # Test session delete
+            self.getPage('/delete', self.cookies)
+            self.assertBody('done')
+
+        def test_1_Concurrency(self):
+            client_thread_count = 5
+            request_count = 30
+
+            # Get initial cookie
+            self.getPage('/')
+            self.assertBody('1')
+            cookies = self.cookies
+
+            data_dict = {}
+
+            def request(index):
+                for i in range(request_count):
+                    self.getPage('/', cookies)
+                    # Uncomment the following line to prove threads overlap.
+                    # sys.stdout.write("%d " % index)
+                if not self.body.isdigit():
+                    self.fail(self.body)
+                data_dict[index] = int(self.body)
+
+            # Start <request_count> concurrent requests from
+            # each of <client_thread_count> clients
+            ts = []
+            for c in range(client_thread_count):
+                data_dict[c] = 0
+                t = threading.Thread(target=request, args=(c,))
+                ts.append(t)
+                t.start()
+
+            for t in ts:
+                t.join()
+
+            hitcount = max(data_dict.values())
+            expected = 1 + (client_thread_count * request_count)
+            self.assertEqual(hitcount, expected)
+
+        def test_3_Redirect(self):
+            # Start a new session
+            self.getPage('/testStr')
+            self.getPage('/iredir', self.cookies)
+            self.assertBody('memcached')
+
+        def test_5_Error_paths(self):
+            self.getPage('/unknown/page')
+            self.assertErrorPage(
+                404, "The path '/unknown/page' was not found.")
+
+            # Note: this path is *not* the same as above. The above
+            # takes a normal route through the session code; this one
+            # skips the session code's before_handler and only calls
+            # before_finalize (save) and on_end (close). So the session
+            # code has to survive calling save/close without init.
+            self.getPage('/restricted', self.cookies, method='POST')
+            self.assertErrorPage(405, response_codes[405][1])
diff --git a/libraries/cherrypy/test/test_sessionauthenticate.py b/libraries/cherrypy/test/test_sessionauthenticate.py
new file mode 100644
index 00000000..63053fcb
--- /dev/null
+++ b/libraries/cherrypy/test/test_sessionauthenticate.py
@@ -0,0 +1,61 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class SessionAuthenticateTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        def check(username, password):
+            # Dummy check_username_and_password function
+            if username != 'test' or password != 'password':
+                return 'Wrong login/password'
+
+        def augment_params():
+            # A simple tool to add some things to request.params
+            # This is to check to make sure that session_auth can handle
+            # request params (ticket #780)
+            cherrypy.request.params['test'] = 'test'
+
+        cherrypy.tools.augment_params = cherrypy.Tool(
+            'before_handler', augment_params, None, priority=30)
+
+        class Test:
+
+            _cp_config = {
+                'tools.sessions.on': True,
+                'tools.session_auth.on': True,
+                'tools.session_auth.check_username_and_password': check,
+                'tools.augment_params.on': True,
+            }
+
+            @cherrypy.expose
+            def index(self, **kwargs):
+                return 'Hi %s, you are logged in' % cherrypy.request.login
+
+        cherrypy.tree.mount(Test())
+
+    def testSessionAuthenticate(self):
+        # request a page and check for login form
+        self.getPage('/')
+        self.assertInBody('<form method="post" action="do_login">')
+
+        # setup credentials
+        login_body = 'username=test&password=password&from_page=/'
+
+        # attempt a login
+        self.getPage('/do_login', method='POST', body=login_body)
+        self.assertStatus((302, 303))
+
+        # get the page now that we are logged in
+        self.getPage('/', self.cookies)
+        self.assertBody('Hi test, you are logged in')
+
+        # do a logout
+        self.getPage('/do_logout', self.cookies, method='POST')
+        self.assertStatus((302, 303))
+
+        # verify we are logged out
+        self.getPage('/', self.cookies)
+        self.assertInBody('<form method="post" action="do_login">')
diff --git a/libraries/cherrypy/test/test_states.py b/libraries/cherrypy/test/test_states.py
new file mode 100644
index 00000000..606ca4f6
--- /dev/null
+++ b/libraries/cherrypy/test/test_states.py
@@ -0,0 +1,473 @@
+import os
+import signal
+import time
+import unittest
+import warnings
+
+from six.moves.http_client import BadStatusLine
+
+import pytest
+import portend
+
+import cherrypy
+import cherrypy.process.servers
+from cherrypy.test import helper
+
+engine = cherrypy.engine
+thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+class Dependency:
+
+    def __init__(self, bus):
+        self.bus = bus
+        self.running = False
+        self.startcount = 0
+        self.gracecount = 0
+        self.threads = {}
+
+    def subscribe(self):
+        self.bus.subscribe('start', self.start)
+        self.bus.subscribe('stop', self.stop)
+        self.bus.subscribe('graceful', self.graceful)
+        self.bus.subscribe('start_thread', self.startthread)
+        self.bus.subscribe('stop_thread', self.stopthread)
+
+    def start(self):
+        self.running = True
+        self.startcount += 1
+
+    def stop(self):
+        self.running = False
+
+    def graceful(self):
+        self.gracecount += 1
+
+    def startthread(self, thread_id):
+        self.threads[thread_id] = None
+
+    def stopthread(self, thread_id):
+        del self.threads[thread_id]
+
+
+db_connection = Dependency(engine)
+
+
+def setup_server():
+    class Root:
+
+        @cherrypy.expose
+        def index(self):
+            return 'Hello World'
+
+        @cherrypy.expose
+        def ctrlc(self):
+            raise KeyboardInterrupt()
+
+        @cherrypy.expose
+        def graceful(self):
+            engine.graceful()
+            return 'app was (gracefully) restarted succesfully'
+
+    cherrypy.tree.mount(Root())
+    cherrypy.config.update({
+        'environment': 'test_suite',
+    })
+
+    db_connection.subscribe()
+
+# ------------ Enough helpers. Time for real live test cases. ------------ #
+
+
+class ServerStateTests(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def setUp(self):
+        cherrypy.server.socket_timeout = 0.1
+        self.do_gc_test = False
+
+    def test_0_NormalStateFlow(self):
+        engine.stop()
+        # Our db_connection should not be running
+        self.assertEqual(db_connection.running, False)
+        self.assertEqual(db_connection.startcount, 1)
+        self.assertEqual(len(db_connection.threads), 0)
+
+        # Test server start
+        engine.start()
+        self.assertEqual(engine.state, engine.states.STARTED)
+
+        host = cherrypy.server.socket_host
+        port = cherrypy.server.socket_port
+        portend.occupied(host, port, timeout=0.1)
+
+        # The db_connection should be running now
+        self.assertEqual(db_connection.running, True)
+        self.assertEqual(db_connection.startcount, 2)
+        self.assertEqual(len(db_connection.threads), 0)
+
+        self.getPage('/')
+        self.assertBody('Hello World')
+        self.assertEqual(len(db_connection.threads), 1)
+
+        # Test engine stop. This will also stop the HTTP server.
+        engine.stop()
+        self.assertEqual(engine.state, engine.states.STOPPED)
+
+        # Verify that our custom stop function was called
+        self.assertEqual(db_connection.running, False)
+        self.assertEqual(len(db_connection.threads), 0)
+
+        # Block the main thread now and verify that exit() works.
+        def exittest():
+            self.getPage('/')
+            self.assertBody('Hello World')
+            engine.exit()
+        cherrypy.server.start()
+        engine.start_with_callback(exittest)
+        engine.block()
+        self.assertEqual(engine.state, engine.states.EXITING)
+
+    def test_1_Restart(self):
+        cherrypy.server.start()
+        engine.start()
+
+        # The db_connection should be running now
+        self.assertEqual(db_connection.running, True)
+        grace = db_connection.gracecount
+
+        self.getPage('/')
+        self.assertBody('Hello World')
+        self.assertEqual(len(db_connection.threads), 1)
+
+        # Test server restart from this thread
+        engine.graceful()
+        self.assertEqual(engine.state, engine.states.STARTED)
+        self.getPage('/')
+        self.assertBody('Hello World')
+        self.assertEqual(db_connection.running, True)
+        self.assertEqual(db_connection.gracecount, grace + 1)
+        self.assertEqual(len(db_connection.threads), 1)
+
+        # Test server restart from inside a page handler
+        self.getPage('/graceful')
+        self.assertEqual(engine.state, engine.states.STARTED)
+        self.assertBody('app was (gracefully) restarted succesfully')
+        self.assertEqual(db_connection.running, True)
+        self.assertEqual(db_connection.gracecount, grace + 2)
+        # Since we are requesting synchronously, is only one thread used?
+        # Note that the "/graceful" request has been flushed.
+        self.assertEqual(len(db_connection.threads), 0)
+
+        engine.stop()
+        self.assertEqual(engine.state, engine.states.STOPPED)
+        self.assertEqual(db_connection.running, False)
+        self.assertEqual(len(db_connection.threads), 0)
+
+    def test_2_KeyboardInterrupt(self):
+        # Raise a keyboard interrupt in the HTTP server's main thread.
+        # We must start the server in this, the main thread
+        engine.start()
+        cherrypy.server.start()
+
+        self.persistent = True
+        try:
+            # Make the first request and assert there's no "Connection: close".
+            self.getPage('/')
+            self.assertStatus('200 OK')
+            self.assertBody('Hello World')
+            self.assertNoHeader('Connection')
+
+            cherrypy.server.httpserver.interrupt = KeyboardInterrupt
+            engine.block()
+
+            self.assertEqual(db_connection.running, False)
+            self.assertEqual(len(db_connection.threads), 0)
+            self.assertEqual(engine.state, engine.states.EXITING)
+        finally:
+            self.persistent = False
+
+        # Raise a keyboard interrupt in a page handler; on multithreaded
+        # servers, this should occur in one of the worker threads.
+        # This should raise a BadStatusLine error, since the worker
+        # thread will just die without writing a response.
+        engine.start()
+        cherrypy.server.start()
+        # From python3.5 a new exception is retuned when the connection
+        # ends abruptly:
+        #   http.client.RemoteDisconnected
+        # RemoteDisconnected is a subclass of:
+        #   (ConnectionResetError, http.client.BadStatusLine)
+        # and ConnectionResetError is an indirect subclass of:
+        #    OSError
+        # From python 3.3 an up socket.error is an alias to OSError
+        # following PEP-3151, therefore http.client.RemoteDisconnected
+        # is considered a socket.error.
+        #
+        # raise_subcls specifies the classes that are not going
+        # to be considered as a socket.error for the retries.
+        # Given that RemoteDisconnected is part BadStatusLine
+        # we can use the same call for all py3 versions without
+        # sideffects. python < 3.5 will raise directly BadStatusLine
+        # which is not a subclass for socket.error/OSError.
+        try:
+            self.getPage('/ctrlc', raise_subcls=BadStatusLine)
+        except BadStatusLine:
+            pass
+        else:
+            print(self.body)
+            self.fail('AssertionError: BadStatusLine not raised')
+
+        engine.block()
+        self.assertEqual(db_connection.running, False)
+        self.assertEqual(len(db_connection.threads), 0)
+
+    @pytest.mark.xfail(
+        'sys.platform == "Darwin" '
+        'and sys.version_info > (3, 7) '
+        'and os.environ["TRAVIS"]',
+        reason='https://github.com/cherrypy/cherrypy/issues/1693',
+    )
+    def test_4_Autoreload(self):
+        # If test_3 has not been executed, the server won't be stopped,
+        # so we'll have to do it.
+        if engine.state != engine.states.EXITING:
+            engine.exit()
+
+        # Start the demo script in a new process
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
+        p.write_conf(extra='test_case_name: "test_4_Autoreload"')
+        p.start(imports='cherrypy.test._test_states_demo')
+        try:
+            self.getPage('/start')
+            start = float(self.body)
+
+            # Give the autoreloader time to cache the file time.
+            time.sleep(2)
+
+            # Touch the file
+            os.utime(os.path.join(thisdir, '_test_states_demo.py'), None)
+
+            # Give the autoreloader time to re-exec the process
+            time.sleep(2)
+            host = cherrypy.server.socket_host
+            port = cherrypy.server.socket_port
+            portend.occupied(host, port, timeout=5)
+
+            self.getPage('/start')
+            if not (float(self.body) > start):
+                raise AssertionError('start time %s not greater than %s' %
+                                     (float(self.body), start))
+        finally:
+            # Shut down the spawned process
+            self.getPage('/exit')
+        p.join()
+
+    def test_5_Start_Error(self):
+        # If test_3 has not been executed, the server won't be stopped,
+        # so we'll have to do it.
+        if engine.state != engine.states.EXITING:
+            engine.exit()
+
+        # If a process errors during start, it should stop the engine
+        # and exit with a non-zero exit code.
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
+                             wait=True)
+        p.write_conf(
+            extra="""starterror: True
+test_case_name: "test_5_Start_Error"
+"""
+        )
+        p.start(imports='cherrypy.test._test_states_demo')
+        if p.exit_code == 0:
+            self.fail('Process failed to return nonzero exit code.')
+
+
+class PluginTests(helper.CPWebCase):
+
+    def test_daemonize(self):
+        if os.name not in ['posix']:
+            return self.skip('skipped (not on posix) ')
+        self.HOST = '127.0.0.1'
+        self.PORT = 8081
+        # Spawn the process and wait, when this returns, the original process
+        # is finished.  If it daemonized properly, we should still be able
+        # to access pages.
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
+                             wait=True, daemonize=True,
+                             socket_host='127.0.0.1',
+                             socket_port=8081)
+        p.write_conf(
+            extra='test_case_name: "test_daemonize"')
+        p.start(imports='cherrypy.test._test_states_demo')
+        try:
+            # Just get the pid of the daemonization process.
+            self.getPage('/pid')
+            self.assertStatus(200)
+            page_pid = int(self.body)
+            self.assertEqual(page_pid, p.get_pid())
+        finally:
+            # Shut down the spawned process
+            self.getPage('/exit')
+        p.join()
+
+        # Wait until here to test the exit code because we want to ensure
+        # that we wait for the daemon to finish running before we fail.
+        if p.exit_code != 0:
+            self.fail('Daemonized parent process failed to exit cleanly.')
+
+
+class SignalHandlingTests(helper.CPWebCase):
+
+    def test_SIGHUP_tty(self):
+        # When not daemonized, SIGHUP should shut down the server.
+        try:
+            from signal import SIGHUP
+        except ImportError:
+            return self.skip('skipped (no SIGHUP) ')
+
+        # Spawn the process.
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
+        p.write_conf(
+            extra='test_case_name: "test_SIGHUP_tty"')
+        p.start(imports='cherrypy.test._test_states_demo')
+        # Send a SIGHUP
+        os.kill(p.get_pid(), SIGHUP)
+        # This might hang if things aren't working right, but meh.
+        p.join()
+
+    def test_SIGHUP_daemonized(self):
+        # When daemonized, SIGHUP should restart the server.
+        try:
+            from signal import SIGHUP
+        except ImportError:
+            return self.skip('skipped (no SIGHUP) ')
+
+        if os.name not in ['posix']:
+            return self.skip('skipped (not on posix) ')
+
+        # Spawn the process and wait, when this returns, the original process
+        # is finished.  If it daemonized properly, we should still be able
+        # to access pages.
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
+                             wait=True, daemonize=True)
+        p.write_conf(
+            extra='test_case_name: "test_SIGHUP_daemonized"')
+        p.start(imports='cherrypy.test._test_states_demo')
+
+        pid = p.get_pid()
+        try:
+            # Send a SIGHUP
+            os.kill(pid, SIGHUP)
+            # Give the server some time to restart
+            time.sleep(2)
+            self.getPage('/pid')
+            self.assertStatus(200)
+            new_pid = int(self.body)
+            self.assertNotEqual(new_pid, pid)
+        finally:
+            # Shut down the spawned process
+            self.getPage('/exit')
+        p.join()
+
+    def _require_signal_and_kill(self, signal_name):
+        if not hasattr(signal, signal_name):
+            self.skip('skipped (no %(signal_name)s)' % vars())
+
+        if not hasattr(os, 'kill'):
+            self.skip('skipped (no os.kill)')
+
+    def test_SIGTERM(self):
+        'SIGTERM should shut down the server whether daemonized or not.'
+        self._require_signal_and_kill('SIGTERM')
+
+        # Spawn a normal, undaemonized process.
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
+        p.write_conf(
+            extra='test_case_name: "test_SIGTERM"')
+        p.start(imports='cherrypy.test._test_states_demo')
+        # Send a SIGTERM
+        os.kill(p.get_pid(), signal.SIGTERM)
+        # This might hang if things aren't working right, but meh.
+        p.join()
+
+        if os.name in ['posix']:
+            # Spawn a daemonized process and test again.
+            p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
+                                 wait=True, daemonize=True)
+            p.write_conf(
+                extra='test_case_name: "test_SIGTERM_2"')
+            p.start(imports='cherrypy.test._test_states_demo')
+            # Send a SIGTERM
+            os.kill(p.get_pid(), signal.SIGTERM)
+            # This might hang if things aren't working right, but meh.
+            p.join()
+
+    def test_signal_handler_unsubscribe(self):
+        self._require_signal_and_kill('SIGTERM')
+
+        # Although Windows has `os.kill` and SIGTERM is defined, the
+        #  platform does not implement signals and sending SIGTERM
+        #  will result in a forced termination of the process.
+        #  Therefore, this test is not suitable for Windows.
+        if os.name == 'nt':
+            self.skip('SIGTERM not available')
+
+        # Spawn a normal, undaemonized process.
+        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
+        p.write_conf(
+            extra="""unsubsig: True
+test_case_name: "test_signal_handler_unsubscribe"
+""")
+        p.start(imports='cherrypy.test._test_states_demo')
+        # Ask the process to quit
+        os.kill(p.get_pid(), signal.SIGTERM)
+        # This might hang if things aren't working right, but meh.
+        p.join()
+
+        # Assert the old handler ran.
+        log_lines = list(open(p.error_log, 'rb'))
+        assert any(
+            line.endswith(b'I am an old SIGTERM handler.\n')
+            for line in log_lines
+        )
+
+
+class WaitTests(unittest.TestCase):
+
+    def test_safe_wait_INADDR_ANY(self):
+        """
+        Wait on INADDR_ANY should not raise IOError
+
+        In cases where the loopback interface does not exist, CherryPy cannot
+        effectively determine if a port binding to INADDR_ANY was effected.
+        In this situation, CherryPy should assume that it failed to detect
+        the binding (not that the binding failed) and only warn that it could
+        not verify it.
+        """
+        # At such a time that CherryPy can reliably determine one or more
+        #  viable IP addresses of the host, this test may be removed.
+
+        # Simulate the behavior we observe when no loopback interface is
+        #  present by: finding a port that's not occupied, then wait on it.
+
+        free_port = portend.find_available_local_port()
+
+        servers = cherrypy.process.servers
+
+        inaddr_any = '0.0.0.0'
+
+        # Wait on the free port that's unbound
+        with warnings.catch_warnings(record=True) as w:
+            with servers._safe_wait(inaddr_any, free_port):
+                portend.occupied(inaddr_any, free_port, timeout=1)
+            self.assertEqual(len(w), 1)
+            self.assertTrue(isinstance(w[0], warnings.WarningMessage))
+            self.assertTrue(
+                'Unable to verify that the server is bound on ' in str(w[0]))
+
+        # The wait should still raise an IO error if INADDR_ANY was
+        #  not supplied.
+        with pytest.raises(IOError):
+            with servers._safe_wait('127.0.0.1', free_port):
+                portend.occupied('127.0.0.1', free_port, timeout=1)
diff --git a/libraries/cherrypy/test/test_static.py b/libraries/cherrypy/test/test_static.py
new file mode 100644
index 00000000..5dc5a144
--- /dev/null
+++ b/libraries/cherrypy/test/test_static.py
@@ -0,0 +1,434 @@
+# -*- coding: utf-8 -*-
+import contextlib
+import io
+import os
+import sys
+import platform
+import tempfile
+
+from six import text_type as str
+from six.moves import urllib
+from six.moves.http_client import HTTPConnection
+
+import pytest
+import py.path
+
+import cherrypy
+from cherrypy.lib import static
+from cherrypy._cpcompat import HTTPSConnection, ntou, tonative
+from cherrypy.test import helper
+
+
+@pytest.fixture
+def unicode_filesystem(tmpdir):
+    filename = tmpdir / ntou('☃', 'utf-8')
+    tmpl = 'File system encoding ({encoding}) cannot support unicode filenames'
+    msg = tmpl.format(encoding=sys.getfilesystemencoding())
+    try:
+        io.open(str(filename), 'w').close()
+    except UnicodeEncodeError:
+        pytest.skip(msg)
+
+
+def ensure_unicode_filesystem():
+    """
+    TODO: replace with simply pytest fixtures once webtest.TestCase
+    no longer implies unittest.
+    """
+    tmpdir = py.path.local(tempfile.mkdtemp())
+    try:
+        unicode_filesystem(tmpdir)
+    finally:
+        tmpdir.remove()
+
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
+bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log')
+
+# The file size needs to be big enough such that half the size of it
+# won't be socket-buffered (or server-buffered) all in one go. See
+# test_file_stream.
+MB = 2 ** 20
+BIGFILE_SIZE = 32 * MB
+
+
+class StaticTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        if not os.path.exists(has_space_filepath):
+            with open(has_space_filepath, 'wb') as f:
+                f.write(b'Hello, world\r\n')
+        needs_bigfile = (
+            not os.path.exists(bigfile_filepath) or
+            os.path.getsize(bigfile_filepath) != BIGFILE_SIZE
+        )
+        if needs_bigfile:
+            with open(bigfile_filepath, 'wb') as f:
+                f.write(b'x' * BIGFILE_SIZE)
+
+        class Root:
+
+            @cherrypy.expose
+            @cherrypy.config(**{'response.stream': True})
+            def bigfile(self):
+                self.f = static.serve_file(bigfile_filepath)
+                return self.f
+
+            @cherrypy.expose
+            def tell(self):
+                if self.f.input.closed:
+                    return ''
+                return repr(self.f.input.tell()).rstrip('L')
+
+            @cherrypy.expose
+            def fileobj(self):
+                f = open(os.path.join(curdir, 'style.css'), 'rb')
+                return static.serve_fileobj(f, content_type='text/css')
+
+            @cherrypy.expose
+            def bytesio(self):
+                f = io.BytesIO(b'Fee\nfie\nfo\nfum')
+                return static.serve_fileobj(f, content_type='text/plain')
+
+        class Static:
+
+            @cherrypy.expose
+            def index(self):
+                return 'You want the Baron? You can have the Baron!'
+
+            @cherrypy.expose
+            def dynamic(self):
+                return 'This is a DYNAMIC page'
+
+        root = Root()
+        root.static = Static()
+
+        rootconf = {
+            '/static': {
+                'tools.staticdir.on': True,
+                'tools.staticdir.dir': 'static',
+                'tools.staticdir.root': curdir,
+            },
+            '/static-long': {
+                'tools.staticdir.on': True,
+                'tools.staticdir.dir': r'\\?\%s' % curdir,
+            },
+            '/style.css': {
+                'tools.staticfile.on': True,
+                'tools.staticfile.filename': os.path.join(curdir, 'style.css'),
+            },
+            '/docroot': {
+                'tools.staticdir.on': True,
+                'tools.staticdir.root': curdir,
+                'tools.staticdir.dir': 'static',
+                'tools.staticdir.index': 'index.html',
+            },
+            '/error': {
+                'tools.staticdir.on': True,
+                'request.show_tracebacks': True,
+            },
+            '/404test': {
+                'tools.staticdir.on': True,
+                'tools.staticdir.root': curdir,
+                'tools.staticdir.dir': 'static',
+                'error_page.404': error_page_404,
+            }
+        }
+        rootApp = cherrypy.Application(root)
+        rootApp.merge(rootconf)
+
+        test_app_conf = {
+            '/test': {
+                'tools.staticdir.index': 'index.html',
+                'tools.staticdir.on': True,
+                'tools.staticdir.root': curdir,
+                'tools.staticdir.dir': 'static',
+            },
+        }
+        testApp = cherrypy.Application(Static())
+        testApp.merge(test_app_conf)
+
+        vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
+        cherrypy.tree.graft(vhost)
+
+    @staticmethod
+    def teardown_server():
+        for f in (has_space_filepath, bigfile_filepath):
+            if os.path.exists(f):
+                try:
+                    os.unlink(f)
+                except Exception:
+                    pass
+
+    def test_static(self):
+        self.getPage('/static/index.html')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html')
+        self.assertBody('Hello, world\r\n')
+
+        # Using a staticdir.root value in a subdir...
+        self.getPage('/docroot/index.html')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html')
+        self.assertBody('Hello, world\r\n')
+
+        # Check a filename with spaces in it
+        self.getPage('/static/has%20space.html')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html')
+        self.assertBody('Hello, world\r\n')
+
+        self.getPage('/style.css')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/css')
+        # Note: The body should be exactly 'Dummy stylesheet\n', but
+        #   unfortunately some tools such as WinZip sometimes turn \n
+        #   into \r\n on Windows when extracting the CherryPy tarball so
+        #   we just check the content
+        self.assertMatchesBody('^Dummy stylesheet')
+
+    @pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only')
+    def test_static_longpath(self):
+        """Test serving of a file in subdir of a Windows long-path
+        staticdir."""
+        self.getPage('/static-long/static/index.html')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html')
+        self.assertBody('Hello, world\r\n')
+
+    def test_fallthrough(self):
+        # Test that NotFound will then try dynamic handlers (see [878]).
+        self.getPage('/static/dynamic')
+        self.assertBody('This is a DYNAMIC page')
+
+        # Check a directory via fall-through to dynamic handler.
+        self.getPage('/static/')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
+        self.assertBody('You want the Baron? You can have the Baron!')
+
+    def test_index(self):
+        # Check a directory via "staticdir.index".
+        self.getPage('/docroot/')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/html')
+        self.assertBody('Hello, world\r\n')
+        # The same page should be returned even if redirected.
+        self.getPage('/docroot')
+        self.assertStatus(301)
+        self.assertHeader('Location', '%s/docroot/' % self.base())
+        self.assertMatchesBody(
+            "This resource .* <a href=(['\"])%s/docroot/\\1>"
+            '%s/docroot/</a>.'
+            % (self.base(), self.base())
+        )
+
+    def test_config_errors(self):
+        # Check that we get an error if no .file or .dir
+        self.getPage('/error/thing.html')
+        self.assertErrorPage(500)
+        if sys.version_info >= (3, 3):
+            errmsg = (
+                r'TypeError: staticdir\(\) missing 2 '
+                'required positional arguments'
+            )
+        else:
+            errmsg = (
+                r'TypeError: staticdir\(\) takes at least 2 '
+                r'(positional )?arguments \(0 given\)'
+            )
+        self.assertMatchesBody(errmsg.encode('ascii'))
+
+    def test_security(self):
+        # Test up-level security
+        self.getPage('/static/../../test/style.css')
+        self.assertStatus((400, 403))
+
+    def test_modif(self):
+        # Test modified-since on a reasonably-large file
+        self.getPage('/static/dirback.jpg')
+        self.assertStatus('200 OK')
+        lastmod = ''
+        for k, v in self.headers:
+            if k == 'Last-Modified':
+                lastmod = v
+        ims = ('If-Modified-Since', lastmod)
+        self.getPage('/static/dirback.jpg', headers=[ims])
+        self.assertStatus(304)
+        self.assertNoHeader('Content-Type')
+        self.assertNoHeader('Content-Length')
+        self.assertNoHeader('Content-Disposition')
+        self.assertBody('')
+
+    def test_755_vhost(self):
+        self.getPage('/test/', [('Host', 'virt.net')])
+        self.assertStatus(200)
+        self.getPage('/test', [('Host', 'virt.net')])
+        self.assertStatus(301)
+        self.assertHeader('Location', self.scheme + '://virt.net/test/')
+
+    def test_serve_fileobj(self):
+        self.getPage('/fileobj')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/css;charset=utf-8')
+        self.assertMatchesBody('^Dummy stylesheet')
+
+    def test_serve_bytesio(self):
+        self.getPage('/bytesio')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
+        self.assertHeader('Content-Length', 14)
+        self.assertMatchesBody('Fee\nfie\nfo\nfum')
+
+    @pytest.mark.xfail(reason='#1475')
+    def test_file_stream(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Make an initial request
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('GET', '/bigfile', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 200)
+
+        body = b''
+        remaining = BIGFILE_SIZE
+        while remaining > 0:
+            data = response.fp.read(65536)
+            if not data:
+                break
+            body += data
+            remaining -= len(data)
+
+            if self.scheme == 'https':
+                newconn = HTTPSConnection
+            else:
+                newconn = HTTPConnection
+            s, h, b = helper.webtest.openURL(
+                b'/tell', headers=[], host=self.HOST, port=self.PORT,
+                http_conn=newconn)
+            if not b:
+                # The file was closed on the server.
+                tell_position = BIGFILE_SIZE
+            else:
+                tell_position = int(b)
+
+            read_so_far = len(body)
+
+            # It is difficult for us to force the server to only read
+            # the bytes that we ask for - there are going to be buffers
+            # inbetween.
+            #
+            # CherryPy will attempt to write as much data as it can to
+            # the socket, and we don't have a way to determine what that
+            # size will be. So we make the following assumption - by
+            # the time we have read in the entire file on the server,
+            # we will have at least received half of it. If this is not
+            # the case, then this is an indicator that either:
+            #   - machines that are running this test are using buffer
+            #     sizes greater than half of BIGFILE_SIZE; or
+            #   - streaming is broken.
+            #
+            # At the time of writing, we seem to have encountered
+            # buffer sizes bigger than 512K, so we've increased
+            # BIGFILE_SIZE to 4MB and in 2016 to 20MB and then 32MB.
+            # This test is going to keep failing according to the
+            # improvements in hardware and OS buffers.
+            if tell_position >= BIGFILE_SIZE:
+                if read_so_far < (BIGFILE_SIZE / 2):
+                    self.fail(
+                        'The file should have advanced to position %r, but '
+                        'has already advanced to the end of the file. It '
+                        'may not be streamed as intended, or at the wrong '
+                        'chunk size (64k)' % read_so_far)
+            elif tell_position < read_so_far:
+                self.fail(
+                    'The file should have advanced to position %r, but has '
+                    'only advanced to position %r. It may not be streamed '
+                    'as intended, or at the wrong chunk size (64k)' %
+                    (read_so_far, tell_position))
+
+        if body != b'x' * BIGFILE_SIZE:
+            self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+                      (BIGFILE_SIZE, body[:50], len(body)))
+        conn.close()
+
+    def test_file_stream_deadlock(self):
+        if cherrypy.server.protocol_version != 'HTTP/1.1':
+            return self.skip()
+
+        self.PROTOCOL = 'HTTP/1.1'
+
+        # Make an initial request but abort early.
+        self.persistent = True
+        conn = self.HTTP_CONN
+        conn.putrequest('GET', '/bigfile', skip_host=True)
+        conn.putheader('Host', self.HOST)
+        conn.endheaders()
+        response = conn.response_class(conn.sock, method='GET')
+        response.begin()
+        self.assertEqual(response.status, 200)
+        body = response.fp.read(65536)
+        if body != b'x' * len(body):
+            self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+                      (65536, body[:50], len(body)))
+        response.close()
+        conn.close()
+
+        # Make a second request, which should fetch the whole file.
+        self.persistent = False
+        self.getPage('/bigfile')
+        if self.body != b'x' * BIGFILE_SIZE:
+            self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
+                      (BIGFILE_SIZE, self.body[:50], len(body)))
+
+    def test_error_page_with_serve_file(self):
+        self.getPage('/404test/yunyeen')
+        self.assertStatus(404)
+        self.assertInBody("I couldn't find that thing")
+
+    def test_null_bytes(self):
+        self.getPage('/static/\x00')
+        self.assertStatus('404 Not Found')
+
+    @staticmethod
+    @contextlib.contextmanager
+    def unicode_file():
+        filename = ntou('Слава Україні.html', 'utf-8')
+        filepath = os.path.join(curdir, 'static', filename)
+        with io.open(filepath, 'w', encoding='utf-8') as strm:
+            strm.write(ntou('Героям Слава!', 'utf-8'))
+        try:
+            yield
+        finally:
+            os.remove(filepath)
+
+    py27_on_windows = (
+        platform.system() == 'Windows' and
+        sys.version_info < (3,)
+    )
+    @pytest.mark.xfail(py27_on_windows, reason='#1544')  # noqa: E301
+    def test_unicode(self):
+        ensure_unicode_filesystem()
+        with self.unicode_file():
+            url = ntou('/static/Слава Україні.html', 'utf-8')
+            # quote function requires str
+            url = tonative(url, 'utf-8')
+            url = urllib.parse.quote(url)
+            self.getPage(url)
+
+            expected = ntou('Героям Слава!', 'utf-8')
+            self.assertInBody(expected)
+
+
+def error_page_404(status, message, traceback, version):
+    path = os.path.join(curdir, 'static', '404.html')
+    return static.serve_file(path, content_type='text/html')
diff --git a/libraries/cherrypy/test/test_tools.py b/libraries/cherrypy/test/test_tools.py
new file mode 100644
index 00000000..a73a3898
--- /dev/null
+++ b/libraries/cherrypy/test/test_tools.py
@@ -0,0 +1,468 @@
+"""Test the various means of instantiating and invoking tools."""
+
+import gzip
+import io
+import sys
+import time
+import types
+import unittest
+import operator
+
+import six
+from six.moves import range, map
+from six.moves.http_client import IncompleteRead
+
+import cherrypy
+from cherrypy import tools
+from cherrypy._cpcompat import ntou
+from cherrypy.test import helper, _test_decorators
+
+
+timeout = 0.2
+europoundUnicode = ntou('\x80\xa3')
+
+
+#                             Client-side code                             #
+
+
+class ToolTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        # Put check_access in a custom toolbox with its own namespace
+        myauthtools = cherrypy._cptools.Toolbox('myauth')
+
+        def check_access(default=False):
+            if not getattr(cherrypy.request, 'userid', default):
+                raise cherrypy.HTTPError(401)
+        myauthtools.check_access = cherrypy.Tool(
+            'before_request_body', check_access)
+
+        def numerify():
+            def number_it(body):
+                for chunk in body:
+                    for k, v in cherrypy.request.numerify_map:
+                        chunk = chunk.replace(k, v)
+                    yield chunk
+            cherrypy.response.body = number_it(cherrypy.response.body)
+
+        class NumTool(cherrypy.Tool):
+
+            def _setup(self):
+                def makemap():
+                    m = self._merged_args().get('map', {})
+                    cherrypy.request.numerify_map = list(six.iteritems(m))
+                cherrypy.request.hooks.attach('on_start_resource', makemap)
+
+                def critical():
+                    cherrypy.request.error_response = cherrypy.HTTPError(
+                        502).set_response
+                critical.failsafe = True
+
+                cherrypy.request.hooks.attach('on_start_resource', critical)
+                cherrypy.request.hooks.attach(self._point, self.callable)
+
+        tools.numerify = NumTool('before_finalize', numerify)
+
+        # It's not mandatory to inherit from cherrypy.Tool.
+        class NadsatTool:
+
+            def __init__(self):
+                self.ended = {}
+                self._name = 'nadsat'
+
+            def nadsat(self):
+                def nadsat_it_up(body):
+                    for chunk in body:
+                        chunk = chunk.replace(b'good', b'horrorshow')
+                        chunk = chunk.replace(b'piece', b'lomtick')
+                        yield chunk
+                cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
+            nadsat.priority = 0
+
+            def cleanup(self):
+                # This runs after the request has been completely written out.
+                cherrypy.response.body = [b'razdrez']
+                id = cherrypy.request.params.get('id')
+                if id:
+                    self.ended[id] = True
+            cleanup.failsafe = True
+
+            def _setup(self):
+                cherrypy.request.hooks.attach('before_finalize', self.nadsat)
+                cherrypy.request.hooks.attach('on_end_request', self.cleanup)
+        tools.nadsat = NadsatTool()
+
+        def pipe_body():
+            cherrypy.request.process_request_body = False
+            clen = int(cherrypy.request.headers['Content-Length'])
+            cherrypy.request.body = cherrypy.request.rfile.read(clen)
+
+        # Assert that we can use a callable object instead of a function.
+        class Rotator(object):
+
+            def __call__(self, scale):
+                r = cherrypy.response
+                r.collapse_body()
+                if six.PY3:
+                    r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
+                else:
+                    r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
+        cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
+
+        def stream_handler(next_handler, *args, **kwargs):
+            actual = cherrypy.request.config.get('tools.streamer.arg')
+            assert actual == 'arg value'
+            cherrypy.response.output = o = io.BytesIO()
+            try:
+                next_handler(*args, **kwargs)
+                # Ignore the response and return our accumulated output
+                # instead.
+                return o.getvalue()
+            finally:
+                o.close()
+        cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(
+            stream_handler)
+
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'Howdy earth!'
+
+            @cherrypy.expose
+            @cherrypy.config(**{
+                'tools.streamer.on': True,
+                'tools.streamer.arg': 'arg value',
+            })
+            def tarfile(self):
+                actual = cherrypy.request.config.get('tools.streamer.arg')
+                assert actual == 'arg value'
+                cherrypy.response.output.write(b'I am ')
+                cherrypy.response.output.write(b'a tarfile')
+
+            @cherrypy.expose
+            def euro(self):
+                hooks = list(cherrypy.request.hooks['before_finalize'])
+                hooks.sort()
+                cbnames = [x.callback.__name__ for x in hooks]
+                assert cbnames == ['gzip'], cbnames
+                priorities = [x.priority for x in hooks]
+                assert priorities == [80], priorities
+                yield ntou('Hello,')
+                yield ntou('world')
+                yield europoundUnicode
+
+            # Bare hooks
+            @cherrypy.expose
+            @cherrypy.config(**{'hooks.before_request_body': pipe_body})
+            def pipe(self):
+                return cherrypy.request.body
+
+            # Multiple decorators; include kwargs just for fun.
+            # Note that rotator must run before gzip.
+            @cherrypy.expose
+            def decorated_euro(self, *vpath):
+                yield ntou('Hello,')
+                yield ntou('world')
+                yield europoundUnicode
+            decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
+            decorated_euro = tools.rotator(scale=3)(decorated_euro)
+
+        root = Root()
+
+        class TestType(type):
+            """Metaclass which automatically exposes all functions in each
+            subclass, and adds an instance of the subclass as an attribute
+            of root.
+            """
+            def __init__(cls, name, bases, dct):
+                type.__init__(cls, name, bases, dct)
+                for value in six.itervalues(dct):
+                    if isinstance(value, types.FunctionType):
+                        cherrypy.expose(value)
+                setattr(root, name.lower(), cls())
+        Test = TestType('Test', (object,), {})
+
+        # METHOD ONE:
+        # Declare Tools in _cp_config
+        @cherrypy.config(**{'tools.nadsat.on': True})
+        class Demo(Test):
+
+            def index(self, id=None):
+                return 'A good piece of cherry pie'
+
+            def ended(self, id):
+                return repr(tools.nadsat.ended[id])
+
+            def err(self, id=None):
+                raise ValueError()
+
+            def errinstream(self, id=None):
+                yield 'nonconfidential'
+                raise ValueError()
+                yield 'confidential'
+
+            # METHOD TWO: decorator using Tool()
+            # We support Python 2.3, but the @-deco syntax would look like
+            # this:
+            # @tools.check_access()
+            def restricted(self):
+                return 'Welcome!'
+            restricted = myauthtools.check_access()(restricted)
+            userid = restricted
+
+            def err_in_onstart(self):
+                return 'success!'
+
+            @cherrypy.config(**{'response.stream': True})
+            def stream(self, id=None):
+                for x in range(100000000):
+                    yield str(x)
+
+        conf = {
+            # METHOD THREE:
+            # Declare Tools in detached config
+            '/demo': {
+                'tools.numerify.on': True,
+                'tools.numerify.map': {b'pie': b'3.14159'},
+            },
+            '/demo/restricted': {
+                'request.show_tracebacks': False,
+            },
+            '/demo/userid': {
+                'request.show_tracebacks': False,
+                'myauth.check_access.default': True,
+            },
+            '/demo/errinstream': {
+                'response.stream': True,
+            },
+            '/demo/err_in_onstart': {
+                # Because this isn't a dict, on_start_resource will error.
+                'tools.numerify.map': 'pie->3.14159'
+            },
+            # Combined tools
+            '/euro': {
+                'tools.gzip.on': True,
+                'tools.encode.on': True,
+            },
+            # Priority specified in config
+            '/decorated_euro/subpath': {
+                'tools.gzip.priority': 10,
+            },
+            # Handler wrappers
+            '/tarfile': {'tools.streamer.on': True}
+        }
+        app = cherrypy.tree.mount(root, config=conf)
+        app.request_class.namespaces['myauth'] = myauthtools
+
+        root.tooldecs = _test_decorators.ToolExamples()
+
+    def testHookErrors(self):
+        self.getPage('/demo/?id=1')
+        # If body is "razdrez", then on_end_request is being called too early.
+        self.assertBody('A horrorshow lomtick of cherry 3.14159')
+        # If this fails, then on_end_request isn't being called at all.
+        time.sleep(0.1)
+        self.getPage('/demo/ended/1')
+        self.assertBody('True')
+
+        valerr = '\n    raise ValueError()\nValueError'
+        self.getPage('/demo/err?id=3')
+        # If body is "razdrez", then on_end_request is being called too early.
+        self.assertErrorPage(502, pattern=valerr)
+        # If this fails, then on_end_request isn't being called at all.
+        time.sleep(0.1)
+        self.getPage('/demo/ended/3')
+        self.assertBody('True')
+
+        # If body is "razdrez", then on_end_request is being called too early.
+        if (cherrypy.server.protocol_version == 'HTTP/1.0' or
+                getattr(cherrypy.server, 'using_apache', False)):
+            self.getPage('/demo/errinstream?id=5')
+            # Because this error is raised after the response body has
+            # started, the status should not change to an error status.
+            self.assertStatus('200 OK')
+            self.assertBody('nonconfidential')
+        else:
+            # Because this error is raised after the response body has
+            # started, and because it's chunked output, an error is raised by
+            # the HTTP client when it encounters incomplete output.
+            self.assertRaises((ValueError, IncompleteRead), self.getPage,
+                              '/demo/errinstream?id=5')
+        # If this fails, then on_end_request isn't being called at all.
+        time.sleep(0.1)
+        self.getPage('/demo/ended/5')
+        self.assertBody('True')
+
+        # Test the "__call__" technique (compile-time decorator).
+        self.getPage('/demo/restricted')
+        self.assertErrorPage(401)
+
+        # Test compile-time decorator with kwargs from config.
+        self.getPage('/demo/userid')
+        self.assertBody('Welcome!')
+
+    def testEndRequestOnDrop(self):
+        old_timeout = None
+        try:
+            httpserver = cherrypy.server.httpserver
+            old_timeout = httpserver.timeout
+        except (AttributeError, IndexError):
+            return self.skip()
+
+        try:
+            httpserver.timeout = timeout
+
+            # Test that on_end_request is called even if the client drops.
+            self.persistent = True
+            try:
+                conn = self.HTTP_CONN
+                conn.putrequest('GET', '/demo/stream?id=9', skip_host=True)
+                conn.putheader('Host', self.HOST)
+                conn.endheaders()
+                # Skip the rest of the request and close the conn. This will
+                # cause the server's active socket to error, which *should*
+                # result in the request being aborted, and request.close being
+                # called all the way up the stack (including WSGI middleware),
+                # eventually calling our on_end_request hook.
+            finally:
+                self.persistent = False
+            time.sleep(timeout * 2)
+            # Test that the on_end_request hook was called.
+            self.getPage('/demo/ended/9')
+            self.assertBody('True')
+        finally:
+            if old_timeout is not None:
+                httpserver.timeout = old_timeout
+
+    def testGuaranteedHooks(self):
+        # The 'critical' on_start_resource hook is 'failsafe' (guaranteed
+        # to run even if there are failures in other on_start methods).
+        # This is NOT true of the other hooks.
+        # Here, we have set up a failure in NumerifyTool.numerify_map,
+        # but our 'critical' hook should run and set the error to 502.
+        self.getPage('/demo/err_in_onstart')
+        self.assertErrorPage(502)
+        tmpl = "AttributeError: 'str' object has no attribute '{attr}'"
+        expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems')
+        self.assertInBody(expected_msg)
+
+    def testCombinedTools(self):
+        expectedResult = (ntou('Hello,world') +
+                          europoundUnicode).encode('utf-8')
+        zbuf = io.BytesIO()
+        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
+        zfile.write(expectedResult)
+        zfile.close()
+
+        self.getPage('/euro',
+                     headers=[
+                         ('Accept-Encoding', 'gzip'),
+                         ('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')])
+        self.assertInBody(zbuf.getvalue()[:3])
+
+        zbuf = io.BytesIO()
+        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
+        zfile.write(expectedResult)
+        zfile.close()
+
+        self.getPage('/decorated_euro', headers=[('Accept-Encoding', 'gzip')])
+        self.assertInBody(zbuf.getvalue()[:3])
+
+        # This returns a different value because gzip's priority was
+        # lowered in conf, allowing the rotator to run after gzip.
+        # Of course, we don't want breakage in production apps,
+        # but it proves the priority was changed.
+        self.getPage('/decorated_euro/subpath',
+                     headers=[('Accept-Encoding', 'gzip')])
+        if six.PY3:
+            self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
+        else:
+            self.assertInBody(''.join([chr((ord(x) + 3) % 256)
+                              for x in zbuf.getvalue()]))
+
+    def testBareHooks(self):
+        content = 'bit of a pain in me gulliver'
+        self.getPage('/pipe',
+                     headers=[('Content-Length', str(len(content))),
+                              ('Content-Type', 'text/plain')],
+                     method='POST', body=content)
+        self.assertBody(content)
+
+    def testHandlerWrapperTool(self):
+        self.getPage('/tarfile')
+        self.assertBody('I am a tarfile')
+
+    def testToolWithConfig(self):
+        if not sys.version_info >= (2, 5):
+            return self.skip('skipped (Python 2.5+ only)')
+
+        self.getPage('/tooldecs/blah')
+        self.assertHeader('Content-Type', 'application/data')
+
+    def testWarnToolOn(self):
+        # get
+        try:
+            cherrypy.tools.numerify.on
+        except AttributeError:
+            pass
+        else:
+            raise AssertionError('Tool.on did not error as it should have.')
+
+        # set
+        try:
+            cherrypy.tools.numerify.on = True
+        except AttributeError:
+            pass
+        else:
+            raise AssertionError('Tool.on did not error as it should have.')
+
+    def testDecorator(self):
+        @cherrypy.tools.register('on_start_resource')
+        def example():
+            pass
+        self.assertTrue(isinstance(cherrypy.tools.example, cherrypy.Tool))
+        self.assertEqual(cherrypy.tools.example._point, 'on_start_resource')
+
+        @cherrypy.tools.register(  # noqa: F811
+            'before_finalize', name='renamed', priority=60,
+        )
+        def example():
+            pass
+        self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool))
+        self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize')
+        self.assertEqual(cherrypy.tools.renamed._name, 'renamed')
+        self.assertEqual(cherrypy.tools.renamed._priority, 60)
+
+
+class SessionAuthTest(unittest.TestCase):
+
+    def test_login_screen_returns_bytes(self):
+        """
+        login_screen must return bytes even if unicode parameters are passed.
+        Issue 1132 revealed that login_screen would return unicode if the
+        username and password were unicode.
+        """
+        sa = cherrypy.lib.cptools.SessionAuth()
+        res = sa.login_screen(None, username=six.text_type('nobody'),
+                              password=six.text_type('anypass'))
+        self.assertTrue(isinstance(res, bytes))
+
+
+class TestHooks:
+    def test_priorities(self):
+        """
+        Hooks should sort by priority order.
+        """
+        Hook = cherrypy._cprequest.Hook
+        hooks = [
+            Hook(None, priority=48),
+            Hook(None),
+            Hook(None, priority=49),
+        ]
+        hooks.sort()
+        by_priority = operator.attrgetter('priority')
+        priorities = list(map(by_priority, hooks))
+        assert priorities == [48, 49, 50]
diff --git a/libraries/cherrypy/test/test_tutorials.py b/libraries/cherrypy/test/test_tutorials.py
new file mode 100644
index 00000000..efa35b99
--- /dev/null
+++ b/libraries/cherrypy/test/test_tutorials.py
@@ -0,0 +1,210 @@
+import sys
+import imp
+import types
+import importlib
+
+import six
+
+import cherrypy
+from cherrypy.test import helper
+
+
+class TutorialTest(helper.CPWebCase):
+
+    @classmethod
+    def setup_server(cls):
+        """
+        Mount something so the engine starts.
+        """
+        class Dummy:
+            pass
+        cherrypy.tree.mount(Dummy())
+
+    @staticmethod
+    def load_module(name):
+        """
+        Import or reload tutorial module as needed.
+        """
+        target = 'cherrypy.tutorial.' + name
+        if target in sys.modules:
+            module = imp.reload(sys.modules[target])
+        else:
+            module = importlib.import_module(target)
+        return module
+
+    @classmethod
+    def setup_tutorial(cls, name, root_name, config={}):
+        cherrypy.config.reset()
+        module = cls.load_module(name)
+        root = getattr(module, root_name)
+        conf = getattr(module, 'tutconf')
+        class_types = type,
+        if six.PY2:
+            class_types += types.ClassType,
+        if isinstance(root, class_types):
+            root = root()
+        cherrypy.tree.mount(root, config=conf)
+        cherrypy.config.update(config)
+
+    def test01HelloWorld(self):
+        self.setup_tutorial('tut01_helloworld', 'HelloWorld')
+        self.getPage('/')
+        self.assertBody('Hello world!')
+
+    def test02ExposeMethods(self):
+        self.setup_tutorial('tut02_expose_methods', 'HelloWorld')
+        self.getPage('/show_msg')
+        self.assertBody('Hello world!')
+
+    def test03GetAndPost(self):
+        self.setup_tutorial('tut03_get_and_post', 'WelcomePage')
+
+        # Try different GET queries
+        self.getPage('/greetUser?name=Bob')
+        self.assertBody("Hey Bob, what's up?")
+
+        self.getPage('/greetUser')
+        self.assertBody('Please enter your name <a href="./">here</a>.')
+
+        self.getPage('/greetUser?name=')
+        self.assertBody('No, really, enter your name <a href="./">here</a>.')
+
+        # Try the same with POST
+        self.getPage('/greetUser', method='POST', body='name=Bob')
+        self.assertBody("Hey Bob, what's up?")
+
+        self.getPage('/greetUser', method='POST', body='name=')
+        self.assertBody('No, really, enter your name <a href="./">here</a>.')
+
+    def test04ComplexSite(self):
+        self.setup_tutorial('tut04_complex_site', 'root')
+
+        msg = '''
+            <p>Here are some extra useful links:</p>
+
+            <ul>
+                <li><a href="http://del.icio.us">del.icio.us</a></li>
+                <li><a href="http://www.cherrypy.org">CherryPy</a></li>
+            </ul>
+
+            <p>[<a href="../">Return to links page</a>]</p>'''
+        self.getPage('/links/extra/')
+        self.assertBody(msg)
+
+    def test05DerivedObjects(self):
+        self.setup_tutorial('tut05_derived_objects', 'HomePage')
+        msg = '''
+            <html>
+            <head>
+                <title>Another Page</title>
+            <head>
+            <body>
+            <h2>Another Page</h2>
+
+            <p>
+            And this is the amazing second page!
+            </p>
+
+            </body>
+            </html>
+        '''
+        # the tutorial has some annoying spaces in otherwise blank lines
+        msg = msg.replace('</h2>\n\n', '</h2>\n        \n')
+        msg = msg.replace('</p>\n\n', '</p>\n        \n')
+        self.getPage('/another/')
+        self.assertBody(msg)
+
+    def test06DefaultMethod(self):
+        self.setup_tutorial('tut06_default_method', 'UsersPage')
+        self.getPage('/hendrik')
+        self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
+                        '(<a href="./">back</a>)')
+
+    def test07Sessions(self):
+        self.setup_tutorial('tut07_sessions', 'HitCounter')
+
+        self.getPage('/')
+        self.assertBody(
+            "\n            During your current session, you've viewed this"
+            '\n            page 1 times! Your life is a patio of fun!'
+            '\n        ')
+
+        self.getPage('/', self.cookies)
+        self.assertBody(
+            "\n            During your current session, you've viewed this"
+            '\n            page 2 times! Your life is a patio of fun!'
+            '\n        ')
+
+    def test08GeneratorsAndYield(self):
+        self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo')
+        self.getPage('/')
+        self.assertBody('<html><body><h2>Generators rule!</h2>'
+                        '<h3>List of users:</h3>'
+                        'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
+                        '</body></html>')
+
+    def test09Files(self):
+        self.setup_tutorial('tut09_files', 'FileDemo')
+
+        # Test upload
+        filesize = 5
+        h = [('Content-type', 'multipart/form-data; boundary=x'),
+             ('Content-Length', str(105 + filesize))]
+        b = ('--x\n'
+             'Content-Disposition: form-data; name="myFile"; '
+             'filename="hello.txt"\r\n'
+             'Content-Type: text/plain\r\n'
+             '\r\n')
+        b += 'a' * filesize + '\n' + '--x--\n'
+        self.getPage('/upload', h, 'POST', b)
+        self.assertBody('''<html>
+        <body>
+            myFile length: %d<br />
+            myFile filename: hello.txt<br />
+            myFile mime-type: text/plain
+        </body>
+        </html>''' % filesize)
+
+        # Test download
+        self.getPage('/download')
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'application/x-download')
+        self.assertHeader('Content-Disposition',
+                          # Make sure the filename is quoted.
+                          'attachment; filename="pdf_file.pdf"')
+        self.assertEqual(len(self.body), 85698)
+
+    def test10HTTPErrors(self):
+        self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo')
+
+        @cherrypy.expose
+        def traceback_setting():
+            return repr(cherrypy.request.show_tracebacks)
+        cherrypy.tree.mount(traceback_setting, '/traceback_setting')
+
+        self.getPage('/')
+        self.assertInBody("""<a href="toggleTracebacks">""")
+        self.assertInBody("""<a href="/doesNotExist">""")
+        self.assertInBody("""<a href="/error?code=403">""")
+        self.assertInBody("""<a href="/error?code=500">""")
+        self.assertInBody("""<a href="/messageArg">""")
+
+        self.getPage('/traceback_setting')
+        setting = self.body
+        self.getPage('/toggleTracebacks')
+        self.assertStatus((302, 303))
+        self.getPage('/traceback_setting')
+        self.assertBody(str(not eval(setting)))
+
+        self.getPage('/error?code=500')
+        self.assertStatus(500)
+        self.assertInBody('The server encountered an unexpected condition '
+                          'which prevented it from fulfilling the request.')
+
+        self.getPage('/error?code=403')
+        self.assertStatus(403)
+        self.assertInBody("<h2>You can't do that!</h2>")
+
+        self.getPage('/messageArg')
+        self.assertStatus(500)
+        self.assertInBody("If you construct an HTTPError with a 'message'")
diff --git a/libraries/cherrypy/test/test_virtualhost.py b/libraries/cherrypy/test/test_virtualhost.py
new file mode 100644
index 00000000..de88f927
--- /dev/null
+++ b/libraries/cherrypy/test/test_virtualhost.py
@@ -0,0 +1,113 @@
+import os
+
+import cherrypy
+from cherrypy.test import helper
+
+curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
+
+
+class VirtualHostTest(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return 'Hello, world'
+
+            @cherrypy.expose
+            def dom4(self):
+                return 'Under construction'
+
+            @cherrypy.expose
+            def method(self, value):
+                return 'You sent %s' % value
+
+        class VHost:
+
+            def __init__(self, sitename):
+                self.sitename = sitename
+
+            @cherrypy.expose
+            def index(self):
+                return 'Welcome to %s' % self.sitename
+
+            @cherrypy.expose
+            def vmethod(self, value):
+                return 'You sent %s' % value
+
+            @cherrypy.expose
+            def url(self):
+                return cherrypy.url('nextpage')
+
+            # Test static as a handler (section must NOT include vhost prefix)
+            static = cherrypy.tools.staticdir.handler(
+                section='/static', dir=curdir)
+
+        root = Root()
+        root.mydom2 = VHost('Domain 2')
+        root.mydom3 = VHost('Domain 3')
+        hostmap = {'www.mydom2.com': '/mydom2',
+                   'www.mydom3.com': '/mydom3',
+                   'www.mydom4.com': '/dom4',
+                   }
+        cherrypy.tree.mount(root, config={
+            '/': {
+                'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)
+            },
+            # Test static in config (section must include vhost prefix)
+            '/mydom2/static2': {
+                'tools.staticdir.on': True,
+                'tools.staticdir.root': curdir,
+                'tools.staticdir.dir': 'static',
+                'tools.staticdir.index': 'index.html',
+            },
+        })
+
+    def testVirtualHost(self):
+        self.getPage('/', [('Host', 'www.mydom1.com')])
+        self.assertBody('Hello, world')
+        self.getPage('/mydom2/', [('Host', 'www.mydom1.com')])
+        self.assertBody('Welcome to Domain 2')
+
+        self.getPage('/', [('Host', 'www.mydom2.com')])
+        self.assertBody('Welcome to Domain 2')
+        self.getPage('/', [('Host', 'www.mydom3.com')])
+        self.assertBody('Welcome to Domain 3')
+        self.getPage('/', [('Host', 'www.mydom4.com')])
+        self.assertBody('Under construction')
+
+        # Test GET, POST, and positional params
+        self.getPage('/method?value=root')
+        self.assertBody('You sent root')
+        self.getPage('/vmethod?value=dom2+GET', [('Host', 'www.mydom2.com')])
+        self.assertBody('You sent dom2 GET')
+        self.getPage('/vmethod', [('Host', 'www.mydom3.com')], method='POST',
+                     body='value=dom3+POST')
+        self.assertBody('You sent dom3 POST')
+        self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')])
+        self.assertBody('You sent pos')
+
+        # Test that cherrypy.url uses the browser url, not the virtual url
+        self.getPage('/url', [('Host', 'www.mydom2.com')])
+        self.assertBody('%s://www.mydom2.com/nextpage' % self.scheme)
+
+    def test_VHost_plus_Static(self):
+        # Test static as a handler
+        self.getPage('/static/style.css', [('Host', 'www.mydom2.com')])
+        self.assertStatus('200 OK')
+        self.assertHeader('Content-Type', 'text/css;charset=utf-8')
+
+        # Test static in config
+        self.getPage('/static2/dirback.jpg', [('Host', 'www.mydom2.com')])
+        self.assertStatus('200 OK')
+        self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg'])
+
+        # Test static config with "index" arg
+        self.getPage('/static2/', [('Host', 'www.mydom2.com')])
+        self.assertStatus('200 OK')
+        self.assertBody('Hello, world\r\n')
+        # Since tools.trailing_slash is on by default, this should redirect
+        self.getPage('/static2', [('Host', 'www.mydom2.com')])
+        self.assertStatus(301)
diff --git a/libraries/cherrypy/test/test_wsgi_ns.py b/libraries/cherrypy/test/test_wsgi_ns.py
new file mode 100644
index 00000000..3545724c
--- /dev/null
+++ b/libraries/cherrypy/test/test_wsgi_ns.py
@@ -0,0 +1,93 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class WSGI_Namespace_Test(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        class WSGIResponse(object):
+
+            def __init__(self, appresults):
+                self.appresults = appresults
+                self.iter = iter(appresults)
+
+            def __iter__(self):
+                return self
+
+            def next(self):
+                return self.iter.next()
+
+            def __next__(self):
+                return next(self.iter)
+
+            def close(self):
+                if hasattr(self.appresults, 'close'):
+                    self.appresults.close()
+
+        class ChangeCase(object):
+
+            def __init__(self, app, to=None):
+                self.app = app
+                self.to = to
+
+            def __call__(self, environ, start_response):
+                res = self.app(environ, start_response)
+
+                class CaseResults(WSGIResponse):
+
+                    def next(this):
+                        return getattr(this.iter.next(), self.to)()
+
+                    def __next__(this):
+                        return getattr(next(this.iter), self.to)()
+                return CaseResults(res)
+
+        class Replacer(object):
+
+            def __init__(self, app, map={}):
+                self.app = app
+                self.map = map
+
+            def __call__(self, environ, start_response):
+                res = self.app(environ, start_response)
+
+                class ReplaceResults(WSGIResponse):
+
+                    def next(this):
+                        line = this.iter.next()
+                        for k, v in self.map.iteritems():
+                            line = line.replace(k, v)
+                        return line
+
+                    def __next__(this):
+                        line = next(this.iter)
+                        for k, v in self.map.items():
+                            line = line.replace(k, v)
+                        return line
+                return ReplaceResults(res)
+
+        class Root(object):
+
+            @cherrypy.expose
+            def index(self):
+                return 'HellO WoRlD!'
+
+        root_conf = {'wsgi.pipeline': [('replace', Replacer)],
+                     'wsgi.replace.map': {b'L': b'X',
+                                          b'l': b'r'},
+                     }
+
+        app = cherrypy.Application(Root())
+        app.wsgiapp.pipeline.append(('changecase', ChangeCase))
+        app.wsgiapp.config['changecase'] = {'to': 'upper'}
+        cherrypy.tree.mount(app, config={'/': root_conf})
+
+    def test_pipeline(self):
+        if not cherrypy.server.httpserver:
+            return self.skip()
+
+        self.getPage('/')
+        # If body is "HEXXO WORXD!", the middleware was applied out of order.
+        self.assertBody('HERRO WORRD!')
diff --git a/libraries/cherrypy/test/test_wsgi_unix_socket.py b/libraries/cherrypy/test/test_wsgi_unix_socket.py
new file mode 100644
index 00000000..8f1cc00b
--- /dev/null
+++ b/libraries/cherrypy/test/test_wsgi_unix_socket.py
@@ -0,0 +1,93 @@
+import os
+import socket
+import atexit
+import tempfile
+
+from six.moves.http_client import HTTPConnection
+
+import pytest
+
+import cherrypy
+from cherrypy.test import helper
+
+
+def usocket_path():
+    fd, path = tempfile.mkstemp('cp_test.sock')
+    os.close(fd)
+    os.remove(path)
+    return path
+
+
+USOCKET_PATH = usocket_path()
+
+
+class USocketHTTPConnection(HTTPConnection):
+    """
+    HTTPConnection over a unix socket.
+    """
+
+    def __init__(self, path):
+        HTTPConnection.__init__(self, 'localhost')
+        self.path = path
+
+    def __call__(self, *args, **kwargs):
+        """
+        Catch-all method just to present itself as a constructor for the
+        HTTPConnection.
+        """
+        return self
+
+    def connect(self):
+        """
+        Override the connect method and assign a unix socket as a transport.
+        """
+        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+        sock.connect(self.path)
+        self.sock = sock
+        atexit.register(lambda: os.remove(self.path))
+
+
+@pytest.mark.skipif("sys.platform == 'win32'")
+class WSGI_UnixSocket_Test(helper.CPWebCase):
+    """
+    Test basic behavior on a cherrypy wsgi server listening
+    on a unix socket.
+
+    It exercises the config option `server.socket_file`.
+    """
+    HTTP_CONN = USocketHTTPConnection(USOCKET_PATH)
+
+    @staticmethod
+    def setup_server():
+        class Root(object):
+
+            @cherrypy.expose
+            def index(self):
+                return 'Test OK'
+
+            @cherrypy.expose
+            def error(self):
+                raise Exception('Invalid page')
+
+        config = {
+            'server.socket_file': USOCKET_PATH
+        }
+        cherrypy.config.update(config)
+        cherrypy.tree.mount(Root())
+
+    def tearDown(self):
+        cherrypy.config.update({'server.socket_file': None})
+
+    def test_simple_request(self):
+        self.getPage('/')
+        self.assertStatus('200 OK')
+        self.assertInBody('Test OK')
+
+    def test_not_found(self):
+        self.getPage('/invalid_path')
+        self.assertStatus('404 Not Found')
+
+    def test_internal_error(self):
+        self.getPage('/error')
+        self.assertStatus('500 Internal Server Error')
+        self.assertInBody('Invalid page')
diff --git a/libraries/cherrypy/test/test_wsgi_vhost.py b/libraries/cherrypy/test/test_wsgi_vhost.py
new file mode 100644
index 00000000..2b6e5ba9
--- /dev/null
+++ b/libraries/cherrypy/test/test_wsgi_vhost.py
@@ -0,0 +1,35 @@
+import cherrypy
+from cherrypy.test import helper
+
+
+class WSGI_VirtualHost_Test(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        class ClassOfRoot(object):
+
+            def __init__(self, name):
+                self.name = name
+
+            @cherrypy.expose
+            def index(self):
+                return 'Welcome to the %s website!' % self.name
+
+        default = cherrypy.Application(None)
+
+        domains = {}
+        for year in range(1997, 2008):
+            app = cherrypy.Application(ClassOfRoot('Class of %s' % year))
+            domains['www.classof%s.example' % year] = app
+
+        cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains))
+
+    def test_welcome(self):
+        if not cherrypy.server.using_wsgi:
+            return self.skip('skipped (not using WSGI)... ')
+
+        for year in range(1997, 2008):
+            self.getPage(
+                '/', headers=[('Host', 'www.classof%s.example' % year)])
+            self.assertBody('Welcome to the Class of %s website!' % year)
diff --git a/libraries/cherrypy/test/test_wsgiapps.py b/libraries/cherrypy/test/test_wsgiapps.py
new file mode 100644
index 00000000..1b3bf28f
--- /dev/null
+++ b/libraries/cherrypy/test/test_wsgiapps.py
@@ -0,0 +1,120 @@
+import sys
+
+import cherrypy
+from cherrypy._cpcompat import ntob
+from cherrypy.test import helper
+
+
+class WSGIGraftTests(helper.CPWebCase):
+
+    @staticmethod
+    def setup_server():
+
+        def test_app(environ, start_response):
+            status = '200 OK'
+            response_headers = [('Content-type', 'text/plain')]
+            start_response(status, response_headers)
+            output = ['Hello, world!\n',
+                      'This is a wsgi app running within CherryPy!\n\n']
+            keys = list(environ.keys())
+            keys.sort()
+            for k in keys:
+                output.append('%s: %s\n' % (k, environ[k]))
+            return [ntob(x, 'utf-8') for x in output]
+
+        def test_empty_string_app(environ, start_response):
+            status = '200 OK'
+            response_headers = [('Content-type', 'text/plain')]
+            start_response(status, response_headers)
+            return [
+                b'Hello', b'', b' ', b'', b'world',
+            ]
+
+        class WSGIResponse(object):
+
+            def __init__(self, appresults):
+                self.appresults = appresults
+                self.iter = iter(appresults)
+
+            def __iter__(self):
+                return self
+
+            if sys.version_info >= (3, 0):
+                def __next__(self):
+                    return next(self.iter)
+            else:
+                def next(self):
+                    return self.iter.next()
+
+            def close(self):
+                if hasattr(self.appresults, 'close'):
+                    self.appresults.close()
+
+        class ReversingMiddleware(object):
+
+            def __init__(self, app):
+                self.app = app
+
+            def __call__(self, environ, start_response):
+                results = app(environ, start_response)
+
+                class Reverser(WSGIResponse):
+
+                    if sys.version_info >= (3, 0):
+                        def __next__(this):
+                            line = list(next(this.iter))
+                            line.reverse()
+                            return bytes(line)
+                    else:
+                        def next(this):
+                            line = list(this.iter.next())
+                            line.reverse()
+                            return ''.join(line)
+
+                return Reverser(results)
+
+        class Root:
+
+            @cherrypy.expose
+            def index(self):
+                return ntob("I'm a regular CherryPy page handler!")
+
+        cherrypy.tree.mount(Root())
+
+        cherrypy.tree.graft(test_app, '/hosted/app1')
+        cherrypy.tree.graft(test_empty_string_app, '/hosted/app3')
+
+        # Set script_name explicitly to None to signal CP that it should
+        # be pulled from the WSGI environ each time.
+        app = cherrypy.Application(Root(), script_name=None)
+        cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2')
+
+    wsgi_output = '''Hello, world!
+This is a wsgi app running within CherryPy!'''
+
+    def test_01_standard_app(self):
+        self.getPage('/')
+        self.assertBody("I'm a regular CherryPy page handler!")
+
+    def test_04_pure_wsgi(self):
+        if not cherrypy.server.using_wsgi:
+            return self.skip('skipped (not using WSGI)... ')
+        self.getPage('/hosted/app1')
+        self.assertHeader('Content-Type', 'text/plain')
+        self.assertInBody(self.wsgi_output)
+
+    def test_05_wrapped_cp_app(self):
+        if not cherrypy.server.using_wsgi:
+            return self.skip('skipped (not using WSGI)... ')
+        self.getPage('/hosted/app2/')
+        body = list("I'm a regular CherryPy page handler!")
+        body.reverse()
+        body = ''.join(body)
+        self.assertInBody(body)
+
+    def test_06_empty_string_app(self):
+        if not cherrypy.server.using_wsgi:
+            return self.skip('skipped (not using WSGI)... ')
+        self.getPage('/hosted/app3')
+        self.assertHeader('Content-Type', 'text/plain')
+        self.assertInBody('Hello world')
diff --git a/libraries/cherrypy/test/test_xmlrpc.py b/libraries/cherrypy/test/test_xmlrpc.py
new file mode 100644
index 00000000..ad93b821
--- /dev/null
+++ b/libraries/cherrypy/test/test_xmlrpc.py
@@ -0,0 +1,183 @@
+import sys
+
+import six
+
+from six.moves.xmlrpc_client import (
+    DateTime, Fault,
+    ProtocolError, ServerProxy, SafeTransport
+)
+
+import cherrypy
+from cherrypy import _cptools
+from cherrypy.test import helper
+
+if six.PY3:
+    HTTPSTransport = SafeTransport
+
+    # Python 3.0's SafeTransport still mistakenly checks for socket.ssl
+    import socket
+    if not hasattr(socket, 'ssl'):
+        socket.ssl = True
+else:
+    class HTTPSTransport(SafeTransport):
+
+        """Subclass of SafeTransport to fix sock.recv errors (by using file).
+        """
+
+        def request(self, host, handler, request_body, verbose=0):
+            # issue XML-RPC request
+            h = self.make_connection(host)
+            if verbose:
+                h.set_debuglevel(1)
+
+            self.send_request(h, handler, request_body)
+            self.send_host(h, host)
+            self.send_user_agent(h)
+            self.send_content(h, request_body)
+
+            errcode, errmsg, headers = h.getreply()
+            if errcode != 200:
+                raise ProtocolError(host + handler, errcode, errmsg, headers)
+
+            self.verbose = verbose
+
+            # Here's where we differ from the superclass. It says:
+            # try:
+            #     sock = h._conn.sock
+            # except AttributeError:
+            #     sock = None
+            # return self._parse_response(h.getfile(), sock)
+
+            return self.parse_response(h.getfile())
+
+
+def setup_server():
+
+    class Root:
+
+        @cherrypy.expose
+        def index(self):
+            return "I'm a standard index!"
+
+    class XmlRpc(_cptools.XMLRPCController):
+
+        @cherrypy.expose
+        def foo(self):
+            return 'Hello world!'
+
+        @cherrypy.expose
+        def return_single_item_list(self):
+            return [42]
+
+        @cherrypy.expose
+        def return_string(self):
+            return 'here is a string'
+
+        @cherrypy.expose
+        def return_tuple(self):
+            return ('here', 'is', 1, 'tuple')
+
+        @cherrypy.expose
+        def return_dict(self):
+            return dict(a=1, b=2, c=3)
+
+        @cherrypy.expose
+        def return_composite(self):
+            return dict(a=1, z=26), 'hi', ['welcome', 'friend']
+
+        @cherrypy.expose
+        def return_int(self):
+            return 42
+
+        @cherrypy.expose
+        def return_float(self):
+            return 3.14
+
+        @cherrypy.expose
+        def return_datetime(self):
+            return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))
+
+        @cherrypy.expose
+        def return_boolean(self):
+            return True
+
+        @cherrypy.expose
+        def test_argument_passing(self, num):
+            return num * 2
+
+        @cherrypy.expose
+        def test_returning_Fault(self):
+            return Fault(1, 'custom Fault response')
+
+    root = Root()
+    root.xmlrpc = XmlRpc()
+    cherrypy.tree.mount(root, config={'/': {
+        'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
+        'tools.xmlrpc.allow_none': 0,
+    }})
+
+
+class XmlRpcTest(helper.CPWebCase):
+    setup_server = staticmethod(setup_server)
+
+    def testXmlRpc(self):
+
+        scheme = self.scheme
+        if scheme == 'https':
+            url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
+            proxy = ServerProxy(url, transport=HTTPSTransport())
+        else:
+            url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
+            proxy = ServerProxy(url)
+
+        # begin the tests ...
+        self.getPage('/xmlrpc/foo')
+        self.assertBody('Hello world!')
+
+        self.assertEqual(proxy.return_single_item_list(), [42])
+        self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion')
+        self.assertEqual(proxy.return_string(), 'here is a string')
+        self.assertEqual(proxy.return_tuple(),
+                         list(('here', 'is', 1, 'tuple')))
+        self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2})
+        self.assertEqual(proxy.return_composite(),
+                         [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']])
+        self.assertEqual(proxy.return_int(), 42)
+        self.assertEqual(proxy.return_float(), 3.14)
+        self.assertEqual(proxy.return_datetime(),
+                         DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)))
+        self.assertEqual(proxy.return_boolean(), True)
+        self.assertEqual(proxy.test_argument_passing(22), 22 * 2)
+
+        # Test an error in the page handler (should raise an xmlrpclib.Fault)
+        try:
+            proxy.test_argument_passing({})
+        except Exception:
+            x = sys.exc_info()[1]
+            self.assertEqual(x.__class__, Fault)
+            self.assertEqual(x.faultString, ('unsupported operand type(s) '
+                                             "for *: 'dict' and 'int'"))
+        else:
+            self.fail('Expected xmlrpclib.Fault')
+
+        # https://github.com/cherrypy/cherrypy/issues/533
+        # if a method is not found, an xmlrpclib.Fault should be raised
+        try:
+            proxy.non_method()
+        except Exception:
+            x = sys.exc_info()[1]
+            self.assertEqual(x.__class__, Fault)
+            self.assertEqual(x.faultString,
+                             'method "non_method" is not supported')
+        else:
+            self.fail('Expected xmlrpclib.Fault')
+
+        # Test returning a Fault from the page handler.
+        try:
+            proxy.test_returning_Fault()
+        except Exception:
+            x = sys.exc_info()[1]
+            self.assertEqual(x.__class__, Fault)
+            self.assertEqual(x.faultString, ('custom Fault response'))
+        else:
+            self.fail('Expected xmlrpclib.Fault')
diff --git a/libraries/cherrypy/test/webtest.py b/libraries/cherrypy/test/webtest.py
new file mode 100644
index 00000000..9fb6ce62
--- /dev/null
+++ b/libraries/cherrypy/test/webtest.py
@@ -0,0 +1,11 @@
+# for compatibility, expose cheroot webtest here
+import warnings
+
+from cheroot.test.webtest import (  # noqa
+    interface,
+    WebCase, cleanHeaders, shb, openURL,
+    ServerError, server_error,
+)
+
+
+warnings.warn('Use cheroot.test.webtest', DeprecationWarning)
diff --git a/libraries/cherrypy/tutorial/README.rst b/libraries/cherrypy/tutorial/README.rst
new file mode 100644
index 00000000..c47e7d32
--- /dev/null
+++ b/libraries/cherrypy/tutorial/README.rst
@@ -0,0 +1,16 @@
+CherryPy Tutorials
+------------------
+
+This is a series of tutorials explaining how to develop dynamic web
+applications using CherryPy. A couple of notes:
+
+
+- Each of these tutorials builds on the ones before it. If you're
+  new to CherryPy, we recommend you start with 01_helloworld.py and
+  work your way upwards. :)
+
+- In most of these tutorials, you will notice that all output is done
+  by returning normal Python strings, often using simple Python
+  variable substitution. In most real-world applications, you will
+  probably want to use a separate template package (like Cheetah,
+  CherryTemplate or XML/XSL).
diff --git a/libraries/cherrypy/tutorial/__init__.py b/libraries/cherrypy/tutorial/__init__.py
new file mode 100644
index 00000000..08c142c5
--- /dev/null
+++ b/libraries/cherrypy/tutorial/__init__.py
@@ -0,0 +1,3 @@
+
+# This is used in test_config to test unrepr of "from A import B"
+thing2 = object()
diff --git a/libraries/cherrypy/tutorial/custom_error.html b/libraries/cherrypy/tutorial/custom_error.html
new file mode 100644
index 00000000..d0f30c8a
--- /dev/null
+++ b/libraries/cherrypy/tutorial/custom_error.html
@@ -0,0 +1,14 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
+  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html>
+<head>
+    <title>403 Unauthorized</title>
+</head>
+    <body>
+        <h2>You can't do that!</h2>
+        <p>%(message)s</p>
+        <p>This is a custom error page that is read from a file.<p>
+        <pre>%(traceback)s</pre>
+    </body>
+</html>
diff --git a/libraries/cherrypy/tutorial/pdf_file.pdf b/libraries/cherrypy/tutorial/pdf_file.pdf
new file mode 100644
index 0000000000000000000000000000000000000000..38b4f15eabdd65d4a674cb32034361245aa7b97e
GIT binary patch
literal 85698
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

literal 0
HcmV?d00001

diff --git a/libraries/cherrypy/tutorial/tut01_helloworld.py b/libraries/cherrypy/tutorial/tut01_helloworld.py
new file mode 100644
index 00000000..e86793c8
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut01_helloworld.py
@@ -0,0 +1,34 @@
+"""
+Tutorial - Hello World
+
+The most basic (working) CherryPy application possible.
+"""
+
+import os.path
+
+# Import CherryPy global namespace
+import cherrypy
+
+
+class HelloWorld:
+
+    """ Sample request handler class. """
+
+    # Expose the index method through the web. CherryPy will never
+    # publish methods that don't have the exposed attribute set to True.
+    @cherrypy.expose
+    def index(self):
+        # CherryPy will call this method for the root URI ("/") and send
+        # its return value to the client. Because this is tutorial
+        # lesson number 01, we'll just send something really simple.
+        # How about...
+        return 'Hello world!'
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(HelloWorld(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut02_expose_methods.py b/libraries/cherrypy/tutorial/tut02_expose_methods.py
new file mode 100644
index 00000000..8afbf7d8
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut02_expose_methods.py
@@ -0,0 +1,32 @@
+"""
+Tutorial - Multiple methods
+
+This tutorial shows you how to link to other methods of your request
+handler.
+"""
+
+import os.path
+
+import cherrypy
+
+
+class HelloWorld:
+
+    @cherrypy.expose
+    def index(self):
+        # Let's link to another method here.
+        return 'We have an <a href="show_msg">important message</a> for you!'
+
+    @cherrypy.expose
+    def show_msg(self):
+        # Here's the important message!
+        return 'Hello world!'
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(HelloWorld(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut03_get_and_post.py b/libraries/cherrypy/tutorial/tut03_get_and_post.py
new file mode 100644
index 00000000..0b3d4613
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut03_get_and_post.py
@@ -0,0 +1,51 @@
+"""
+Tutorial - Passing variables
+
+This tutorial shows you how to pass GET/POST variables to methods.
+"""
+
+import os.path
+
+import cherrypy
+
+
+class WelcomePage:
+
+    @cherrypy.expose
+    def index(self):
+        # Ask for the user's name.
+        return '''
+            <form action="greetUser" method="GET">
+            What is your name?
+            <input type="text" name="name" />
+            <input type="submit" />
+            </form>'''
+
+    @cherrypy.expose
+    def greetUser(self, name=None):
+        # CherryPy passes all GET and POST variables as method parameters.
+        # It doesn't make a difference where the variables come from, how
+        # large their contents are, and so on.
+        #
+        # You can define default parameter values as usual. In this
+        # example, the "name" parameter defaults to None so we can check
+        # if a name was actually specified.
+
+        if name:
+            # Greet the user!
+            return "Hey %s, what's up?" % name
+        else:
+            if name is None:
+                # No name was specified
+                return 'Please enter your name <a href="./">here</a>.'
+            else:
+                return 'No, really, enter your name <a href="./">here</a>.'
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(WelcomePage(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut04_complex_site.py b/libraries/cherrypy/tutorial/tut04_complex_site.py
new file mode 100644
index 00000000..3caa1775
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut04_complex_site.py
@@ -0,0 +1,103 @@
+"""
+Tutorial - Multiple objects
+
+This tutorial shows you how to create a site structure through multiple
+possibly nested request handler objects.
+"""
+
+import os.path
+
+import cherrypy
+
+
+class HomePage:
+
+    @cherrypy.expose
+    def index(self):
+        return '''
+            <p>Hi, this is the home page! Check out the other
+            fun stuff on this site:</p>
+
+            <ul>
+                <li><a href="/joke/">A silly joke</a></li>
+                <li><a href="/links/">Useful links</a></li>
+            </ul>'''
+
+
+class JokePage:
+
+    @cherrypy.expose
+    def index(self):
+        return '''
+            <p>"In Python, how do you create a string of random
+            characters?" -- "Read a Perl file!"</p>
+            <p>[<a href="../">Return</a>]</p>'''
+
+
+class LinksPage:
+
+    def __init__(self):
+        # Request handler objects can create their own nested request
+        # handler objects. Simply create them inside their __init__
+        # methods!
+        self.extra = ExtraLinksPage()
+
+    @cherrypy.expose
+    def index(self):
+        # Note the way we link to the extra links page (and back).
+        # As you can see, this object doesn't really care about its
+        # absolute position in the site tree, since we use relative
+        # links exclusively.
+        return '''
+            <p>Here are some useful links:</p>
+
+            <ul>
+                <li>
+                    <a href="http://www.cherrypy.org">The CherryPy Homepage</a>
+                </li>
+                <li>
+                    <a href="http://www.python.org">The Python Homepage</a>
+                </li>
+            </ul>
+
+            <p>You can check out some extra useful
+            links <a href="./extra/">here</a>.</p>
+
+            <p>[<a href="../">Return</a>]</p>
+        '''
+
+
+class ExtraLinksPage:
+
+    @cherrypy.expose
+    def index(self):
+        # Note the relative link back to the Links page!
+        return '''
+            <p>Here are some extra useful links:</p>
+
+            <ul>
+                <li><a href="http://del.icio.us">del.icio.us</a></li>
+                <li><a href="http://www.cherrypy.org">CherryPy</a></li>
+            </ul>
+
+            <p>[<a href="../">Return to links page</a>]</p>'''
+
+
+# Of course we can also mount request handler objects right here!
+root = HomePage()
+root.joke = JokePage()
+root.links = LinksPage()
+
+# Remember, we don't need to mount ExtraLinksPage here, because
+# LinksPage does that itself on initialization. In fact, there is
+# no reason why you shouldn't let your root object take care of
+# creating all contained request handler objects.
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(root, config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut05_derived_objects.py b/libraries/cherrypy/tutorial/tut05_derived_objects.py
new file mode 100644
index 00000000..f626e03f
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut05_derived_objects.py
@@ -0,0 +1,80 @@
+"""
+Tutorial - Object inheritance
+
+You are free to derive your request handler classes from any base
+class you wish. In most real-world applications, you will probably
+want to create a central base class used for all your pages, which takes
+care of things like printing a common page header and footer.
+"""
+
+import os.path
+
+import cherrypy
+
+
+class Page:
+    # Store the page title in a class attribute
+    title = 'Untitled Page'
+
+    def header(self):
+        return '''
+            <html>
+            <head>
+                <title>%s</title>
+            <head>
+            <body>
+            <h2>%s</h2>
+        ''' % (self.title, self.title)
+
+    def footer(self):
+        return '''
+            </body>
+            </html>
+        '''
+
+    # Note that header and footer don't get their exposed attributes
+    # set to True. This isn't necessary since the user isn't supposed
+    # to call header or footer directly; instead, we'll call them from
+    # within the actually exposed handler methods defined in this
+    # class' subclasses.
+
+
+class HomePage(Page):
+    # Different title for this page
+    title = 'Tutorial 5'
+
+    def __init__(self):
+        # create a subpage
+        self.another = AnotherPage()
+
+    @cherrypy.expose
+    def index(self):
+        # Note that we call the header and footer methods inherited
+        # from the Page class!
+        return self.header() + '''
+            <p>
+            Isn't this exciting? There's
+            <a href="./another/">another page</a>, too!
+            </p>
+        ''' + self.footer()
+
+
+class AnotherPage(Page):
+    title = 'Another Page'
+
+    @cherrypy.expose
+    def index(self):
+        return self.header() + '''
+            <p>
+            And this is the amazing second page!
+            </p>
+        ''' + self.footer()
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(HomePage(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut06_default_method.py b/libraries/cherrypy/tutorial/tut06_default_method.py
new file mode 100644
index 00000000..0ce4cabe
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut06_default_method.py
@@ -0,0 +1,61 @@
+"""
+Tutorial - The default method
+
+Request handler objects can implement a method called "default" that
+is called when no other suitable method/object could be found.
+Essentially, if CherryPy2 can't find a matching request handler object
+for the given request URI, it will use the default method of the object
+located deepest on the URI path.
+
+Using this mechanism you can easily simulate virtual URI structures
+by parsing the extra URI string, which you can access through
+cherrypy.request.virtualPath.
+
+The application in this tutorial simulates an URI structure looking
+like /users/<username>. Since the <username> bit will not be found (as
+there are no matching methods), it is handled by the default method.
+"""
+
+import os.path
+
+import cherrypy
+
+
+class UsersPage:
+
+    @cherrypy.expose
+    def index(self):
+        # Since this is just a stupid little example, we'll simply
+        # display a list of links to random, made-up users. In a real
+        # application, this could be generated from a database result set.
+        return '''
+            <a href="./remi">Remi Delon</a><br/>
+            <a href="./hendrik">Hendrik Mans</a><br/>
+            <a href="./lorenzo">Lorenzo Lamas</a><br/>
+        '''
+
+    @cherrypy.expose
+    def default(self, user):
+        # Here we react depending on the virtualPath -- the part of the
+        # path that could not be mapped to an object method. In a real
+        # application, we would probably do some database lookups here
+        # instead of the silly if/elif/else construct.
+        if user == 'remi':
+            out = 'Remi Delon, CherryPy lead developer'
+        elif user == 'hendrik':
+            out = 'Hendrik Mans, CherryPy co-developer & crazy German'
+        elif user == 'lorenzo':
+            out = 'Lorenzo Lamas, famous actor and singer!'
+        else:
+            out = 'Unknown user. :-('
+
+        return '%s (<a href="./">back</a>)' % out
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(UsersPage(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut07_sessions.py b/libraries/cherrypy/tutorial/tut07_sessions.py
new file mode 100644
index 00000000..204322b5
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut07_sessions.py
@@ -0,0 +1,41 @@
+"""
+Tutorial - Sessions
+
+Storing session data in CherryPy applications is very easy: cherrypy
+provides a dictionary called "session" that represents the session
+data for the current user. If you use RAM based sessions, you can store
+any kind of object into that dictionary; otherwise, you are limited to
+objects that can be pickled.
+"""
+
+import os.path
+
+import cherrypy
+
+
+class HitCounter:
+
+    _cp_config = {'tools.sessions.on': True}
+
+    @cherrypy.expose
+    def index(self):
+        # Increase the silly hit counter
+        count = cherrypy.session.get('count', 0) + 1
+
+        # Store the new value in the session dictionary
+        cherrypy.session['count'] = count
+
+        # And display a silly hit count message!
+        return '''
+            During your current session, you've viewed this
+            page %s times! Your life is a patio of fun!
+        ''' % count
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(HitCounter(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut08_generators_and_yield.py b/libraries/cherrypy/tutorial/tut08_generators_and_yield.py
new file mode 100644
index 00000000..18f42f93
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut08_generators_and_yield.py
@@ -0,0 +1,44 @@
+"""
+Bonus Tutorial: Using generators to return result bodies
+
+Instead of returning a complete result string, you can use the yield
+statement to return one result part after another. This may be convenient
+in situations where using a template package like CherryPy or Cheetah
+would be overkill, and messy string concatenation too uncool. ;-)
+"""
+
+import os.path
+
+import cherrypy
+
+
+class GeneratorDemo:
+
+    def header(self):
+        return '<html><body><h2>Generators rule!</h2>'
+
+    def footer(self):
+        return '</body></html>'
+
+    @cherrypy.expose
+    def index(self):
+        # Let's make up a list of users for presentation purposes
+        users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas']
+
+        # Every yield line adds one part to the total result body.
+        yield self.header()
+        yield '<h3>List of users:</h3>'
+
+        for user in users:
+            yield '%s<br/>' % user
+
+        yield self.footer()
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(GeneratorDemo(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut09_files.py b/libraries/cherrypy/tutorial/tut09_files.py
new file mode 100644
index 00000000..48585cbe
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut09_files.py
@@ -0,0 +1,105 @@
+"""
+
+Tutorial: File upload and download
+
+Uploads
+-------
+
+When a client uploads a file to a CherryPy application, it's placed
+on disk immediately. CherryPy will pass it to your exposed method
+as an argument (see "myFile" below); that arg will have a "file"
+attribute, which is a handle to the temporary uploaded file.
+If you wish to permanently save the file, you need to read()
+from myFile.file and write() somewhere else.
+
+Note the use of 'enctype="multipart/form-data"' and 'input type="file"'
+in the HTML which the client uses to upload the file.
+
+
+Downloads
+---------
+
+If you wish to send a file to the client, you have two options:
+First, you can simply return a file-like object from your page handler.
+CherryPy will read the file and serve it as the content (HTTP body)
+of the response. However, that doesn't tell the client that
+the response is a file to be saved, rather than displayed.
+Use cherrypy.lib.static.serve_file for that; it takes four
+arguments:
+
+serve_file(path, content_type=None, disposition=None, name=None)
+
+Set "name" to the filename that you expect clients to use when they save
+your file. Note that the "name" argument is ignored if you don't also
+provide a "disposition" (usually "attachement"). You can manually set
+"content_type", but be aware that if you also use the encoding tool, it
+may choke if the file extension is not recognized as belonging to a known
+Content-Type. Setting the content_type to "application/x-download" works
+in most cases, and should prompt the user with an Open/Save dialog in
+popular browsers.
+
+"""
+
+import os
+import os.path
+
+import cherrypy
+from cherrypy.lib import static
+
+localDir = os.path.dirname(__file__)
+absDir = os.path.join(os.getcwd(), localDir)
+
+
+class FileDemo(object):
+
+    @cherrypy.expose
+    def index(self):
+        return """
+        <html><body>
+            <h2>Upload a file</h2>
+            <form action="upload" method="post" enctype="multipart/form-data">
+            filename: <input type="file" name="myFile" /><br />
+            <input type="submit" />
+            </form>
+            <h2>Download a file</h2>
+            <a href='download'>This one</a>
+        </body></html>
+        """
+
+    @cherrypy.expose
+    def upload(self, myFile):
+        out = """<html>
+        <body>
+            myFile length: %s<br />
+            myFile filename: %s<br />
+            myFile mime-type: %s
+        </body>
+        </html>"""
+
+        # Although this just counts the file length, it demonstrates
+        # how to read large files in chunks instead of all at once.
+        # CherryPy reads the uploaded file into a temporary file;
+        # myFile.file.read reads from that.
+        size = 0
+        while True:
+            data = myFile.file.read(8192)
+            if not data:
+                break
+            size += len(data)
+
+        return out % (size, myFile.filename, myFile.content_type)
+
+    @cherrypy.expose
+    def download(self):
+        path = os.path.join(absDir, 'pdf_file.pdf')
+        return static.serve_file(path, 'application/x-download',
+                                 'attachment', os.path.basename(path))
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(FileDemo(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut10_http_errors.py b/libraries/cherrypy/tutorial/tut10_http_errors.py
new file mode 100644
index 00000000..18f02fd0
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tut10_http_errors.py
@@ -0,0 +1,84 @@
+"""
+
+Tutorial: HTTP errors
+
+HTTPError is used to return an error response to the client.
+CherryPy has lots of options regarding how such errors are
+logged, displayed, and formatted.
+
+"""
+
+import os
+import os.path
+
+import cherrypy
+
+localDir = os.path.dirname(__file__)
+curpath = os.path.normpath(os.path.join(os.getcwd(), localDir))
+
+
+class HTTPErrorDemo(object):
+
+    # Set a custom response for 403 errors.
+    _cp_config = {'error_page.403':
+                  os.path.join(curpath, 'custom_error.html')}
+
+    @cherrypy.expose
+    def index(self):
+        # display some links that will result in errors
+        tracebacks = cherrypy.request.show_tracebacks
+        if tracebacks:
+            trace = 'off'
+        else:
+            trace = 'on'
+
+        return """
+        <html><body>
+            <p>Toggle tracebacks <a href="toggleTracebacks">%s</a></p>
+            <p><a href="/doesNotExist">Click me; I'm a broken link!</a></p>
+            <p>
+              <a href="/error?code=403">
+                Use a custom error page from a file.
+              </a>
+            </p>
+            <p>These errors are explicitly raised by the application:</p>
+            <ul>
+                <li><a href="/error?code=400">400</a></li>
+                <li><a href="/error?code=401">401</a></li>
+                <li><a href="/error?code=402">402</a></li>
+                <li><a href="/error?code=500">500</a></li>
+            </ul>
+            <p><a href="/messageArg">You can also set the response body
+            when you raise an error.</a></p>
+        </body></html>
+        """ % trace
+
+    @cherrypy.expose
+    def toggleTracebacks(self):
+        # simple function to toggle tracebacks on and off
+        tracebacks = cherrypy.request.show_tracebacks
+        cherrypy.config.update({'request.show_tracebacks': not tracebacks})
+
+        # redirect back to the index
+        raise cherrypy.HTTPRedirect('/')
+
+    @cherrypy.expose
+    def error(self, code):
+        # raise an error based on the get query
+        raise cherrypy.HTTPError(status=code)
+
+    @cherrypy.expose
+    def messageArg(self):
+        message = ("If you construct an HTTPError with a 'message' "
+                   'argument, it wil be placed on the error page '
+                   '(underneath the status line by default).')
+        raise cherrypy.HTTPError(500, message=message)
+
+
+tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
+
+if __name__ == '__main__':
+    # CherryPy always starts with app.root when trying to map request URIs
+    # to objects, so we need to mount a request handler root. A request
+    # to '/' will be mapped to HelloWorld().index().
+    cherrypy.quickstart(HTTPErrorDemo(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tutorial.conf b/libraries/cherrypy/tutorial/tutorial.conf
new file mode 100644
index 00000000..43dfa60f
--- /dev/null
+++ b/libraries/cherrypy/tutorial/tutorial.conf
@@ -0,0 +1,4 @@
+[global]
+server.socket_host = "127.0.0.1"
+server.socket_port = 8080
+server.thread_pool = 10
diff --git a/libraries/contextlib2.py b/libraries/contextlib2.py
new file mode 100644
index 00000000..f08df14c
--- /dev/null
+++ b/libraries/contextlib2.py
@@ -0,0 +1,436 @@
+"""contextlib2 - backports and enhancements to the contextlib module"""
+
+import sys
+import warnings
+from collections import deque
+from functools import wraps
+
+__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
+           "redirect_stdout", "redirect_stderr", "suppress"]
+
+# Backwards compatibility
+__all__ += ["ContextStack"]
+
+class ContextDecorator(object):
+    "A base class or mixin that enables context managers to work as decorators."
+
+    def refresh_cm(self):
+        """Returns the context manager used to actually wrap the call to the
+        decorated function.
+
+        The default implementation just returns *self*.
+
+        Overriding this method allows otherwise one-shot context managers
+        like _GeneratorContextManager to support use as decorators via
+        implicit recreation.
+
+        DEPRECATED: refresh_cm was never added to the standard library's
+                    ContextDecorator API
+        """
+        warnings.warn("refresh_cm was never added to the standard library",
+                      DeprecationWarning)
+        return self._recreate_cm()
+
+    def _recreate_cm(self):
+        """Return a recreated instance of self.
+
+        Allows an otherwise one-shot context manager like
+        _GeneratorContextManager to support use as
+        a decorator via implicit recreation.
+
+        This is a private interface just for _GeneratorContextManager.
+        See issue #11647 for details.
+        """
+        return self
+
+    def __call__(self, func):
+        @wraps(func)
+        def inner(*args, **kwds):
+            with self._recreate_cm():
+                return func(*args, **kwds)
+        return inner
+
+
+class _GeneratorContextManager(ContextDecorator):
+    """Helper for @contextmanager decorator."""
+
+    def __init__(self, func, args, kwds):
+        self.gen = func(*args, **kwds)
+        self.func, self.args, self.kwds = func, args, kwds
+        # Issue 19330: ensure context manager instances have good docstrings
+        doc = getattr(func, "__doc__", None)
+        if doc is None:
+            doc = type(self).__doc__
+        self.__doc__ = doc
+        # Unfortunately, this still doesn't provide good help output when
+        # inspecting the created context manager instances, since pydoc
+        # currently bypasses the instance docstring and shows the docstring
+        # for the class instead.
+        # See http://bugs.python.org/issue19404 for more details.
+
+    def _recreate_cm(self):
+        # _GCM instances are one-shot context managers, so the
+        # CM must be recreated each time a decorated function is
+        # called
+        return self.__class__(self.func, self.args, self.kwds)
+
+    def __enter__(self):
+        try:
+            return next(self.gen)
+        except StopIteration:
+            raise RuntimeError("generator didn't yield")
+
+    def __exit__(self, type, value, traceback):
+        if type is None:
+            try:
+                next(self.gen)
+            except StopIteration:
+                return
+            else:
+                raise RuntimeError("generator didn't stop")
+        else:
+            if value is None:
+                # Need to force instantiation so we can reliably
+                # tell if we get the same exception back
+                value = type()
+            try:
+                self.gen.throw(type, value, traceback)
+                raise RuntimeError("generator didn't stop after throw()")
+            except StopIteration as exc:
+                # Suppress StopIteration *unless* it's the same exception that
+                # was passed to throw().  This prevents a StopIteration
+                # raised inside the "with" statement from being suppressed.
+                return exc is not value
+            except RuntimeError as exc:
+                # Don't re-raise the passed in exception
+                if exc is value:
+                    return False
+                # Likewise, avoid suppressing if a StopIteration exception
+                # was passed to throw() and later wrapped into a RuntimeError
+                # (see PEP 479).
+                if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value:
+                    return False
+                raise
+            except:
+                # only re-raise if it's *not* the exception that was
+                # passed to throw(), because __exit__() must not raise
+                # an exception unless __exit__() itself failed.  But throw()
+                # has to raise the exception to signal propagation, so this
+                # fixes the impedance mismatch between the throw() protocol
+                # and the __exit__() protocol.
+                #
+                if sys.exc_info()[1] is not value:
+                    raise
+
+
+def contextmanager(func):
+    """@contextmanager decorator.
+
+    Typical usage:
+
+        @contextmanager
+        def some_generator(<arguments>):
+            <setup>
+            try:
+                yield <value>
+            finally:
+                <cleanup>
+
+    This makes this:
+
+        with some_generator(<arguments>) as <variable>:
+            <body>
+
+    equivalent to this:
+
+        <setup>
+        try:
+            <variable> = <value>
+            <body>
+        finally:
+            <cleanup>
+
+    """
+    @wraps(func)
+    def helper(*args, **kwds):
+        return _GeneratorContextManager(func, args, kwds)
+    return helper
+
+
+class closing(object):
+    """Context to automatically close something at the end of a block.
+
+    Code like this:
+
+        with closing(<module>.open(<arguments>)) as f:
+            <block>
+
+    is equivalent to this:
+
+        f = <module>.open(<arguments>)
+        try:
+            <block>
+        finally:
+            f.close()
+
+    """
+    def __init__(self, thing):
+        self.thing = thing
+    def __enter__(self):
+        return self.thing
+    def __exit__(self, *exc_info):
+        self.thing.close()
+
+
+class _RedirectStream(object):
+
+    _stream = None
+
+    def __init__(self, new_target):
+        self._new_target = new_target
+        # We use a list of old targets to make this CM re-entrant
+        self._old_targets = []
+
+    def __enter__(self):
+        self._old_targets.append(getattr(sys, self._stream))
+        setattr(sys, self._stream, self._new_target)
+        return self._new_target
+
+    def __exit__(self, exctype, excinst, exctb):
+        setattr(sys, self._stream, self._old_targets.pop())
+
+
+class redirect_stdout(_RedirectStream):
+    """Context manager for temporarily redirecting stdout to another file.
+
+        # How to send help() to stderr
+        with redirect_stdout(sys.stderr):
+            help(dir)
+
+        # How to write help() to a file
+        with open('help.txt', 'w') as f:
+            with redirect_stdout(f):
+                help(pow)
+    """
+
+    _stream = "stdout"
+
+
+class redirect_stderr(_RedirectStream):
+    """Context manager for temporarily redirecting stderr to another file."""
+
+    _stream = "stderr"
+
+
+class suppress(object):
+    """Context manager to suppress specified exceptions
+
+    After the exception is suppressed, execution proceeds with the next
+    statement following the with statement.
+
+         with suppress(FileNotFoundError):
+             os.remove(somefile)
+         # Execution still resumes here if the file was already removed
+    """
+
+    def __init__(self, *exceptions):
+        self._exceptions = exceptions
+
+    def __enter__(self):
+        pass
+
+    def __exit__(self, exctype, excinst, exctb):
+        # Unlike isinstance and issubclass, CPython exception handling
+        # currently only looks at the concrete type hierarchy (ignoring
+        # the instance and subclass checking hooks). While Guido considers
+        # that a bug rather than a feature, it's a fairly hard one to fix
+        # due to various internal implementation details. suppress provides
+        # the simpler issubclass based semantics, rather than trying to
+        # exactly reproduce the limitations of the CPython interpreter.
+        #
+        # See http://bugs.python.org/issue12029 for more details
+        return exctype is not None and issubclass(exctype, self._exceptions)
+
+
+# Context manipulation is Python 3 only
+_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3
+if _HAVE_EXCEPTION_CHAINING:
+    def _make_context_fixer(frame_exc):
+        def _fix_exception_context(new_exc, old_exc):
+            # Context may not be correct, so find the end of the chain
+            while 1:
+                exc_context = new_exc.__context__
+                if exc_context is old_exc:
+                    # Context is already set correctly (see issue 20317)
+                    return
+                if exc_context is None or exc_context is frame_exc:
+                    break
+                new_exc = exc_context
+            # Change the end of the chain to point to the exception
+            # we expect it to reference
+            new_exc.__context__ = old_exc
+        return _fix_exception_context
+
+    def _reraise_with_existing_context(exc_details):
+        try:
+            # bare "raise exc_details[1]" replaces our carefully
+            # set-up context
+            fixed_ctx = exc_details[1].__context__
+            raise exc_details[1]
+        except BaseException:
+            exc_details[1].__context__ = fixed_ctx
+            raise
+else:
+    # No exception context in Python 2
+    def _make_context_fixer(frame_exc):
+        return lambda new_exc, old_exc: None
+
+    # Use 3 argument raise in Python 2,
+    # but use exec to avoid SyntaxError in Python 3
+    def _reraise_with_existing_context(exc_details):
+        exc_type, exc_value, exc_tb = exc_details
+        exec ("raise exc_type, exc_value, exc_tb")
+
+# Handle old-style classes if they exist
+try:
+    from types import InstanceType
+except ImportError:
+    # Python 3 doesn't have old-style classes
+    _get_type = type
+else:
+    # Need to handle old-style context managers on Python 2
+    def _get_type(obj):
+        obj_type = type(obj)
+        if obj_type is InstanceType:
+            return obj.__class__ # Old-style class
+        return obj_type # New-style class
+
+# Inspired by discussions on http://bugs.python.org/issue13585
+class ExitStack(object):
+    """Context manager for dynamic management of a stack of exit callbacks
+
+    For example:
+
+        with ExitStack() as stack:
+            files = [stack.enter_context(open(fname)) for fname in filenames]
+            # All opened files will automatically be closed at the end of
+            # the with statement, even if attempts to open files later
+            # in the list raise an exception
+
+    """
+    def __init__(self):
+        self._exit_callbacks = deque()
+
+    def pop_all(self):
+        """Preserve the context stack by transferring it to a new instance"""
+        new_stack = type(self)()
+        new_stack._exit_callbacks = self._exit_callbacks
+        self._exit_callbacks = deque()
+        return new_stack
+
+    def _push_cm_exit(self, cm, cm_exit):
+        """Helper to correctly register callbacks to __exit__ methods"""
+        def _exit_wrapper(*exc_details):
+            return cm_exit(cm, *exc_details)
+        _exit_wrapper.__self__ = cm
+        self.push(_exit_wrapper)
+
+    def push(self, exit):
+        """Registers a callback with the standard __exit__ method signature
+
+        Can suppress exceptions the same way __exit__ methods can.
+
+        Also accepts any object with an __exit__ method (registering a call
+        to the method instead of the object itself)
+        """
+        # We use an unbound method rather than a bound method to follow
+        # the standard lookup behaviour for special methods
+        _cb_type = _get_type(exit)
+        try:
+            exit_method = _cb_type.__exit__
+        except AttributeError:
+            # Not a context manager, so assume its a callable
+            self._exit_callbacks.append(exit)
+        else:
+            self._push_cm_exit(exit, exit_method)
+        return exit # Allow use as a decorator
+
+    def callback(self, callback, *args, **kwds):
+        """Registers an arbitrary callback and arguments.
+
+        Cannot suppress exceptions.
+        """
+        def _exit_wrapper(exc_type, exc, tb):
+            callback(*args, **kwds)
+        # We changed the signature, so using @wraps is not appropriate, but
+        # setting __wrapped__ may still help with introspection
+        _exit_wrapper.__wrapped__ = callback
+        self.push(_exit_wrapper)
+        return callback # Allow use as a decorator
+
+    def enter_context(self, cm):
+        """Enters the supplied context manager
+
+        If successful, also pushes its __exit__ method as a callback and
+        returns the result of the __enter__ method.
+        """
+        # We look up the special methods on the type to match the with statement
+        _cm_type = _get_type(cm)
+        _exit = _cm_type.__exit__
+        result = _cm_type.__enter__(cm)
+        self._push_cm_exit(cm, _exit)
+        return result
+
+    def close(self):
+        """Immediately unwind the context stack"""
+        self.__exit__(None, None, None)
+
+    def __enter__(self):
+        return self
+
+    def __exit__(self, *exc_details):
+        received_exc = exc_details[0] is not None
+
+        # We manipulate the exception state so it behaves as though
+        # we were actually nesting multiple with statements
+        frame_exc = sys.exc_info()[1]
+        _fix_exception_context = _make_context_fixer(frame_exc)
+
+        # Callbacks are invoked in LIFO order to match the behaviour of
+        # nested context managers
+        suppressed_exc = False
+        pending_raise = False
+        while self._exit_callbacks:
+            cb = self._exit_callbacks.pop()
+            try:
+                if cb(*exc_details):
+                    suppressed_exc = True
+                    pending_raise = False
+                    exc_details = (None, None, None)
+            except:
+                new_exc_details = sys.exc_info()
+                # simulate the stack of exceptions by setting the context
+                _fix_exception_context(new_exc_details[1], exc_details[1])
+                pending_raise = True
+                exc_details = new_exc_details
+        if pending_raise:
+            _reraise_with_existing_context(exc_details)
+        return received_exc and suppressed_exc
+
+# Preserve backwards compatibility
+class ContextStack(ExitStack):
+    """Backwards compatibility alias for ExitStack"""
+
+    def __init__(self):
+        warnings.warn("ContextStack has been renamed to ExitStack",
+                      DeprecationWarning)
+        super(ContextStack, self).__init__()
+
+    def register_exit(self, callback):
+        return self.push(callback)
+
+    def register(self, callback, *args, **kwds):
+        return self.callback(callback, *args, **kwds)
+
+    def preserve(self):
+        return self.pop_all()
diff --git a/libraries/more_itertools/__init__.py b/libraries/more_itertools/__init__.py
new file mode 100644
index 00000000..bba462c3
--- /dev/null
+++ b/libraries/more_itertools/__init__.py
@@ -0,0 +1,2 @@
+from more_itertools.more import *  # noqa
+from more_itertools.recipes import *  # noqa
diff --git a/libraries/more_itertools/more.py b/libraries/more_itertools/more.py
new file mode 100644
index 00000000..05e851ee
--- /dev/null
+++ b/libraries/more_itertools/more.py
@@ -0,0 +1,2211 @@
+from __future__ import print_function
+
+from collections import Counter, defaultdict, deque
+from functools import partial, wraps
+from heapq import merge
+from itertools import (
+    chain,
+    compress,
+    count,
+    cycle,
+    dropwhile,
+    groupby,
+    islice,
+    repeat,
+    starmap,
+    takewhile,
+    tee
+)
+from operator import itemgetter, lt, gt, sub
+from sys import maxsize, version_info
+try:
+    from collections.abc import Sequence
+except ImportError:
+    from collections import Sequence
+
+from six import binary_type, string_types, text_type
+from six.moves import filter, map, range, zip, zip_longest
+
+from .recipes import consume, flatten, take
+
+__all__ = [
+    'adjacent',
+    'always_iterable',
+    'always_reversible',
+    'bucket',
+    'chunked',
+    'circular_shifts',
+    'collapse',
+    'collate',
+    'consecutive_groups',
+    'consumer',
+    'count_cycle',
+    'difference',
+    'distinct_permutations',
+    'distribute',
+    'divide',
+    'exactly_n',
+    'first',
+    'groupby_transform',
+    'ilen',
+    'interleave_longest',
+    'interleave',
+    'intersperse',
+    'islice_extended',
+    'iterate',
+    'last',
+    'locate',
+    'lstrip',
+    'make_decorator',
+    'map_reduce',
+    'numeric_range',
+    'one',
+    'padded',
+    'peekable',
+    'replace',
+    'rlocate',
+    'rstrip',
+    'run_length',
+    'seekable',
+    'SequenceView',
+    'side_effect',
+    'sliced',
+    'sort_together',
+    'split_at',
+    'split_after',
+    'split_before',
+    'spy',
+    'stagger',
+    'strip',
+    'unique_to_each',
+    'windowed',
+    'with_iter',
+    'zip_offset',
+]
+
+_marker = object()
+
+
+def chunked(iterable, n):
+    """Break *iterable* into lists of length *n*:
+
+        >>> list(chunked([1, 2, 3, 4, 5, 6], 3))
+        [[1, 2, 3], [4, 5, 6]]
+
+    If the length of *iterable* is not evenly divisible by *n*, the last
+    returned list will be shorter:
+
+        >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3))
+        [[1, 2, 3], [4, 5, 6], [7, 8]]
+
+    To use a fill-in value instead, see the :func:`grouper` recipe.
+
+    :func:`chunked` is useful for splitting up a computation on a large number
+    of keys into batches, to be pickled and sent off to worker processes. One
+    example is operations on rows in MySQL, which does not implement
+    server-side cursors properly and would otherwise load the entire dataset
+    into RAM on the client.
+
+    """
+    return iter(partial(take, n, iter(iterable)), [])
+
+
+def first(iterable, default=_marker):
+    """Return the first item of *iterable*, or *default* if *iterable* is
+    empty.
+
+        >>> first([0, 1, 2, 3])
+        0
+        >>> first([], 'some default')
+        'some default'
+
+    If *default* is not provided and there are no items in the iterable,
+    raise ``ValueError``.
+
+    :func:`first` is useful when you have a generator of expensive-to-retrieve
+    values and want any arbitrary one. It is marginally shorter than
+    ``next(iter(iterable), default)``.
+
+    """
+    try:
+        return next(iter(iterable))
+    except StopIteration:
+        # I'm on the edge about raising ValueError instead of StopIteration. At
+        # the moment, ValueError wins, because the caller could conceivably
+        # want to do something different with flow control when I raise the
+        # exception, and it's weird to explicitly catch StopIteration.
+        if default is _marker:
+            raise ValueError('first() was called on an empty iterable, and no '
+                             'default value was provided.')
+        return default
+
+
+def last(iterable, default=_marker):
+    """Return the last item of *iterable*, or *default* if *iterable* is
+    empty.
+
+        >>> last([0, 1, 2, 3])
+        3
+        >>> last([], 'some default')
+        'some default'
+
+    If *default* is not provided and there are no items in the iterable,
+    raise ``ValueError``.
+    """
+    try:
+        try:
+            # Try to access the last item directly
+            return iterable[-1]
+        except (TypeError, AttributeError, KeyError):
+            # If not slice-able, iterate entirely using length-1 deque
+            return deque(iterable, maxlen=1)[0]
+    except IndexError:  # If the iterable was empty
+        if default is _marker:
+            raise ValueError('last() was called on an empty iterable, and no '
+                             'default value was provided.')
+        return default
+
+
+class peekable(object):
+    """Wrap an iterator to allow lookahead and prepending elements.
+
+    Call :meth:`peek` on the result to get the value that will be returned
+    by :func:`next`. This won't advance the iterator:
+
+        >>> p = peekable(['a', 'b'])
+        >>> p.peek()
+        'a'
+        >>> next(p)
+        'a'
+
+    Pass :meth:`peek` a default value to return that instead of raising
+    ``StopIteration`` when the iterator is exhausted.
+
+        >>> p = peekable([])
+        >>> p.peek('hi')
+        'hi'
+
+    peekables also offer a :meth:`prepend` method, which "inserts" items
+    at the head of the iterable:
+
+        >>> p = peekable([1, 2, 3])
+        >>> p.prepend(10, 11, 12)
+        >>> next(p)
+        10
+        >>> p.peek()
+        11
+        >>> list(p)
+        [11, 12, 1, 2, 3]
+
+    peekables can be indexed. Index 0 is the item that will be returned by
+    :func:`next`, index 1 is the item after that, and so on:
+    The values up to the given index will be cached.
+
+        >>> p = peekable(['a', 'b', 'c', 'd'])
+        >>> p[0]
+        'a'
+        >>> p[1]
+        'b'
+        >>> next(p)
+        'a'
+
+    Negative indexes are supported, but be aware that they will cache the
+    remaining items in the source iterator, which may require significant
+    storage.
+
+    To check whether a peekable is exhausted, check its truth value:
+
+        >>> p = peekable(['a', 'b'])
+        >>> if p:  # peekable has items
+        ...     list(p)
+        ['a', 'b']
+        >>> if not p:  # peekable is exhaused
+        ...     list(p)
+        []
+
+    """
+    def __init__(self, iterable):
+        self._it = iter(iterable)
+        self._cache = deque()
+
+    def __iter__(self):
+        return self
+
+    def __bool__(self):
+        try:
+            self.peek()
+        except StopIteration:
+            return False
+        return True
+
+    def __nonzero__(self):
+        # For Python 2 compatibility
+        return self.__bool__()
+
+    def peek(self, default=_marker):
+        """Return the item that will be next returned from ``next()``.
+
+        Return ``default`` if there are no items left. If ``default`` is not
+        provided, raise ``StopIteration``.
+
+        """
+        if not self._cache:
+            try:
+                self._cache.append(next(self._it))
+            except StopIteration:
+                if default is _marker:
+                    raise
+                return default
+        return self._cache[0]
+
+    def prepend(self, *items):
+        """Stack up items to be the next ones returned from ``next()`` or
+        ``self.peek()``. The items will be returned in
+        first in, first out order::
+
+            >>> p = peekable([1, 2, 3])
+            >>> p.prepend(10, 11, 12)
+            >>> next(p)
+            10
+            >>> list(p)
+            [11, 12, 1, 2, 3]
+
+        It is possible, by prepending items, to "resurrect" a peekable that
+        previously raised ``StopIteration``.
+
+            >>> p = peekable([])
+            >>> next(p)
+            Traceback (most recent call last):
+              ...
+            StopIteration
+            >>> p.prepend(1)
+            >>> next(p)
+            1
+            >>> next(p)
+            Traceback (most recent call last):
+              ...
+            StopIteration
+
+        """
+        self._cache.extendleft(reversed(items))
+
+    def __next__(self):
+        if self._cache:
+            return self._cache.popleft()
+
+        return next(self._it)
+
+    next = __next__  # For Python 2 compatibility
+
+    def _get_slice(self, index):
+        # Normalize the slice's arguments
+        step = 1 if (index.step is None) else index.step
+        if step > 0:
+            start = 0 if (index.start is None) else index.start
+            stop = maxsize if (index.stop is None) else index.stop
+        elif step < 0:
+            start = -1 if (index.start is None) else index.start
+            stop = (-maxsize - 1) if (index.stop is None) else index.stop
+        else:
+            raise ValueError('slice step cannot be zero')
+
+        # If either the start or stop index is negative, we'll need to cache
+        # the rest of the iterable in order to slice from the right side.
+        if (start < 0) or (stop < 0):
+            self._cache.extend(self._it)
+        # Otherwise we'll need to find the rightmost index and cache to that
+        # point.
+        else:
+            n = min(max(start, stop) + 1, maxsize)
+            cache_len = len(self._cache)
+            if n >= cache_len:
+                self._cache.extend(islice(self._it, n - cache_len))
+
+        return list(self._cache)[index]
+
+    def __getitem__(self, index):
+        if isinstance(index, slice):
+            return self._get_slice(index)
+
+        cache_len = len(self._cache)
+        if index < 0:
+            self._cache.extend(self._it)
+        elif index >= cache_len:
+            self._cache.extend(islice(self._it, index + 1 - cache_len))
+
+        return self._cache[index]
+
+
+def _collate(*iterables, **kwargs):
+    """Helper for ``collate()``, called when the user is using the ``reverse``
+    or ``key`` keyword arguments on Python versions below 3.5.
+
+    """
+    key = kwargs.pop('key', lambda a: a)
+    reverse = kwargs.pop('reverse', False)
+
+    min_or_max = partial(max if reverse else min, key=itemgetter(0))
+    peekables = [peekable(it) for it in iterables]
+    peekables = [p for p in peekables if p]  # Kill empties.
+    while peekables:
+        _, p = min_or_max((key(p.peek()), p) for p in peekables)
+        yield next(p)
+        peekables = [x for x in peekables if x]
+
+
+def collate(*iterables, **kwargs):
+    """Return a sorted merge of the items from each of several already-sorted
+    *iterables*.
+
+        >>> list(collate('ACDZ', 'AZ', 'JKL'))
+        ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z']
+
+    Works lazily, keeping only the next value from each iterable in memory. Use
+    :func:`collate` to, for example, perform a n-way mergesort of items that
+    don't fit in memory.
+
+    If a *key* function is specified, the iterables will be sorted according
+    to its result:
+
+        >>> key = lambda s: int(s)  # Sort by numeric value, not by string
+        >>> list(collate(['1', '10'], ['2', '11'], key=key))
+        ['1', '2', '10', '11']
+
+
+    If the *iterables* are sorted in descending order, set *reverse* to
+    ``True``:
+
+        >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True))
+        [5, 4, 3, 2, 1, 0]
+
+    If the elements of the passed-in iterables are out of order, you might get
+    unexpected results.
+
+    On Python 2.7, this function delegates to :func:`heapq.merge` if neither
+    of the keyword arguments are specified. On Python 3.5+, this function
+    is an alias for :func:`heapq.merge`.
+
+    """
+    if not kwargs:
+        return merge(*iterables)
+
+    return _collate(*iterables, **kwargs)
+
+
+# If using Python version 3.5 or greater, heapq.merge() will be faster than
+# collate - use that instead.
+if version_info >= (3, 5, 0):
+    _collate_docstring = collate.__doc__
+    collate = partial(merge)
+    collate.__doc__ = _collate_docstring
+
+
+def consumer(func):
+    """Decorator that automatically advances a PEP-342-style "reverse iterator"
+    to its first yield point so you don't have to call ``next()`` on it
+    manually.
+
+        >>> @consumer
+        ... def tally():
+        ...     i = 0
+        ...     while True:
+        ...         print('Thing number %s is %s.' % (i, (yield)))
+        ...         i += 1
+        ...
+        >>> t = tally()
+        >>> t.send('red')
+        Thing number 0 is red.
+        >>> t.send('fish')
+        Thing number 1 is fish.
+
+    Without the decorator, you would have to call ``next(t)`` before
+    ``t.send()`` could be used.
+
+    """
+    @wraps(func)
+    def wrapper(*args, **kwargs):
+        gen = func(*args, **kwargs)
+        next(gen)
+        return gen
+    return wrapper
+
+
+def ilen(iterable):
+    """Return the number of items in *iterable*.
+
+        >>> ilen(x for x in range(1000000) if x % 3 == 0)
+        333334
+
+    This consumes the iterable, so handle with care.
+
+    """
+    # maxlen=1 only stores the last item in the deque
+    d = deque(enumerate(iterable, 1), maxlen=1)
+    # since we started enumerate at 1,
+    # the first item of the last pair will be the length of the iterable
+    # (assuming there were items)
+    return d[0][0] if d else 0
+
+
+def iterate(func, start):
+    """Return ``start``, ``func(start)``, ``func(func(start))``, ...
+
+        >>> from itertools import islice
+        >>> list(islice(iterate(lambda x: 2*x, 1), 10))
+        [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
+
+    """
+    while True:
+        yield start
+        start = func(start)
+
+
+def with_iter(context_manager):
+    """Wrap an iterable in a ``with`` statement, so it closes once exhausted.
+
+    For example, this will close the file when the iterator is exhausted::
+
+        upper_lines = (line.upper() for line in with_iter(open('foo')))
+
+    Any context manager which returns an iterable is a candidate for
+    ``with_iter``.
+
+    """
+    with context_manager as iterable:
+        for item in iterable:
+            yield item
+
+
+def one(iterable, too_short=None, too_long=None):
+    """Return the first item from *iterable*, which is expected to contain only
+    that item. Raise an exception if *iterable* is empty or has more than one
+    item.
+
+    :func:`one` is useful for ensuring that an iterable contains only one item.
+    For example, it can be used to retrieve the result of a database query
+    that is expected to return a single row.
+
+    If *iterable* is empty, ``ValueError`` will be raised. You may specify a
+    different exception with the *too_short* keyword:
+
+        >>> it = []
+        >>> one(it)  # doctest: +IGNORE_EXCEPTION_DETAIL
+        Traceback (most recent call last):
+        ...
+        ValueError: too many items in iterable (expected 1)'
+        >>> too_short = IndexError('too few items')
+        >>> one(it, too_short=too_short)  # doctest: +IGNORE_EXCEPTION_DETAIL
+        Traceback (most recent call last):
+        ...
+        IndexError: too few items
+
+    Similarly, if *iterable* contains more than one item, ``ValueError`` will
+    be raised. You may specify a different exception with the *too_long*
+    keyword:
+
+        >>> it = ['too', 'many']
+        >>> one(it)  # doctest: +IGNORE_EXCEPTION_DETAIL
+        Traceback (most recent call last):
+        ...
+        ValueError: too many items in iterable (expected 1)'
+        >>> too_long = RuntimeError
+        >>> one(it, too_long=too_long)  # doctest: +IGNORE_EXCEPTION_DETAIL
+        Traceback (most recent call last):
+        ...
+        RuntimeError
+
+    Note that :func:`one` attempts to advance *iterable* twice to ensure there
+    is only one item. If there is more than one, both items will be discarded.
+    See :func:`spy` or :func:`peekable` to check iterable contents less
+    destructively.
+
+    """
+    it = iter(iterable)
+
+    try:
+        value = next(it)
+    except StopIteration:
+        raise too_short or ValueError('too few items in iterable (expected 1)')
+
+    try:
+        next(it)
+    except StopIteration:
+        pass
+    else:
+        raise too_long or ValueError('too many items in iterable (expected 1)')
+
+    return value
+
+
+def distinct_permutations(iterable):
+    """Yield successive distinct permutations of the elements in *iterable*.
+
+        >>> sorted(distinct_permutations([1, 0, 1]))
+        [(0, 1, 1), (1, 0, 1), (1, 1, 0)]
+
+    Equivalent to ``set(permutations(iterable))``, except duplicates are not
+    generated and thrown away. For larger input sequences this is much more
+    efficient.
+
+    Duplicate permutations arise when there are duplicated elements in the
+    input iterable. The number of items returned is
+    `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of
+    items input, and each `x_i` is the count of a distinct item in the input
+    sequence.
+
+    """
+    def perm_unique_helper(item_counts, perm, i):
+        """Internal helper function
+
+        :arg item_counts: Stores the unique items in ``iterable`` and how many
+            times they are repeated
+        :arg perm: The permutation that is being built for output
+        :arg i: The index of the permutation being modified
+
+        The output permutations are built up recursively; the distinct items
+        are placed until their repetitions are exhausted.
+        """
+        if i < 0:
+            yield tuple(perm)
+        else:
+            for item in item_counts:
+                if item_counts[item] <= 0:
+                    continue
+                perm[i] = item
+                item_counts[item] -= 1
+                for x in perm_unique_helper(item_counts, perm, i - 1):
+                    yield x
+                item_counts[item] += 1
+
+    item_counts = Counter(iterable)
+    length = sum(item_counts.values())
+
+    return perm_unique_helper(item_counts, [None] * length, length - 1)
+
+
+def intersperse(e, iterable, n=1):
+    """Intersperse filler element *e* among the items in *iterable*, leaving
+    *n* items between each filler element.
+
+        >>> list(intersperse('!', [1, 2, 3, 4, 5]))
+        [1, '!', 2, '!', 3, '!', 4, '!', 5]
+
+        >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2))
+        [1, 2, None, 3, 4, None, 5]
+
+    """
+    if n == 0:
+        raise ValueError('n must be > 0')
+    elif n == 1:
+        # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2...
+        # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2...
+        return islice(interleave(repeat(e), iterable), 1, None)
+    else:
+        # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]...
+        # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]...
+        # flatten(...) -> x_0, x_1, e, x_2, x_3...
+        filler = repeat([e])
+        chunks = chunked(iterable, n)
+        return flatten(islice(interleave(filler, chunks), 1, None))
+
+
+def unique_to_each(*iterables):
+    """Return the elements from each of the input iterables that aren't in the
+    other input iterables.
+
+    For example, suppose you have a set of packages, each with a set of
+    dependencies::
+
+        {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}}
+
+    If you remove one package, which dependencies can also be removed?
+
+    If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not
+    associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for
+    ``pkg_2``, and ``D`` is only needed for ``pkg_3``::
+
+        >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'})
+        [['A'], ['C'], ['D']]
+
+    If there are duplicates in one input iterable that aren't in the others
+    they will be duplicated in the output. Input order is preserved::
+
+        >>> unique_to_each("mississippi", "missouri")
+        [['p', 'p'], ['o', 'u', 'r']]
+
+    It is assumed that the elements of each iterable are hashable.
+
+    """
+    pool = [list(it) for it in iterables]
+    counts = Counter(chain.from_iterable(map(set, pool)))
+    uniques = {element for element in counts if counts[element] == 1}
+    return [list(filter(uniques.__contains__, it)) for it in pool]
+
+
+def windowed(seq, n, fillvalue=None, step=1):
+    """Return a sliding window of width *n* over the given iterable.
+
+        >>> all_windows = windowed([1, 2, 3, 4, 5], 3)
+        >>> list(all_windows)
+        [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
+
+    When the window is larger than the iterable, *fillvalue* is used in place
+    of missing values::
+
+        >>> list(windowed([1, 2, 3], 4))
+        [(1, 2, 3, None)]
+
+    Each window will advance in increments of *step*:
+
+        >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2))
+        [(1, 2, 3), (3, 4, 5), (5, 6, '!')]
+
+    """
+    if n < 0:
+        raise ValueError('n must be >= 0')
+    if n == 0:
+        yield tuple()
+        return
+    if step < 1:
+        raise ValueError('step must be >= 1')
+
+    it = iter(seq)
+    window = deque([], n)
+    append = window.append
+
+    # Initial deque fill
+    for _ in range(n):
+        append(next(it, fillvalue))
+    yield tuple(window)
+
+    # Appending new items to the right causes old items to fall off the left
+    i = 0
+    for item in it:
+        append(item)
+        i = (i + 1) % step
+        if i % step == 0:
+            yield tuple(window)
+
+    # If there are items from the iterable in the window, pad with the given
+    # value and emit them.
+    if (i % step) and (step - i < n):
+        for _ in range(step - i):
+            append(fillvalue)
+        yield tuple(window)
+
+
+class bucket(object):
+    """Wrap *iterable* and return an object that buckets it iterable into
+    child iterables based on a *key* function.
+
+        >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
+        >>> s = bucket(iterable, key=lambda x: x[0])
+        >>> a_iterable = s['a']
+        >>> next(a_iterable)
+        'a1'
+        >>> next(a_iterable)
+        'a2'
+        >>> list(s['b'])
+        ['b1', 'b2', 'b3']
+
+    The original iterable will be advanced and its items will be cached until
+    they are used by the child iterables. This may require significant storage.
+
+    By default, attempting to select a bucket to which no items belong  will
+    exhaust the iterable and cache all values.
+    If you specify a *validator* function, selected buckets will instead be
+    checked against it.
+
+        >>> from itertools import count
+        >>> it = count(1, 2)  # Infinite sequence of odd numbers
+        >>> key = lambda x: x % 10  # Bucket by last digit
+        >>> validator = lambda x: x in {1, 3, 5, 7, 9}  # Odd digits only
+        >>> s = bucket(it, key=key, validator=validator)
+        >>> 2 in s
+        False
+        >>> list(s[2])
+        []
+
+    """
+    def __init__(self, iterable, key, validator=None):
+        self._it = iter(iterable)
+        self._key = key
+        self._cache = defaultdict(deque)
+        self._validator = validator or (lambda x: True)
+
+    def __contains__(self, value):
+        if not self._validator(value):
+            return False
+
+        try:
+            item = next(self[value])
+        except StopIteration:
+            return False
+        else:
+            self._cache[value].appendleft(item)
+
+        return True
+
+    def _get_values(self, value):
+        """
+        Helper to yield items from the parent iterator that match *value*.
+        Items that don't match are stored in the local cache as they
+        are encountered.
+        """
+        while True:
+            # If we've cached some items that match the target value, emit
+            # the first one and evict it from the cache.
+            if self._cache[value]:
+                yield self._cache[value].popleft()
+            # Otherwise we need to advance the parent iterator to search for
+            # a matching item, caching the rest.
+            else:
+                while True:
+                    try:
+                        item = next(self._it)
+                    except StopIteration:
+                        return
+                    item_value = self._key(item)
+                    if item_value == value:
+                        yield item
+                        break
+                    elif self._validator(item_value):
+                        self._cache[item_value].append(item)
+
+    def __getitem__(self, value):
+        if not self._validator(value):
+            return iter(())
+
+        return self._get_values(value)
+
+
+def spy(iterable, n=1):
+    """Return a 2-tuple with a list containing the first *n* elements of
+    *iterable*, and an iterator with the same items as *iterable*.
+    This allows you to "look ahead" at the items in the iterable without
+    advancing it.
+
+    There is one item in the list by default:
+
+        >>> iterable = 'abcdefg'
+        >>> head, iterable = spy(iterable)
+        >>> head
+        ['a']
+        >>> list(iterable)
+        ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+
+    You may use unpacking to retrieve items instead of lists:
+
+        >>> (head,), iterable = spy('abcdefg')
+        >>> head
+        'a'
+        >>> (first, second), iterable = spy('abcdefg', 2)
+        >>> first
+        'a'
+        >>> second
+        'b'
+
+    The number of items requested can be larger than the number of items in
+    the iterable:
+
+        >>> iterable = [1, 2, 3, 4, 5]
+        >>> head, iterable = spy(iterable, 10)
+        >>> head
+        [1, 2, 3, 4, 5]
+        >>> list(iterable)
+        [1, 2, 3, 4, 5]
+
+    """
+    it = iter(iterable)
+    head = take(n, it)
+
+    return head, chain(head, it)
+
+
+def interleave(*iterables):
+    """Return a new iterable yielding from each iterable in turn,
+    until the shortest is exhausted.
+
+        >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8]))
+        [1, 4, 6, 2, 5, 7]
+
+    For a version that doesn't terminate after the shortest iterable is
+    exhausted, see :func:`interleave_longest`.
+
+    """
+    return chain.from_iterable(zip(*iterables))
+
+
+def interleave_longest(*iterables):
+    """Return a new iterable yielding from each iterable in turn,
+    skipping any that are exhausted.
+
+        >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8]))
+        [1, 4, 6, 2, 5, 7, 3, 8]
+
+    This function produces the same output as :func:`roundrobin`, but may
+    perform better for some inputs (in particular when the number of iterables
+    is large).
+
+    """
+    i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker))
+    return (x for x in i if x is not _marker)
+
+
+def collapse(iterable, base_type=None, levels=None):
+    """Flatten an iterable with multiple levels of nesting (e.g., a list of
+    lists of tuples) into non-iterable types.
+
+        >>> iterable = [(1, 2), ([3, 4], [[5], [6]])]
+        >>> list(collapse(iterable))
+        [1, 2, 3, 4, 5, 6]
+
+    String types are not considered iterable and will not be collapsed.
+    To avoid collapsing other types, specify *base_type*:
+
+        >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']]
+        >>> list(collapse(iterable, base_type=tuple))
+        ['ab', ('cd', 'ef'), 'gh', 'ij']
+
+    Specify *levels* to stop flattening after a certain level:
+
+    >>> iterable = [('a', ['b']), ('c', ['d'])]
+    >>> list(collapse(iterable))  # Fully flattened
+    ['a', 'b', 'c', 'd']
+    >>> list(collapse(iterable, levels=1))  # Only one level flattened
+    ['a', ['b'], 'c', ['d']]
+
+    """
+    def walk(node, level):
+        if (
+            ((levels is not None) and (level > levels)) or
+            isinstance(node, string_types) or
+            ((base_type is not None) and isinstance(node, base_type))
+        ):
+            yield node
+            return
+
+        try:
+            tree = iter(node)
+        except TypeError:
+            yield node
+            return
+        else:
+            for child in tree:
+                for x in walk(child, level + 1):
+                    yield x
+
+    for x in walk(iterable, 0):
+        yield x
+
+
+def side_effect(func, iterable, chunk_size=None, before=None, after=None):
+    """Invoke *func* on each item in *iterable* (or on each *chunk_size* group
+    of items) before yielding the item.
+
+    `func` must be a function that takes a single argument. Its return value
+    will be discarded.
+
+    *before* and *after* are optional functions that take no arguments. They
+    will be executed before iteration starts and after it ends, respectively.
+
+    `side_effect` can be used for logging, updating progress bars, or anything
+    that is not functionally "pure."
+
+    Emitting a status message:
+
+        >>> from more_itertools import consume
+        >>> func = lambda item: print('Received {}'.format(item))
+        >>> consume(side_effect(func, range(2)))
+        Received 0
+        Received 1
+
+    Operating on chunks of items:
+
+        >>> pair_sums = []
+        >>> func = lambda chunk: pair_sums.append(sum(chunk))
+        >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2))
+        [0, 1, 2, 3, 4, 5]
+        >>> list(pair_sums)
+        [1, 5, 9]
+
+    Writing to a file-like object:
+
+        >>> from io import StringIO
+        >>> from more_itertools import consume
+        >>> f = StringIO()
+        >>> func = lambda x: print(x, file=f)
+        >>> before = lambda: print(u'HEADER', file=f)
+        >>> after = f.close
+        >>> it = [u'a', u'b', u'c']
+        >>> consume(side_effect(func, it, before=before, after=after))
+        >>> f.closed
+        True
+
+    """
+    try:
+        if before is not None:
+            before()
+
+        if chunk_size is None:
+            for item in iterable:
+                func(item)
+                yield item
+        else:
+            for chunk in chunked(iterable, chunk_size):
+                func(chunk)
+                for item in chunk:
+                    yield item
+    finally:
+        if after is not None:
+            after()
+
+
+def sliced(seq, n):
+    """Yield slices of length *n* from the sequence *seq*.
+
+        >>> list(sliced((1, 2, 3, 4, 5, 6), 3))
+        [(1, 2, 3), (4, 5, 6)]
+
+    If the length of the sequence is not divisible by the requested slice
+    length, the last slice will be shorter.
+
+        >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3))
+        [(1, 2, 3), (4, 5, 6), (7, 8)]
+
+    This function will only work for iterables that support slicing.
+    For non-sliceable iterables, see :func:`chunked`.
+
+    """
+    return takewhile(bool, (seq[i: i + n] for i in count(0, n)))
+
+
+def split_at(iterable, pred):
+    """Yield lists of items from *iterable*, where each list is delimited by
+    an item where callable *pred* returns ``True``. The lists do not include
+    the delimiting items.
+
+        >>> list(split_at('abcdcba', lambda x: x == 'b'))
+        [['a'], ['c', 'd', 'c'], ['a']]
+
+        >>> list(split_at(range(10), lambda n: n % 2 == 1))
+        [[0], [2], [4], [6], [8], []]
+    """
+    buf = []
+    for item in iterable:
+        if pred(item):
+            yield buf
+            buf = []
+        else:
+            buf.append(item)
+    yield buf
+
+
+def split_before(iterable, pred):
+    """Yield lists of items from *iterable*, where each list starts with an
+    item where callable *pred* returns ``True``:
+
+        >>> list(split_before('OneTwo', lambda s: s.isupper()))
+        [['O', 'n', 'e'], ['T', 'w', 'o']]
+
+        >>> list(split_before(range(10), lambda n: n % 3 == 0))
+        [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
+
+    """
+    buf = []
+    for item in iterable:
+        if pred(item) and buf:
+            yield buf
+            buf = []
+        buf.append(item)
+    yield buf
+
+
+def split_after(iterable, pred):
+    """Yield lists of items from *iterable*, where each list ends with an
+    item where callable *pred* returns ``True``:
+
+        >>> list(split_after('one1two2', lambda s: s.isdigit()))
+        [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']]
+
+        >>> list(split_after(range(10), lambda n: n % 3 == 0))
+        [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]]
+
+    """
+    buf = []
+    for item in iterable:
+        buf.append(item)
+        if pred(item) and buf:
+            yield buf
+            buf = []
+    if buf:
+        yield buf
+
+
+def padded(iterable, fillvalue=None, n=None, next_multiple=False):
+    """Yield the elements from *iterable*, followed by *fillvalue*, such that
+    at least *n* items are emitted.
+
+        >>> list(padded([1, 2, 3], '?', 5))
+        [1, 2, 3, '?', '?']
+
+    If *next_multiple* is ``True``, *fillvalue* will be emitted until the
+    number of items emitted is a multiple of *n*::
+
+        >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True))
+        [1, 2, 3, 4, None, None]
+
+    If *n* is ``None``, *fillvalue* will be emitted indefinitely.
+
+    """
+    it = iter(iterable)
+    if n is None:
+        for item in chain(it, repeat(fillvalue)):
+            yield item
+    elif n < 1:
+        raise ValueError('n must be at least 1')
+    else:
+        item_count = 0
+        for item in it:
+            yield item
+            item_count += 1
+
+        remaining = (n - item_count) % n if next_multiple else n - item_count
+        for _ in range(remaining):
+            yield fillvalue
+
+
+def distribute(n, iterable):
+    """Distribute the items from *iterable* among *n* smaller iterables.
+
+        >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6])
+        >>> list(group_1)
+        [1, 3, 5]
+        >>> list(group_2)
+        [2, 4, 6]
+
+    If the length of *iterable* is not evenly divisible by *n*, then the
+    length of the returned iterables will not be identical:
+
+        >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7])
+        >>> [list(c) for c in children]
+        [[1, 4, 7], [2, 5], [3, 6]]
+
+    If the length of *iterable* is smaller than *n*, then the last returned
+    iterables will be empty:
+
+        >>> children = distribute(5, [1, 2, 3])
+        >>> [list(c) for c in children]
+        [[1], [2], [3], [], []]
+
+    This function uses :func:`itertools.tee` and may require significant
+    storage. If you need the order items in the smaller iterables to match the
+    original iterable, see :func:`divide`.
+
+    """
+    if n < 1:
+        raise ValueError('n must be at least 1')
+
+    children = tee(iterable, n)
+    return [islice(it, index, None, n) for index, it in enumerate(children)]
+
+
+def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None):
+    """Yield tuples whose elements are offset from *iterable*.
+    The amount by which the `i`-th item in each tuple is offset is given by
+    the `i`-th item in *offsets*.
+
+        >>> list(stagger([0, 1, 2, 3]))
+        [(None, 0, 1), (0, 1, 2), (1, 2, 3)]
+        >>> list(stagger(range(8), offsets=(0, 2, 4)))
+        [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)]
+
+    By default, the sequence will end when the final element of a tuple is the
+    last item in the iterable. To continue until the first element of a tuple
+    is the last item in the iterable, set *longest* to ``True``::
+
+        >>> list(stagger([0, 1, 2, 3], longest=True))
+        [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)]
+
+    By default, ``None`` will be used to replace offsets beyond the end of the
+    sequence. Specify *fillvalue* to use some other value.
+
+    """
+    children = tee(iterable, len(offsets))
+
+    return zip_offset(
+        *children, offsets=offsets, longest=longest, fillvalue=fillvalue
+    )
+
+
+def zip_offset(*iterables, **kwargs):
+    """``zip`` the input *iterables* together, but offset the `i`-th iterable
+    by the `i`-th item in *offsets*.
+
+        >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1)))
+        [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')]
+
+    This can be used as a lightweight alternative to SciPy or pandas to analyze
+    data sets in which somes series have a lead or lag relationship.
+
+    By default, the sequence will end when the shortest iterable is exhausted.
+    To continue until the longest iterable is exhausted, set *longest* to
+    ``True``.
+
+        >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True))
+        [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')]
+
+    By default, ``None`` will be used to replace offsets beyond the end of the
+    sequence. Specify *fillvalue* to use some other value.
+
+    """
+    offsets = kwargs['offsets']
+    longest = kwargs.get('longest', False)
+    fillvalue = kwargs.get('fillvalue', None)
+
+    if len(iterables) != len(offsets):
+        raise ValueError("Number of iterables and offsets didn't match")
+
+    staggered = []
+    for it, n in zip(iterables, offsets):
+        if n < 0:
+            staggered.append(chain(repeat(fillvalue, -n), it))
+        elif n > 0:
+            staggered.append(islice(it, n, None))
+        else:
+            staggered.append(it)
+
+    if longest:
+        return zip_longest(*staggered, fillvalue=fillvalue)
+
+    return zip(*staggered)
+
+
+def sort_together(iterables, key_list=(0,), reverse=False):
+    """Return the input iterables sorted together, with *key_list* as the
+    priority for sorting. All iterables are trimmed to the length of the
+    shortest one.
+
+    This can be used like the sorting function in a spreadsheet. If each
+    iterable represents a column of data, the key list determines which
+    columns are used for sorting.
+
+    By default, all iterables are sorted using the ``0``-th iterable::
+
+        >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')]
+        >>> sort_together(iterables)
+        [(1, 2, 3, 4), ('d', 'c', 'b', 'a')]
+
+    Set a different key list to sort according to another iterable.
+    Specifying mutliple keys dictates how ties are broken::
+
+        >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')]
+        >>> sort_together(iterables, key_list=(1, 2))
+        [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')]
+
+    Set *reverse* to ``True`` to sort in descending order.
+
+        >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True)
+        [(3, 2, 1), ('a', 'b', 'c')]
+
+    """
+    return list(zip(*sorted(zip(*iterables),
+                            key=itemgetter(*key_list),
+                            reverse=reverse)))
+
+
+def divide(n, iterable):
+    """Divide the elements from *iterable* into *n* parts, maintaining
+    order.
+
+        >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6])
+        >>> list(group_1)
+        [1, 2, 3]
+        >>> list(group_2)
+        [4, 5, 6]
+
+    If the length of *iterable* is not evenly divisible by *n*, then the
+    length of the returned iterables will not be identical:
+
+        >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7])
+        >>> [list(c) for c in children]
+        [[1, 2, 3], [4, 5], [6, 7]]
+
+    If the length of the iterable is smaller than n, then the last returned
+    iterables will be empty:
+
+        >>> children = divide(5, [1, 2, 3])
+        >>> [list(c) for c in children]
+        [[1], [2], [3], [], []]
+
+    This function will exhaust the iterable before returning and may require
+    significant storage. If order is not important, see :func:`distribute`,
+    which does not first pull the iterable into memory.
+
+    """
+    if n < 1:
+        raise ValueError('n must be at least 1')
+
+    seq = tuple(iterable)
+    q, r = divmod(len(seq), n)
+
+    ret = []
+    for i in range(n):
+        start = (i * q) + (i if i < r else r)
+        stop = ((i + 1) * q) + (i + 1 if i + 1 < r else r)
+        ret.append(iter(seq[start:stop]))
+
+    return ret
+
+
+def always_iterable(obj, base_type=(text_type, binary_type)):
+    """If *obj* is iterable, return an iterator over its items::
+
+        >>> obj = (1, 2, 3)
+        >>> list(always_iterable(obj))
+        [1, 2, 3]
+
+    If *obj* is not iterable, return a one-item iterable containing *obj*::
+
+        >>> obj = 1
+        >>> list(always_iterable(obj))
+        [1]
+
+    If *obj* is ``None``, return an empty iterable:
+
+        >>> obj = None
+        >>> list(always_iterable(None))
+        []
+
+    By default, binary and text strings are not considered iterable::
+
+        >>> obj = 'foo'
+        >>> list(always_iterable(obj))
+        ['foo']
+
+    If *base_type* is set, objects for which ``isinstance(obj, base_type)``
+    returns ``True`` won't be considered iterable.
+
+        >>> obj = {'a': 1}
+        >>> list(always_iterable(obj))  # Iterate over the dict's keys
+        ['a']
+        >>> list(always_iterable(obj, base_type=dict))  # Treat dicts as a unit
+        [{'a': 1}]
+
+    Set *base_type* to ``None`` to avoid any special handling and treat objects
+    Python considers iterable as iterable:
+
+        >>> obj = 'foo'
+        >>> list(always_iterable(obj, base_type=None))
+        ['f', 'o', 'o']
+    """
+    if obj is None:
+        return iter(())
+
+    if (base_type is not None) and isinstance(obj, base_type):
+        return iter((obj,))
+
+    try:
+        return iter(obj)
+    except TypeError:
+        return iter((obj,))
+
+
+def adjacent(predicate, iterable, distance=1):
+    """Return an iterable over `(bool, item)` tuples where the `item` is
+    drawn from *iterable* and the `bool` indicates whether
+    that item satisfies the *predicate* or is adjacent to an item that does.
+
+    For example, to find whether items are adjacent to a ``3``::
+
+        >>> list(adjacent(lambda x: x == 3, range(6)))
+        [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)]
+
+    Set *distance* to change what counts as adjacent. For example, to find
+    whether items are two places away from a ``3``:
+
+        >>> list(adjacent(lambda x: x == 3, range(6), distance=2))
+        [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)]
+
+    This is useful for contextualizing the results of a search function.
+    For example, a code comparison tool might want to identify lines that
+    have changed, but also surrounding lines to give the viewer of the diff
+    context.
+
+    The predicate function will only be called once for each item in the
+    iterable.
+
+    See also :func:`groupby_transform`, which can be used with this function
+    to group ranges of items with the same `bool` value.
+
+    """
+    # Allow distance=0 mainly for testing that it reproduces results with map()
+    if distance < 0:
+        raise ValueError('distance must be at least 0')
+
+    i1, i2 = tee(iterable)
+    padding = [False] * distance
+    selected = chain(padding, map(predicate, i1), padding)
+    adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1))
+    return zip(adjacent_to_selected, i2)
+
+
+def groupby_transform(iterable, keyfunc=None, valuefunc=None):
+    """An extension of :func:`itertools.groupby` that transforms the values of
+    *iterable* after grouping them.
+    *keyfunc* is a function used to compute a grouping key for each item.
+    *valuefunc* is a function for transforming the items after grouping.
+
+        >>> iterable = 'AaaABbBCcA'
+        >>> keyfunc = lambda x: x.upper()
+        >>> valuefunc = lambda x: x.lower()
+        >>> grouper = groupby_transform(iterable, keyfunc, valuefunc)
+        >>> [(k, ''.join(g)) for k, g in grouper]
+        [('A', 'aaaa'), ('B', 'bbb'), ('C', 'cc'), ('A', 'a')]
+
+    *keyfunc* and *valuefunc* default to identity functions if they are not
+    specified.
+
+    :func:`groupby_transform` is useful when grouping elements of an iterable
+    using a separate iterable as the key. To do this, :func:`zip` the iterables
+    and pass a *keyfunc* that extracts the first element and a *valuefunc*
+    that extracts the second element::
+
+        >>> from operator import itemgetter
+        >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3]
+        >>> values = 'abcdefghi'
+        >>> iterable = zip(keys, values)
+        >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1))
+        >>> [(k, ''.join(g)) for k, g in grouper]
+        [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')]
+
+    Note that the order of items in the iterable is significant.
+    Only adjacent items are grouped together, so if you don't want any
+    duplicate groups, you should sort the iterable by the key function.
+
+    """
+    valuefunc = (lambda x: x) if valuefunc is None else valuefunc
+    return ((k, map(valuefunc, g)) for k, g in groupby(iterable, keyfunc))
+
+
+def numeric_range(*args):
+    """An extension of the built-in ``range()`` function whose arguments can
+    be any orderable numeric type.
+
+    With only *stop* specified, *start* defaults to ``0`` and *step*
+    defaults to ``1``. The output items will match the type of *stop*:
+
+        >>> list(numeric_range(3.5))
+        [0.0, 1.0, 2.0, 3.0]
+
+    With only *start* and *stop* specified, *step* defaults to ``1``. The
+    output items will match the type of *start*:
+
+        >>> from decimal import Decimal
+        >>> start = Decimal('2.1')
+        >>> stop = Decimal('5.1')
+        >>> list(numeric_range(start, stop))
+        [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')]
+
+    With *start*, *stop*, and *step*  specified the output items will match
+    the type of ``start + step``:
+
+        >>> from fractions import Fraction
+        >>> start = Fraction(1, 2)  # Start at 1/2
+        >>> stop = Fraction(5, 2)  # End at 5/2
+        >>> step = Fraction(1, 2)  # Count by 1/2
+        >>> list(numeric_range(start, stop, step))
+        [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)]
+
+    If *step* is zero, ``ValueError`` is raised. Negative steps are supported:
+
+        >>> list(numeric_range(3, -1, -1.0))
+        [3.0, 2.0, 1.0, 0.0]
+
+    Be aware of the limitations of floating point numbers; the representation
+    of the yielded numbers may be surprising.
+
+    """
+    argc = len(args)
+    if argc == 1:
+        stop, = args
+        start = type(stop)(0)
+        step = 1
+    elif argc == 2:
+        start, stop = args
+        step = 1
+    elif argc == 3:
+        start, stop, step = args
+    else:
+        err_msg = 'numeric_range takes at most 3 arguments, got {}'
+        raise TypeError(err_msg.format(argc))
+
+    values = (start + (step * n) for n in count())
+    if step > 0:
+        return takewhile(partial(gt, stop), values)
+    elif step < 0:
+        return takewhile(partial(lt, stop), values)
+    else:
+        raise ValueError('numeric_range arg 3 must not be zero')
+
+
+def count_cycle(iterable, n=None):
+    """Cycle through the items from *iterable* up to *n* times, yielding
+    the number of completed cycles along with each item. If *n* is omitted the
+    process repeats indefinitely.
+
+    >>> list(count_cycle('AB', 3))
+    [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')]
+
+    """
+    iterable = tuple(iterable)
+    if not iterable:
+        return iter(())
+    counter = count() if n is None else range(n)
+    return ((i, item) for i in counter for item in iterable)
+
+
+def locate(iterable, pred=bool, window_size=None):
+    """Yield the index of each item in *iterable* for which *pred* returns
+    ``True``.
+
+    *pred* defaults to :func:`bool`, which will select truthy items:
+
+        >>> list(locate([0, 1, 1, 0, 1, 0, 0]))
+        [1, 2, 4]
+
+    Set *pred* to a custom function to, e.g., find the indexes for a particular
+    item.
+
+        >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b'))
+        [1, 3]
+
+    If *window_size* is given, then the *pred* function will be called with
+    that many items. This enables searching for sub-sequences:
+
+        >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
+        >>> pred = lambda *args: args == (1, 2, 3)
+        >>> list(locate(iterable, pred=pred, window_size=3))
+        [1, 5, 9]
+
+    Use with :func:`seekable` to find indexes and then retrieve the associated
+    items:
+
+        >>> from itertools import count
+        >>> from more_itertools import seekable
+        >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count())
+        >>> it = seekable(source)
+        >>> pred = lambda x: x > 100
+        >>> indexes = locate(it, pred=pred)
+        >>> i = next(indexes)
+        >>> it.seek(i)
+        >>> next(it)
+        106
+
+    """
+    if window_size is None:
+        return compress(count(), map(pred, iterable))
+
+    if window_size < 1:
+        raise ValueError('window size must be at least 1')
+
+    it = windowed(iterable, window_size, fillvalue=_marker)
+    return compress(count(), starmap(pred, it))
+
+
+def lstrip(iterable, pred):
+    """Yield the items from *iterable*, but strip any from the beginning
+    for which *pred* returns ``True``.
+
+    For example, to remove a set of items from the start of an iterable:
+
+        >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
+        >>> pred = lambda x: x in {None, False, ''}
+        >>> list(lstrip(iterable, pred))
+        [1, 2, None, 3, False, None]
+
+    This function is analogous to to :func:`str.lstrip`, and is essentially
+    an wrapper for :func:`itertools.dropwhile`.
+
+    """
+    return dropwhile(pred, iterable)
+
+
+def rstrip(iterable, pred):
+    """Yield the items from *iterable*, but strip any from the end
+    for which *pred* returns ``True``.
+
+    For example, to remove a set of items from the end of an iterable:
+
+        >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
+        >>> pred = lambda x: x in {None, False, ''}
+        >>> list(rstrip(iterable, pred))
+        [None, False, None, 1, 2, None, 3]
+
+    This function is analogous to :func:`str.rstrip`.
+
+    """
+    cache = []
+    cache_append = cache.append
+    for x in iterable:
+        if pred(x):
+            cache_append(x)
+        else:
+            for y in cache:
+                yield y
+            del cache[:]
+            yield x
+
+
+def strip(iterable, pred):
+    """Yield the items from *iterable*, but strip any from the
+    beginning and end for which *pred* returns ``True``.
+
+    For example, to remove a set of items from both ends of an iterable:
+
+        >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
+        >>> pred = lambda x: x in {None, False, ''}
+        >>> list(strip(iterable, pred))
+        [1, 2, None, 3]
+
+    This function is analogous to :func:`str.strip`.
+
+    """
+    return rstrip(lstrip(iterable, pred), pred)
+
+
+def islice_extended(iterable, *args):
+    """An extension of :func:`itertools.islice` that supports negative values
+    for *stop*, *start*, and *step*.
+
+        >>> iterable = iter('abcdefgh')
+        >>> list(islice_extended(iterable, -4, -1))
+        ['e', 'f', 'g']
+
+    Slices with negative values require some caching of *iterable*, but this
+    function takes care to minimize the amount of memory required.
+
+    For example, you can use a negative step with an infinite iterator:
+
+        >>> from itertools import count
+        >>> list(islice_extended(count(), 110, 99, -2))
+        [110, 108, 106, 104, 102, 100]
+
+    """
+    s = slice(*args)
+    start = s.start
+    stop = s.stop
+    if s.step == 0:
+        raise ValueError('step argument must be a non-zero integer or None.')
+    step = s.step or 1
+
+    it = iter(iterable)
+
+    if step > 0:
+        start = 0 if (start is None) else start
+
+        if (start < 0):
+            # Consume all but the last -start items
+            cache = deque(enumerate(it, 1), maxlen=-start)
+            len_iter = cache[-1][0] if cache else 0
+
+            # Adjust start to be positive
+            i = max(len_iter + start, 0)
+
+            # Adjust stop to be positive
+            if stop is None:
+                j = len_iter
+            elif stop >= 0:
+                j = min(stop, len_iter)
+            else:
+                j = max(len_iter + stop, 0)
+
+            # Slice the cache
+            n = j - i
+            if n <= 0:
+                return
+
+            for index, item in islice(cache, 0, n, step):
+                yield item
+        elif (stop is not None) and (stop < 0):
+            # Advance to the start position
+            next(islice(it, start, start), None)
+
+            # When stop is negative, we have to carry -stop items while
+            # iterating
+            cache = deque(islice(it, -stop), maxlen=-stop)
+
+            for index, item in enumerate(it):
+                cached_item = cache.popleft()
+                if index % step == 0:
+                    yield cached_item
+                cache.append(item)
+        else:
+            # When both start and stop are positive we have the normal case
+            for item in islice(it, start, stop, step):
+                yield item
+    else:
+        start = -1 if (start is None) else start
+
+        if (stop is not None) and (stop < 0):
+            # Consume all but the last items
+            n = -stop - 1
+            cache = deque(enumerate(it, 1), maxlen=n)
+            len_iter = cache[-1][0] if cache else 0
+
+            # If start and stop are both negative they are comparable and
+            # we can just slice. Otherwise we can adjust start to be negative
+            # and then slice.
+            if start < 0:
+                i, j = start, stop
+            else:
+                i, j = min(start - len_iter, -1), None
+
+            for index, item in list(cache)[i:j:step]:
+                yield item
+        else:
+            # Advance to the stop position
+            if stop is not None:
+                m = stop + 1
+                next(islice(it, m, m), None)
+
+            # stop is positive, so if start is negative they are not comparable
+            # and we need the rest of the items.
+            if start < 0:
+                i = start
+                n = None
+            # stop is None and start is positive, so we just need items up to
+            # the start index.
+            elif stop is None:
+                i = None
+                n = start + 1
+            # Both stop and start are positive, so they are comparable.
+            else:
+                i = None
+                n = start - stop
+                if n <= 0:
+                    return
+
+            cache = list(islice(it, n))
+
+            for item in cache[i::step]:
+                yield item
+
+
+def always_reversible(iterable):
+    """An extension of :func:`reversed` that supports all iterables, not
+    just those which implement the ``Reversible`` or ``Sequence`` protocols.
+
+        >>> print(*always_reversible(x for x in range(3)))
+        2 1 0
+
+    If the iterable is already reversible, this function returns the
+    result of :func:`reversed()`. If the iterable is not reversible,
+    this function will cache the remaining items in the iterable and
+    yield them in reverse order, which may require significant storage.
+    """
+    try:
+        return reversed(iterable)
+    except TypeError:
+        return reversed(list(iterable))
+
+
+def consecutive_groups(iterable, ordering=lambda x: x):
+    """Yield groups of consecutive items using :func:`itertools.groupby`.
+    The *ordering* function determines whether two items are adjacent by
+    returning their position.
+
+    By default, the ordering function is the identity function. This is
+    suitable for finding runs of numbers:
+
+        >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40]
+        >>> for group in consecutive_groups(iterable):
+        ...     print(list(group))
+        [1]
+        [10, 11, 12]
+        [20]
+        [30, 31, 32, 33]
+        [40]
+
+    For finding runs of adjacent letters, try using the :meth:`index` method
+    of a string of letters:
+
+        >>> from string import ascii_lowercase
+        >>> iterable = 'abcdfgilmnop'
+        >>> ordering = ascii_lowercase.index
+        >>> for group in consecutive_groups(iterable, ordering):
+        ...     print(list(group))
+        ['a', 'b', 'c', 'd']
+        ['f', 'g']
+        ['i']
+        ['l', 'm', 'n', 'o', 'p']
+
+    """
+    for k, g in groupby(
+        enumerate(iterable), key=lambda x: x[0] - ordering(x[1])
+    ):
+        yield map(itemgetter(1), g)
+
+
+def difference(iterable, func=sub):
+    """By default, compute the first difference of *iterable* using
+    :func:`operator.sub`.
+
+        >>> iterable = [0, 1, 3, 6, 10]
+        >>> list(difference(iterable))
+        [0, 1, 2, 3, 4]
+
+    This is the opposite of :func:`accumulate`'s default behavior:
+
+        >>> from more_itertools import accumulate
+        >>> iterable = [0, 1, 2, 3, 4]
+        >>> list(accumulate(iterable))
+        [0, 1, 3, 6, 10]
+        >>> list(difference(accumulate(iterable)))
+        [0, 1, 2, 3, 4]
+
+    By default *func* is :func:`operator.sub`, but other functions can be
+    specified. They will be applied as follows::
+
+        A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ...
+
+    For example, to do progressive division:
+
+        >>> iterable = [1, 2, 6, 24, 120]  # Factorial sequence
+        >>> func = lambda x, y: x // y
+        >>> list(difference(iterable, func))
+        [1, 2, 3, 4, 5]
+
+    """
+    a, b = tee(iterable)
+    try:
+        item = next(b)
+    except StopIteration:
+        return iter([])
+    return chain([item], map(lambda x: func(x[1], x[0]), zip(a, b)))
+
+
+class SequenceView(Sequence):
+    """Return a read-only view of the sequence object *target*.
+
+    :class:`SequenceView` objects are analagous to Python's built-in
+    "dictionary view" types. They provide a dynamic view of a sequence's items,
+    meaning that when the sequence updates, so does the view.
+
+        >>> seq = ['0', '1', '2']
+        >>> view = SequenceView(seq)
+        >>> view
+        SequenceView(['0', '1', '2'])
+        >>> seq.append('3')
+        >>> view
+        SequenceView(['0', '1', '2', '3'])
+
+    Sequence views support indexing, slicing, and length queries. They act
+    like the underlying sequence, except they don't allow assignment:
+
+        >>> view[1]
+        '1'
+        >>> view[1:-1]
+        ['1', '2']
+        >>> len(view)
+        4
+
+    Sequence views are useful as an alternative to copying, as they don't
+    require (much) extra storage.
+
+    """
+    def __init__(self, target):
+        if not isinstance(target, Sequence):
+            raise TypeError
+        self._target = target
+
+    def __getitem__(self, index):
+        return self._target[index]
+
+    def __len__(self):
+        return len(self._target)
+
+    def __repr__(self):
+        return '{}({})'.format(self.__class__.__name__, repr(self._target))
+
+
+class seekable(object):
+    """Wrap an iterator to allow for seeking backward and forward. This
+    progressively caches the items in the source iterable so they can be
+    re-visited.
+
+    Call :meth:`seek` with an index to seek to that position in the source
+    iterable.
+
+    To "reset" an iterator, seek to ``0``:
+
+        >>> from itertools import count
+        >>> it = seekable((str(n) for n in count()))
+        >>> next(it), next(it), next(it)
+        ('0', '1', '2')
+        >>> it.seek(0)
+        >>> next(it), next(it), next(it)
+        ('0', '1', '2')
+        >>> next(it)
+        '3'
+
+    You can also seek forward:
+
+        >>> it = seekable((str(n) for n in range(20)))
+        >>> it.seek(10)
+        >>> next(it)
+        '10'
+        >>> it.seek(20)  # Seeking past the end of the source isn't a problem
+        >>> list(it)
+        []
+        >>> it.seek(0)  # Resetting works even after hitting the end
+        >>> next(it), next(it), next(it)
+        ('0', '1', '2')
+
+    The cache grows as the source iterable progresses, so beware of wrapping
+    very large or infinite iterables.
+
+    You may view the contents of the cache with the :meth:`elements` method.
+    That returns a :class:`SequenceView`, a view that updates automatically:
+
+        >>> it = seekable((str(n) for n in range(10)))
+        >>> next(it), next(it), next(it)
+        ('0', '1', '2')
+        >>> elements = it.elements()
+        >>> elements
+        SequenceView(['0', '1', '2'])
+        >>> next(it)
+        '3'
+        >>> elements
+        SequenceView(['0', '1', '2', '3'])
+
+    """
+
+    def __init__(self, iterable):
+        self._source = iter(iterable)
+        self._cache = []
+        self._index = None
+
+    def __iter__(self):
+        return self
+
+    def __next__(self):
+        if self._index is not None:
+            try:
+                item = self._cache[self._index]
+            except IndexError:
+                self._index = None
+            else:
+                self._index += 1
+                return item
+
+        item = next(self._source)
+        self._cache.append(item)
+        return item
+
+    next = __next__
+
+    def elements(self):
+        return SequenceView(self._cache)
+
+    def seek(self, index):
+        self._index = index
+        remainder = index - len(self._cache)
+        if remainder > 0:
+            consume(self, remainder)
+
+
+class run_length(object):
+    """
+    :func:`run_length.encode` compresses an iterable with run-length encoding.
+    It yields groups of repeated items with the count of how many times they
+    were repeated:
+
+        >>> uncompressed = 'abbcccdddd'
+        >>> list(run_length.encode(uncompressed))
+        [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
+
+    :func:`run_length.decode` decompresses an iterable that was previously
+    compressed with run-length encoding. It yields the items of the
+    decompressed iterable:
+
+        >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
+        >>> list(run_length.decode(compressed))
+        ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd']
+
+    """
+
+    @staticmethod
+    def encode(iterable):
+        return ((k, ilen(g)) for k, g in groupby(iterable))
+
+    @staticmethod
+    def decode(iterable):
+        return chain.from_iterable(repeat(k, n) for k, n in iterable)
+
+
+def exactly_n(iterable, n, predicate=bool):
+    """Return ``True`` if exactly ``n`` items in the iterable are ``True``
+    according to the *predicate* function.
+
+        >>> exactly_n([True, True, False], 2)
+        True
+        >>> exactly_n([True, True, False], 1)
+        False
+        >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3)
+        True
+
+    The iterable will be advanced until ``n + 1`` truthy items are encountered,
+    so avoid calling it on infinite iterables.
+
+    """
+    return len(take(n + 1, filter(predicate, iterable))) == n
+
+
+def circular_shifts(iterable):
+    """Return a list of circular shifts of *iterable*.
+
+        >>> circular_shifts(range(4))
+        [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)]
+    """
+    lst = list(iterable)
+    return take(len(lst), windowed(cycle(lst), len(lst)))
+
+
+def make_decorator(wrapping_func, result_index=0):
+    """Return a decorator version of *wrapping_func*, which is a function that
+    modifies an iterable. *result_index* is the position in that function's
+    signature where the iterable goes.
+
+    This lets you use itertools on the "production end," i.e. at function
+    definition. This can augment what the function returns without changing the
+    function's code.
+
+    For example, to produce a decorator version of :func:`chunked`:
+
+        >>> from more_itertools import chunked
+        >>> chunker = make_decorator(chunked, result_index=0)
+        >>> @chunker(3)
+        ... def iter_range(n):
+        ...     return iter(range(n))
+        ...
+        >>> list(iter_range(9))
+        [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
+
+    To only allow truthy items to be returned:
+
+        >>> truth_serum = make_decorator(filter, result_index=1)
+        >>> @truth_serum(bool)
+        ... def boolean_test():
+        ...     return [0, 1, '', ' ', False, True]
+        ...
+        >>> list(boolean_test())
+        [1, ' ', True]
+
+    The :func:`peekable` and :func:`seekable` wrappers make for practical
+    decorators:
+
+        >>> from more_itertools import peekable
+        >>> peekable_function = make_decorator(peekable)
+        >>> @peekable_function()
+        ... def str_range(*args):
+        ...     return (str(x) for x in range(*args))
+        ...
+        >>> it = str_range(1, 20, 2)
+        >>> next(it), next(it), next(it)
+        ('1', '3', '5')
+        >>> it.peek()
+        '7'
+        >>> next(it)
+        '7'
+
+    """
+    # See https://sites.google.com/site/bbayles/index/decorator_factory for
+    # notes on how this works.
+    def decorator(*wrapping_args, **wrapping_kwargs):
+        def outer_wrapper(f):
+            def inner_wrapper(*args, **kwargs):
+                result = f(*args, **kwargs)
+                wrapping_args_ = list(wrapping_args)
+                wrapping_args_.insert(result_index, result)
+                return wrapping_func(*wrapping_args_, **wrapping_kwargs)
+
+            return inner_wrapper
+
+        return outer_wrapper
+
+    return decorator
+
+
+def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None):
+    """Return a dictionary that maps the items in *iterable* to categories
+    defined by *keyfunc*, transforms them with *valuefunc*, and
+    then summarizes them by category with *reducefunc*.
+
+    *valuefunc* defaults to the identity function if it is unspecified.
+    If *reducefunc* is unspecified, no summarization takes place:
+
+        >>> keyfunc = lambda x: x.upper()
+        >>> result = map_reduce('abbccc', keyfunc)
+        >>> sorted(result.items())
+        [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])]
+
+    Specifying *valuefunc* transforms the categorized items:
+
+        >>> keyfunc = lambda x: x.upper()
+        >>> valuefunc = lambda x: 1
+        >>> result = map_reduce('abbccc', keyfunc, valuefunc)
+        >>> sorted(result.items())
+        [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])]
+
+    Specifying *reducefunc* summarizes the categorized items:
+
+        >>> keyfunc = lambda x: x.upper()
+        >>> valuefunc = lambda x: 1
+        >>> reducefunc = sum
+        >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc)
+        >>> sorted(result.items())
+        [('A', 1), ('B', 2), ('C', 3)]
+
+    You may want to filter the input iterable before applying the map/reduce
+    procedure:
+
+        >>> all_items = range(30)
+        >>> items = [x for x in all_items if 10 <= x <= 20]  # Filter
+        >>> keyfunc = lambda x: x % 2  # Evens map to 0; odds to 1
+        >>> categories = map_reduce(items, keyfunc=keyfunc)
+        >>> sorted(categories.items())
+        [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])]
+        >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum)
+        >>> sorted(summaries.items())
+        [(0, 90), (1, 75)]
+
+    Note that all items in the iterable are gathered into a list before the
+    summarization step, which may require significant storage.
+
+    The returned object is a :obj:`collections.defaultdict` with the
+    ``default_factory`` set to ``None``, such that it behaves like a normal
+    dictionary.
+
+    """
+    valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc
+
+    ret = defaultdict(list)
+    for item in iterable:
+        key = keyfunc(item)
+        value = valuefunc(item)
+        ret[key].append(value)
+
+    if reducefunc is not None:
+        for key, value_list in ret.items():
+            ret[key] = reducefunc(value_list)
+
+    ret.default_factory = None
+    return ret
+
+
+def rlocate(iterable, pred=bool, window_size=None):
+    """Yield the index of each item in *iterable* for which *pred* returns
+    ``True``, starting from the right and moving left.
+
+    *pred* defaults to :func:`bool`, which will select truthy items:
+
+        >>> list(rlocate([0, 1, 1, 0, 1, 0, 0]))  # Truthy at 1, 2, and 4
+        [4, 2, 1]
+
+    Set *pred* to a custom function to, e.g., find the indexes for a particular
+    item:
+
+        >>> iterable = iter('abcb')
+        >>> pred = lambda x: x == 'b'
+        >>> list(rlocate(iterable, pred))
+        [3, 1]
+
+    If *window_size* is given, then the *pred* function will be called with
+    that many items. This enables searching for sub-sequences:
+
+        >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
+        >>> pred = lambda *args: args == (1, 2, 3)
+        >>> list(rlocate(iterable, pred=pred, window_size=3))
+        [9, 5, 1]
+
+    Beware, this function won't return anything for infinite iterables.
+    If *iterable* is reversible, ``rlocate`` will reverse it and search from
+    the right. Otherwise, it will search from the left and return the results
+    in reverse order.
+
+    See :func:`locate` to for other example applications.
+
+    """
+    if window_size is None:
+        try:
+            len_iter = len(iterable)
+            return (
+                len_iter - i - 1 for i in locate(reversed(iterable), pred)
+            )
+        except TypeError:
+            pass
+
+    return reversed(list(locate(iterable, pred, window_size)))
+
+
+def replace(iterable, pred, substitutes, count=None, window_size=1):
+    """Yield the items from *iterable*, replacing the items for which *pred*
+    returns ``True`` with the items from the iterable *substitutes*.
+
+        >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1]
+        >>> pred = lambda x: x == 0
+        >>> substitutes = (2, 3)
+        >>> list(replace(iterable, pred, substitutes))
+        [1, 1, 2, 3, 1, 1, 2, 3, 1, 1]
+
+    If *count* is given, the number of replacements will be limited:
+
+        >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0]
+        >>> pred = lambda x: x == 0
+        >>> substitutes = [None]
+        >>> list(replace(iterable, pred, substitutes, count=2))
+        [1, 1, None, 1, 1, None, 1, 1, 0]
+
+    Use *window_size* to control the number of items passed as arguments to
+    *pred*. This allows for locating and replacing subsequences.
+
+        >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5]
+        >>> window_size = 3
+        >>> pred = lambda *args: args == (0, 1, 2)  # 3 items passed to pred
+        >>> substitutes = [3, 4] # Splice in these items
+        >>> list(replace(iterable, pred, substitutes, window_size=window_size))
+        [3, 4, 5, 3, 4, 5]
+
+    """
+    if window_size < 1:
+        raise ValueError('window_size must be at least 1')
+
+    # Save the substitutes iterable, since it's used more than once
+    substitutes = tuple(substitutes)
+
+    # Add padding such that the number of windows matches the length of the
+    # iterable
+    it = chain(iterable, [_marker] * (window_size - 1))
+    windows = windowed(it, window_size)
+
+    n = 0
+    for w in windows:
+        # If the current window matches our predicate (and we haven't hit
+        # our maximum number of replacements), splice in the substitutes
+        # and then consume the following windows that overlap with this one.
+        # For example, if the iterable is (0, 1, 2, 3, 4...)
+        # and the window size is 2, we have (0, 1), (1, 2), (2, 3)...
+        # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2)
+        if pred(*w):
+            if (count is None) or (n < count):
+                n += 1
+                for s in substitutes:
+                    yield s
+                consume(windows, window_size - 1)
+                continue
+
+        # If there was no match (or we've reached the replacement limit),
+        # yield the first item from the window.
+        if w and (w[0] is not _marker):
+            yield w[0]
diff --git a/libraries/more_itertools/recipes.py b/libraries/more_itertools/recipes.py
new file mode 100644
index 00000000..3a7706cb
--- /dev/null
+++ b/libraries/more_itertools/recipes.py
@@ -0,0 +1,565 @@
+"""Imported from the recipes section of the itertools documentation.
+
+All functions taken from the recipes section of the itertools library docs
+[1]_.
+Some backward-compatible usability improvements have been made.
+
+.. [1] http://docs.python.org/library/itertools.html#recipes
+
+"""
+from collections import deque
+from itertools import (
+    chain, combinations, count, cycle, groupby, islice, repeat, starmap, tee
+)
+import operator
+from random import randrange, sample, choice
+
+from six import PY2
+from six.moves import filter, filterfalse, map, range, zip, zip_longest
+
+__all__ = [
+    'accumulate',
+    'all_equal',
+    'consume',
+    'dotproduct',
+    'first_true',
+    'flatten',
+    'grouper',
+    'iter_except',
+    'ncycles',
+    'nth',
+    'nth_combination',
+    'padnone',
+    'pairwise',
+    'partition',
+    'powerset',
+    'prepend',
+    'quantify',
+    'random_combination_with_replacement',
+    'random_combination',
+    'random_permutation',
+    'random_product',
+    'repeatfunc',
+    'roundrobin',
+    'tabulate',
+    'tail',
+    'take',
+    'unique_everseen',
+    'unique_justseen',
+]
+
+
+def accumulate(iterable, func=operator.add):
+    """
+    Return an iterator whose items are the accumulated results of a function
+    (specified by the optional *func* argument) that takes two arguments.
+    By default, returns accumulated sums with :func:`operator.add`.
+
+        >>> list(accumulate([1, 2, 3, 4, 5]))  # Running sum
+        [1, 3, 6, 10, 15]
+        >>> list(accumulate([1, 2, 3], func=operator.mul))  # Running product
+        [1, 2, 6]
+        >>> list(accumulate([0, 1, -1, 2, 3, 2], func=max))  # Running maximum
+        [0, 1, 1, 2, 3, 3]
+
+    This function is available in the ``itertools`` module for Python 3.2 and
+    greater.
+
+    """
+    it = iter(iterable)
+    try:
+        total = next(it)
+    except StopIteration:
+        return
+    else:
+        yield total
+
+    for element in it:
+        total = func(total, element)
+        yield total
+
+
+def take(n, iterable):
+    """Return first *n* items of the iterable as a list.
+
+        >>> take(3, range(10))
+        [0, 1, 2]
+        >>> take(5, range(3))
+        [0, 1, 2]
+
+    Effectively a short replacement for ``next`` based iterator consumption
+    when you want more than one item, but less than the whole iterator.
+
+    """
+    return list(islice(iterable, n))
+
+
+def tabulate(function, start=0):
+    """Return an iterator over the results of ``func(start)``,
+    ``func(start + 1)``, ``func(start + 2)``...
+
+    *func* should be a function that accepts one integer argument.
+
+    If *start* is not specified it defaults to 0. It will be incremented each
+    time the iterator is advanced.
+
+        >>> square = lambda x: x ** 2
+        >>> iterator = tabulate(square, -3)
+        >>> take(4, iterator)
+        [9, 4, 1, 0]
+
+    """
+    return map(function, count(start))
+
+
+def tail(n, iterable):
+    """Return an iterator over the last *n* items of *iterable*.
+
+        >>> t = tail(3, 'ABCDEFG')
+        >>> list(t)
+        ['E', 'F', 'G']
+
+    """
+    return iter(deque(iterable, maxlen=n))
+
+
+def consume(iterator, n=None):
+    """Advance *iterable* by *n* steps. If *n* is ``None``, consume it
+    entirely.
+
+    Efficiently exhausts an iterator without returning values. Defaults to
+    consuming the whole iterator, but an optional second argument may be
+    provided to limit consumption.
+
+        >>> i = (x for x in range(10))
+        >>> next(i)
+        0
+        >>> consume(i, 3)
+        >>> next(i)
+        4
+        >>> consume(i)
+        >>> next(i)
+        Traceback (most recent call last):
+          File "<stdin>", line 1, in <module>
+        StopIteration
+
+    If the iterator has fewer items remaining than the provided limit, the
+    whole iterator will be consumed.
+
+        >>> i = (x for x in range(3))
+        >>> consume(i, 5)
+        >>> next(i)
+        Traceback (most recent call last):
+          File "<stdin>", line 1, in <module>
+        StopIteration
+
+    """
+    # Use functions that consume iterators at C speed.
+    if n is None:
+        # feed the entire iterator into a zero-length deque
+        deque(iterator, maxlen=0)
+    else:
+        # advance to the empty slice starting at position n
+        next(islice(iterator, n, n), None)
+
+
+def nth(iterable, n, default=None):
+    """Returns the nth item or a default value.
+
+        >>> l = range(10)
+        >>> nth(l, 3)
+        3
+        >>> nth(l, 20, "zebra")
+        'zebra'
+
+    """
+    return next(islice(iterable, n, None), default)
+
+
+def all_equal(iterable):
+    """
+    Returns ``True`` if all the elements are equal to each other.
+
+        >>> all_equal('aaaa')
+        True
+        >>> all_equal('aaab')
+        False
+
+    """
+    g = groupby(iterable)
+    return next(g, True) and not next(g, False)
+
+
+def quantify(iterable, pred=bool):
+    """Return the how many times the predicate is true.
+
+        >>> quantify([True, False, True])
+        2
+
+    """
+    return sum(map(pred, iterable))
+
+
+def padnone(iterable):
+    """Returns the sequence of elements and then returns ``None`` indefinitely.
+
+        >>> take(5, padnone(range(3)))
+        [0, 1, 2, None, None]
+
+    Useful for emulating the behavior of the built-in :func:`map` function.
+
+    See also :func:`padded`.
+
+    """
+    return chain(iterable, repeat(None))
+
+
+def ncycles(iterable, n):
+    """Returns the sequence elements *n* times
+
+        >>> list(ncycles(["a", "b"], 3))
+        ['a', 'b', 'a', 'b', 'a', 'b']
+
+    """
+    return chain.from_iterable(repeat(tuple(iterable), n))
+
+
+def dotproduct(vec1, vec2):
+    """Returns the dot product of the two iterables.
+
+        >>> dotproduct([10, 10], [20, 20])
+        400
+
+    """
+    return sum(map(operator.mul, vec1, vec2))
+
+
+def flatten(listOfLists):
+    """Return an iterator flattening one level of nesting in a list of lists.
+
+        >>> list(flatten([[0, 1], [2, 3]]))
+        [0, 1, 2, 3]
+
+    See also :func:`collapse`, which can flatten multiple levels of nesting.
+
+    """
+    return chain.from_iterable(listOfLists)
+
+
+def repeatfunc(func, times=None, *args):
+    """Call *func* with *args* repeatedly, returning an iterable over the
+    results.
+
+    If *times* is specified, the iterable will terminate after that many
+    repetitions:
+
+        >>> from operator import add
+        >>> times = 4
+        >>> args = 3, 5
+        >>> list(repeatfunc(add, times, *args))
+        [8, 8, 8, 8]
+
+    If *times* is ``None`` the iterable will not terminate:
+
+        >>> from random import randrange
+        >>> times = None
+        >>> args = 1, 11
+        >>> take(6, repeatfunc(randrange, times, *args))  # doctest:+SKIP
+        [2, 4, 8, 1, 8, 4]
+
+    """
+    if times is None:
+        return starmap(func, repeat(args))
+    return starmap(func, repeat(args, times))
+
+
+def pairwise(iterable):
+    """Returns an iterator of paired items, overlapping, from the original
+
+        >>> take(4, pairwise(count()))
+        [(0, 1), (1, 2), (2, 3), (3, 4)]
+
+    """
+    a, b = tee(iterable)
+    next(b, None)
+    return zip(a, b)
+
+
+def grouper(n, iterable, fillvalue=None):
+    """Collect data into fixed-length chunks or blocks.
+
+        >>> list(grouper(3, 'ABCDEFG', 'x'))
+        [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')]
+
+    """
+    args = [iter(iterable)] * n
+    return zip_longest(fillvalue=fillvalue, *args)
+
+
+def roundrobin(*iterables):
+    """Yields an item from each iterable, alternating between them.
+
+        >>> list(roundrobin('ABC', 'D', 'EF'))
+        ['A', 'D', 'E', 'B', 'F', 'C']
+
+    This function produces the same output as :func:`interleave_longest`, but
+    may perform better for some inputs (in particular when the number of
+    iterables is small).
+
+    """
+    # Recipe credited to George Sakkis
+    pending = len(iterables)
+    if PY2:
+        nexts = cycle(iter(it).next for it in iterables)
+    else:
+        nexts = cycle(iter(it).__next__ for it in iterables)
+    while pending:
+        try:
+            for next in nexts:
+                yield next()
+        except StopIteration:
+            pending -= 1
+            nexts = cycle(islice(nexts, pending))
+
+
+def partition(pred, iterable):
+    """
+    Returns a 2-tuple of iterables derived from the input iterable.
+    The first yields the items that have ``pred(item) == False``.
+    The second yields the items that have ``pred(item) == True``.
+
+        >>> is_odd = lambda x: x % 2 != 0
+        >>> iterable = range(10)
+        >>> even_items, odd_items = partition(is_odd, iterable)
+        >>> list(even_items), list(odd_items)
+        ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
+
+    """
+    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9
+    t1, t2 = tee(iterable)
+    return filterfalse(pred, t1), filter(pred, t2)
+
+
+def powerset(iterable):
+    """Yields all possible subsets of the iterable.
+
+        >>> list(powerset([1,2,3]))
+        [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
+
+    """
+    s = list(iterable)
+    return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
+
+
+def unique_everseen(iterable, key=None):
+    """
+    Yield unique elements, preserving order.
+
+        >>> list(unique_everseen('AAAABBBCCDAABBB'))
+        ['A', 'B', 'C', 'D']
+        >>> list(unique_everseen('ABBCcAD', str.lower))
+        ['A', 'B', 'C', 'D']
+
+    Sequences with a mix of hashable and unhashable items can be used.
+    The function will be slower (i.e., `O(n^2)`) for unhashable items.
+
+    """
+    seenset = set()
+    seenset_add = seenset.add
+    seenlist = []
+    seenlist_add = seenlist.append
+    if key is None:
+        for element in iterable:
+            try:
+                if element not in seenset:
+                    seenset_add(element)
+                    yield element
+            except TypeError:
+                if element not in seenlist:
+                    seenlist_add(element)
+                    yield element
+    else:
+        for element in iterable:
+            k = key(element)
+            try:
+                if k not in seenset:
+                    seenset_add(k)
+                    yield element
+            except TypeError:
+                if k not in seenlist:
+                    seenlist_add(k)
+                    yield element
+
+
+def unique_justseen(iterable, key=None):
+    """Yields elements in order, ignoring serial duplicates
+
+        >>> list(unique_justseen('AAAABBBCCDAABBB'))
+        ['A', 'B', 'C', 'D', 'A', 'B']
+        >>> list(unique_justseen('ABBCcAD', str.lower))
+        ['A', 'B', 'C', 'A', 'D']
+
+    """
+    return map(next, map(operator.itemgetter(1), groupby(iterable, key)))
+
+
+def iter_except(func, exception, first=None):
+    """Yields results from a function repeatedly until an exception is raised.
+
+    Converts a call-until-exception interface to an iterator interface.
+    Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel
+    to end the loop.
+
+        >>> l = [0, 1, 2]
+        >>> list(iter_except(l.pop, IndexError))
+        [2, 1, 0]
+
+    """
+    try:
+        if first is not None:
+            yield first()
+        while 1:
+            yield func()
+    except exception:
+        pass
+
+
+def first_true(iterable, default=False, pred=None):
+    """
+    Returns the first true value in the iterable.
+
+    If no true value is found, returns *default*
+
+    If *pred* is not None, returns the first item for which
+    ``pred(item) == True`` .
+
+        >>> first_true(range(10))
+        1
+        >>> first_true(range(10), pred=lambda x: x > 5)
+        6
+        >>> first_true(range(10), default='missing', pred=lambda x: x > 9)
+        'missing'
+
+    """
+    return next(filter(pred, iterable), default)
+
+
+def random_product(*args, **kwds):
+    """Draw an item at random from each of the input iterables.
+
+        >>> random_product('abc', range(4), 'XYZ')  # doctest:+SKIP
+        ('c', 3, 'Z')
+
+    If *repeat* is provided as a keyword argument, that many items will be
+    drawn from each iterable.
+
+        >>> random_product('abcd', range(4), repeat=2)  # doctest:+SKIP
+        ('a', 2, 'd', 3)
+
+    This equivalent to taking a random selection from
+    ``itertools.product(*args, **kwarg)``.
+
+    """
+    pools = [tuple(pool) for pool in args] * kwds.get('repeat', 1)
+    return tuple(choice(pool) for pool in pools)
+
+
+def random_permutation(iterable, r=None):
+    """Return a random *r* length permutation of the elements in *iterable*.
+
+    If *r* is not specified or is ``None``, then *r* defaults to the length of
+    *iterable*.
+
+        >>> random_permutation(range(5))  # doctest:+SKIP
+        (3, 4, 0, 1, 2)
+
+    This equivalent to taking a random selection from
+    ``itertools.permutations(iterable, r)``.
+
+    """
+    pool = tuple(iterable)
+    r = len(pool) if r is None else r
+    return tuple(sample(pool, r))
+
+
+def random_combination(iterable, r):
+    """Return a random *r* length subsequence of the elements in *iterable*.
+
+        >>> random_combination(range(5), 3)  # doctest:+SKIP
+        (2, 3, 4)
+
+    This equivalent to taking a random selection from
+    ``itertools.combinations(iterable, r)``.
+
+    """
+    pool = tuple(iterable)
+    n = len(pool)
+    indices = sorted(sample(range(n), r))
+    return tuple(pool[i] for i in indices)
+
+
+def random_combination_with_replacement(iterable, r):
+    """Return a random *r* length subsequence of elements in *iterable*,
+    allowing individual elements to be repeated.
+
+        >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP
+        (0, 0, 1, 2, 2)
+
+    This equivalent to taking a random selection from
+    ``itertools.combinations_with_replacement(iterable, r)``.
+
+    """
+    pool = tuple(iterable)
+    n = len(pool)
+    indices = sorted(randrange(n) for i in range(r))
+    return tuple(pool[i] for i in indices)
+
+
+def nth_combination(iterable, r, index):
+    """Equivalent to ``list(combinations(iterable, r))[index]``.
+
+    The subsequences of *iterable* that are of length *r* can be ordered
+    lexicographically. :func:`nth_combination` computes the subsequence at
+    sort position *index* directly, without computing the previous
+    subsequences.
+
+    """
+    pool = tuple(iterable)
+    n = len(pool)
+    if (r < 0) or (r > n):
+        raise ValueError
+
+    c = 1
+    k = min(r, n - r)
+    for i in range(1, k + 1):
+        c = c * (n - k + i) // i
+
+    if index < 0:
+        index += c
+
+    if (index < 0) or (index >= c):
+        raise IndexError
+
+    result = []
+    while r:
+        c, n, r = c * r // n, n - 1, r - 1
+        while index >= c:
+            index -= c
+            c, n = c * (n - r) // n, n - 1
+        result.append(pool[-1 - n])
+
+    return tuple(result)
+
+
+def prepend(value, iterator):
+    """Yield *value*, followed by the elements in *iterator*.
+
+        >>> value = '0'
+        >>> iterator = ['1', '2', '3']
+        >>> list(prepend(value, iterator))
+        ['0', '1', '2', '3']
+
+    To prepend multiple values, see :func:`itertools.chain`.
+
+    """
+    return chain([value], iterator)
diff --git a/libraries/more_itertools/tests/__init__.py b/libraries/more_itertools/tests/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/libraries/more_itertools/tests/test_more.py b/libraries/more_itertools/tests/test_more.py
new file mode 100644
index 00000000..a1b1e431
--- /dev/null
+++ b/libraries/more_itertools/tests/test_more.py
@@ -0,0 +1,2074 @@
+from __future__ import division, print_function, unicode_literals
+
+from collections import OrderedDict
+from decimal import Decimal
+from doctest import DocTestSuite
+from fractions import Fraction
+from functools import partial, reduce
+from heapq import merge
+from io import StringIO
+from itertools import (
+    chain,
+    count,
+    groupby,
+    islice,
+    permutations,
+    product,
+    repeat,
+)
+from operator import add, mul, itemgetter
+from unittest import TestCase
+
+from six.moves import filter, map, range, zip
+
+import more_itertools as mi
+
+
+def load_tests(loader, tests, ignore):
+    # Add the doctests
+    tests.addTests(DocTestSuite('more_itertools.more'))
+    return tests
+
+
+class CollateTests(TestCase):
+    """Unit tests for ``collate()``"""
+    # Also accidentally tests peekable, though that could use its own tests
+
+    def test_default(self):
+        """Test with the default `key` function."""
+        iterables = [range(4), range(7), range(3, 6)]
+        self.assertEqual(
+            sorted(reduce(list.__add__, [list(it) for it in iterables])),
+            list(mi.collate(*iterables))
+        )
+
+    def test_key(self):
+        """Test using a custom `key` function."""
+        iterables = [range(5, 0, -1), range(4, 0, -1)]
+        actual = sorted(
+            reduce(list.__add__, [list(it) for it in iterables]), reverse=True
+        )
+        expected = list(mi.collate(*iterables, key=lambda x: -x))
+        self.assertEqual(actual, expected)
+
+    def test_empty(self):
+        """Be nice if passed an empty list of iterables."""
+        self.assertEqual([], list(mi.collate()))
+
+    def test_one(self):
+        """Work when only 1 iterable is passed."""
+        self.assertEqual([0, 1], list(mi.collate(range(2))))
+
+    def test_reverse(self):
+        """Test the `reverse` kwarg."""
+        iterables = [range(4, 0, -1), range(7, 0, -1), range(3, 6, -1)]
+
+        actual = sorted(
+            reduce(list.__add__, [list(it) for it in iterables]), reverse=True
+        )
+        expected = list(mi.collate(*iterables, reverse=True))
+        self.assertEqual(actual, expected)
+
+    def test_alias(self):
+        self.assertNotEqual(merge.__doc__, mi.collate.__doc__)
+        self.assertNotEqual(partial.__doc__, mi.collate.__doc__)
+
+
+class ChunkedTests(TestCase):
+    """Tests for ``chunked()``"""
+
+    def test_even(self):
+        """Test when ``n`` divides evenly into the length of the iterable."""
+        self.assertEqual(
+            list(mi.chunked('ABCDEF', 3)), [['A', 'B', 'C'], ['D', 'E', 'F']]
+        )
+
+    def test_odd(self):
+        """Test when ``n`` does not divide evenly into the length of the
+        iterable.
+
+        """
+        self.assertEqual(
+            list(mi.chunked('ABCDE', 3)), [['A', 'B', 'C'], ['D', 'E']]
+        )
+
+
+class FirstTests(TestCase):
+    """Tests for ``first()``"""
+
+    def test_many(self):
+        """Test that it works on many-item iterables."""
+        # Also try it on a generator expression to make sure it works on
+        # whatever those return, across Python versions.
+        self.assertEqual(mi.first(x for x in range(4)), 0)
+
+    def test_one(self):
+        """Test that it doesn't raise StopIteration prematurely."""
+        self.assertEqual(mi.first([3]), 3)
+
+    def test_empty_stop_iteration(self):
+        """It should raise StopIteration for empty iterables."""
+        self.assertRaises(ValueError, lambda: mi.first([]))
+
+    def test_default(self):
+        """It should return the provided default arg for empty iterables."""
+        self.assertEqual(mi.first([], 'boo'), 'boo')
+
+
+class IterOnlyRange:
+    """User-defined iterable class which only support __iter__.
+
+    It is not specified to inherit ``object``, so indexing on a instance will
+    raise an ``AttributeError`` rather than ``TypeError`` in Python 2.
+
+    >>> r = IterOnlyRange(5)
+    >>> r[0]
+    AttributeError: IterOnlyRange instance has no attribute '__getitem__'
+
+    Note: In Python 3, ``TypeError`` will be raised because ``object`` is
+    inherited implicitly by default.
+
+    >>> r[0]
+    TypeError: 'IterOnlyRange' object does not support indexing
+    """
+    def __init__(self, n):
+        """Set the length of the range."""
+        self.n = n
+
+    def __iter__(self):
+        """Works same as range()."""
+        return iter(range(self.n))
+
+
+class LastTests(TestCase):
+    """Tests for ``last()``"""
+
+    def test_many_nonsliceable(self):
+        """Test that it works on many-item non-slice-able iterables."""
+        # Also try it on a generator expression to make sure it works on
+        # whatever those return, across Python versions.
+        self.assertEqual(mi.last(x for x in range(4)), 3)
+
+    def test_one_nonsliceable(self):
+        """Test that it doesn't raise StopIteration prematurely."""
+        self.assertEqual(mi.last(x for x in range(1)), 0)
+
+    def test_empty_stop_iteration_nonsliceable(self):
+        """It should raise ValueError for empty non-slice-able iterables."""
+        self.assertRaises(ValueError, lambda: mi.last(x for x in range(0)))
+
+    def test_default_nonsliceable(self):
+        """It should return the provided default arg for empty non-slice-able
+        iterables.
+        """
+        self.assertEqual(mi.last((x for x in range(0)), 'boo'), 'boo')
+
+    def test_many_sliceable(self):
+        """Test that it works on many-item slice-able iterables."""
+        self.assertEqual(mi.last([0, 1, 2, 3]), 3)
+
+    def test_one_sliceable(self):
+        """Test that it doesn't raise StopIteration prematurely."""
+        self.assertEqual(mi.last([3]), 3)
+
+    def test_empty_stop_iteration_sliceable(self):
+        """It should raise ValueError for empty slice-able iterables."""
+        self.assertRaises(ValueError, lambda: mi.last([]))
+
+    def test_default_sliceable(self):
+        """It should return the provided default arg for empty slice-able
+        iterables.
+        """
+        self.assertEqual(mi.last([], 'boo'), 'boo')
+
+    def test_dict(self):
+        """last(dic) and last(dic.keys()) should return same result."""
+        dic = {'a': 1, 'b': 2, 'c': 3}
+        self.assertEqual(mi.last(dic), mi.last(dic.keys()))
+
+    def test_ordereddict(self):
+        """last(dic) should return the last key."""
+        od = OrderedDict()
+        od['a'] = 1
+        od['b'] = 2
+        od['c'] = 3
+        self.assertEqual(mi.last(od), 'c')
+
+    def test_customrange(self):
+        """It should work on custom class where [] raises AttributeError."""
+        self.assertEqual(mi.last(IterOnlyRange(5)), 4)
+
+
+class PeekableTests(TestCase):
+    """Tests for ``peekable()`` behavor not incidentally covered by testing
+    ``collate()``
+
+    """
+    def test_peek_default(self):
+        """Make sure passing a default into ``peek()`` works."""
+        p = mi.peekable([])
+        self.assertEqual(p.peek(7), 7)
+
+    def test_truthiness(self):
+        """Make sure a ``peekable`` tests true iff there are items remaining in
+        the iterable.
+
+        """
+        p = mi.peekable([])
+        self.assertFalse(p)
+
+        p = mi.peekable(range(3))
+        self.assertTrue(p)
+
+    def test_simple_peeking(self):
+        """Make sure ``next`` and ``peek`` advance and don't advance the
+        iterator, respectively.
+
+        """
+        p = mi.peekable(range(10))
+        self.assertEqual(next(p), 0)
+        self.assertEqual(p.peek(), 1)
+        self.assertEqual(next(p), 1)
+
+    def test_indexing(self):
+        """
+        Indexing into the peekable shouldn't advance the iterator.
+        """
+        p = mi.peekable('abcdefghijkl')
+
+        # The 0th index is what ``next()`` will return
+        self.assertEqual(p[0], 'a')
+        self.assertEqual(next(p), 'a')
+
+        # Indexing further into the peekable shouldn't advance the itertor
+        self.assertEqual(p[2], 'd')
+        self.assertEqual(next(p), 'b')
+
+        # The 0th index moves up with the iterator; the last index follows
+        self.assertEqual(p[0], 'c')
+        self.assertEqual(p[9], 'l')
+
+        self.assertEqual(next(p), 'c')
+        self.assertEqual(p[8], 'l')
+
+        # Negative indexing should work too
+        self.assertEqual(p[-2], 'k')
+        self.assertEqual(p[-9], 'd')
+        self.assertRaises(IndexError, lambda: p[-10])
+
+    def test_slicing(self):
+        """Slicing the peekable shouldn't advance the iterator."""
+        seq = list('abcdefghijkl')
+        p = mi.peekable(seq)
+
+        # Slicing the peekable should just be like slicing a re-iterable
+        self.assertEqual(p[1:4], seq[1:4])
+
+        # Advancing the iterator moves the slices up also
+        self.assertEqual(next(p), 'a')
+        self.assertEqual(p[1:4], seq[1:][1:4])
+
+        # Implicit starts and stop should work
+        self.assertEqual(p[:5], seq[1:][:5])
+        self.assertEqual(p[:], seq[1:][:])
+
+        # Indexing past the end should work
+        self.assertEqual(p[:100], seq[1:][:100])
+
+        # Steps should work, including negative
+        self.assertEqual(p[::2], seq[1:][::2])
+        self.assertEqual(p[::-1], seq[1:][::-1])
+
+    def test_slicing_reset(self):
+        """Test slicing on a fresh iterable each time"""
+        iterable = ['0', '1', '2', '3', '4', '5']
+        indexes = list(range(-4, len(iterable) + 4)) + [None]
+        steps = [1, 2, 3, 4, -1, -2, -3, 4]
+        for slice_args in product(indexes, indexes, steps):
+            it = iter(iterable)
+            p = mi.peekable(it)
+            next(p)
+            index = slice(*slice_args)
+            actual = p[index]
+            expected = iterable[1:][index]
+            self.assertEqual(actual, expected, slice_args)
+
+    def test_slicing_error(self):
+        iterable = '01234567'
+        p = mi.peekable(iter(iterable))
+
+        # Prime the cache
+        p.peek()
+        old_cache = list(p._cache)
+
+        # Illegal slice
+        with self.assertRaises(ValueError):
+            p[1:-1:0]
+
+        # Neither the cache nor the iteration should be affected
+        self.assertEqual(old_cache, list(p._cache))
+        self.assertEqual(list(p), list(iterable))
+
+    def test_passthrough(self):
+        """Iterating a peekable without using ``peek()`` or ``prepend()``
+        should just give the underlying iterable's elements (a trivial test but
+        useful to set a baseline in case something goes wrong)"""
+        expected = [1, 2, 3, 4, 5]
+        actual = list(mi.peekable(expected))
+        self.assertEqual(actual, expected)
+
+    # prepend() behavior tests
+
+    def test_prepend(self):
+        """Tests intersperesed ``prepend()`` and ``next()`` calls"""
+        it = mi.peekable(range(2))
+        actual = []
+
+        # Test prepend() before next()
+        it.prepend(10)
+        actual += [next(it), next(it)]
+
+        # Test prepend() between next()s
+        it.prepend(11)
+        actual += [next(it), next(it)]
+
+        # Test prepend() after source iterable is consumed
+        it.prepend(12)
+        actual += [next(it)]
+
+        expected = [10, 0, 11, 1, 12]
+        self.assertEqual(actual, expected)
+
+    def test_multi_prepend(self):
+        """Tests prepending multiple items and getting them in proper order"""
+        it = mi.peekable(range(5))
+        actual = [next(it), next(it)]
+        it.prepend(10, 11, 12)
+        it.prepend(20, 21)
+        actual += list(it)
+        expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4]
+        self.assertEqual(actual, expected)
+
+    def test_empty(self):
+        """Tests prepending in front of an empty iterable"""
+        it = mi.peekable([])
+        it.prepend(10)
+        actual = list(it)
+        expected = [10]
+        self.assertEqual(actual, expected)
+
+    def test_prepend_truthiness(self):
+        """Tests that ``__bool__()`` or ``__nonzero__()`` works properly
+        with ``prepend()``"""
+        it = mi.peekable(range(5))
+        self.assertTrue(it)
+        actual = list(it)
+        self.assertFalse(it)
+        it.prepend(10)
+        self.assertTrue(it)
+        actual += [next(it)]
+        self.assertFalse(it)
+        expected = [0, 1, 2, 3, 4, 10]
+        self.assertEqual(actual, expected)
+
+    def test_multi_prepend_peek(self):
+        """Tests prepending multiple elements and getting them in reverse order
+        while peeking"""
+        it = mi.peekable(range(5))
+        actual = [next(it), next(it)]
+        self.assertEqual(it.peek(), 2)
+        it.prepend(10, 11, 12)
+        self.assertEqual(it.peek(), 10)
+        it.prepend(20, 21)
+        self.assertEqual(it.peek(), 20)
+        actual += list(it)
+        self.assertFalse(it)
+        expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4]
+        self.assertEqual(actual, expected)
+
+    def test_prepend_after_stop(self):
+        """Test resuming iteration after a previous exhaustion"""
+        it = mi.peekable(range(3))
+        self.assertEqual(list(it), [0, 1, 2])
+        self.assertRaises(StopIteration, lambda: next(it))
+        it.prepend(10)
+        self.assertEqual(next(it), 10)
+        self.assertRaises(StopIteration, lambda: next(it))
+
+    def test_prepend_slicing(self):
+        """Tests interaction between prepending and slicing"""
+        seq = list(range(20))
+        p = mi.peekable(seq)
+
+        p.prepend(30, 40, 50)
+        pseq = [30, 40, 50] + seq  # pseq for prepended_seq
+
+        # adapt the specific tests from test_slicing
+        self.assertEqual(p[0], 30)
+        self.assertEqual(p[1:8], pseq[1:8])
+        self.assertEqual(p[1:], pseq[1:])
+        self.assertEqual(p[:5], pseq[:5])
+        self.assertEqual(p[:], pseq[:])
+        self.assertEqual(p[:100], pseq[:100])
+        self.assertEqual(p[::2], pseq[::2])
+        self.assertEqual(p[::-1], pseq[::-1])
+
+    def test_prepend_indexing(self):
+        """Tests interaction between prepending and indexing"""
+        seq = list(range(20))
+        p = mi.peekable(seq)
+
+        p.prepend(30, 40, 50)
+
+        self.assertEqual(p[0], 30)
+        self.assertEqual(next(p), 30)
+        self.assertEqual(p[2], 0)
+        self.assertEqual(next(p), 40)
+        self.assertEqual(p[0], 50)
+        self.assertEqual(p[9], 8)
+        self.assertEqual(next(p), 50)
+        self.assertEqual(p[8], 8)
+        self.assertEqual(p[-2], 18)
+        self.assertEqual(p[-9], 11)
+        self.assertRaises(IndexError, lambda: p[-21])
+
+    def test_prepend_iterable(self):
+        """Tests prepending from an iterable"""
+        it = mi.peekable(range(5))
+        # Don't directly use the range() object to avoid any range-specific
+        # optimizations
+        it.prepend(*(x for x in range(5)))
+        actual = list(it)
+        expected = list(chain(range(5), range(5)))
+        self.assertEqual(actual, expected)
+
+    def test_prepend_many(self):
+        """Tests that prepending a huge number of elements works"""
+        it = mi.peekable(range(5))
+        # Don't directly use the range() object to avoid any range-specific
+        # optimizations
+        it.prepend(*(x for x in range(20000)))
+        actual = list(it)
+        expected = list(chain(range(20000), range(5)))
+        self.assertEqual(actual, expected)
+
+    def test_prepend_reversed(self):
+        """Tests prepending from a reversed iterable"""
+        it = mi.peekable(range(3))
+        it.prepend(*reversed((10, 11, 12)))
+        actual = list(it)
+        expected = [12, 11, 10, 0, 1, 2]
+        self.assertEqual(actual, expected)
+
+
+class ConsumerTests(TestCase):
+    """Tests for ``consumer()``"""
+
+    def test_consumer(self):
+        @mi.consumer
+        def eater():
+            while True:
+                x = yield  # noqa
+
+        e = eater()
+        e.send('hi')  # without @consumer, would raise TypeError
+
+
+class DistinctPermutationsTests(TestCase):
+    def test_distinct_permutations(self):
+        """Make sure the output for ``distinct_permutations()`` is the same as
+        set(permutations(it)).
+
+        """
+        iterable = ['z', 'a', 'a', 'q', 'q', 'q', 'y']
+        test_output = sorted(mi.distinct_permutations(iterable))
+        ref_output = sorted(set(permutations(iterable)))
+        self.assertEqual(test_output, ref_output)
+
+    def test_other_iterables(self):
+        """Make sure ``distinct_permutations()`` accepts a different type of
+        iterables.
+
+        """
+        # a generator
+        iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+        test_output = sorted(mi.distinct_permutations(iterable))
+        # "reload" it
+        iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+        ref_output = sorted(set(permutations(iterable)))
+        self.assertEqual(test_output, ref_output)
+
+        # an iterator
+        iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+        test_output = sorted(mi.distinct_permutations(iterable))
+        # "reload" it
+        iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y'])
+        ref_output = sorted(set(permutations(iterable)))
+        self.assertEqual(test_output, ref_output)
+
+
+class IlenTests(TestCase):
+    def test_ilen(self):
+        """Sanity-checks for ``ilen()``."""
+        # Non-empty
+        self.assertEqual(
+            mi.ilen(filter(lambda x: x % 10 == 0, range(101))), 11
+        )
+
+        # Empty
+        self.assertEqual(mi.ilen((x for x in range(0))), 0)
+
+        # Iterable with __len__
+        self.assertEqual(mi.ilen(list(range(6))), 6)
+
+
+class WithIterTests(TestCase):
+    def test_with_iter(self):
+        s = StringIO('One fish\nTwo fish')
+        initial_words = [line.split()[0] for line in mi.with_iter(s)]
+
+        # Iterable's items should be faithfully represented
+        self.assertEqual(initial_words, ['One', 'Two'])
+        # The file object should be closed
+        self.assertEqual(s.closed, True)
+
+
+class OneTests(TestCase):
+    def test_basic(self):
+        it = iter(['item'])
+        self.assertEqual(mi.one(it), 'item')
+
+    def test_too_short(self):
+        it = iter([])
+        self.assertRaises(ValueError, lambda: mi.one(it))
+        self.assertRaises(IndexError, lambda: mi.one(it, too_short=IndexError))
+
+    def test_too_long(self):
+        it = count()
+        self.assertRaises(ValueError, lambda: mi.one(it))  # burn 0 and 1
+        self.assertEqual(next(it), 2)
+        self.assertRaises(
+            OverflowError, lambda: mi.one(it, too_long=OverflowError)
+        )
+
+
+class IntersperseTest(TestCase):
+    """ Tests for intersperse() """
+
+    def test_even(self):
+        iterable = (x for x in '01')
+        self.assertEqual(
+            list(mi.intersperse(None, iterable)), ['0', None, '1']
+        )
+
+    def test_odd(self):
+        iterable = (x for x in '012')
+        self.assertEqual(
+            list(mi.intersperse(None, iterable)), ['0', None, '1', None, '2']
+        )
+
+    def test_nested(self):
+        element = ('a', 'b')
+        iterable = (x for x in '012')
+        actual = list(mi.intersperse(element, iterable))
+        expected = ['0', ('a', 'b'), '1', ('a', 'b'), '2']
+        self.assertEqual(actual, expected)
+
+    def test_not_iterable(self):
+        self.assertRaises(TypeError, lambda: mi.intersperse('x', 1))
+
+    def test_n(self):
+        for n, element, expected in [
+            (1, '_', ['0', '_', '1', '_', '2', '_', '3', '_', '4', '_', '5']),
+            (2, '_', ['0', '1', '_', '2', '3', '_', '4', '5']),
+            (3, '_', ['0', '1', '2', '_', '3', '4', '5']),
+            (4, '_', ['0', '1', '2', '3', '_', '4', '5']),
+            (5, '_', ['0', '1', '2', '3', '4', '_', '5']),
+            (6, '_', ['0', '1', '2', '3', '4', '5']),
+            (7, '_', ['0', '1', '2', '3', '4', '5']),
+            (3, ['a', 'b'], ['0', '1', '2', ['a', 'b'], '3', '4', '5']),
+        ]:
+            iterable = (x for x in '012345')
+            actual = list(mi.intersperse(element, iterable, n=n))
+            self.assertEqual(actual, expected)
+
+    def test_n_zero(self):
+        self.assertRaises(
+            ValueError, lambda: list(mi.intersperse('x', '012', n=0))
+        )
+
+
+class UniqueToEachTests(TestCase):
+    """Tests for ``unique_to_each()``"""
+
+    def test_all_unique(self):
+        """When all the input iterables are unique the output should match
+        the input."""
+        iterables = [[1, 2], [3, 4, 5], [6, 7, 8]]
+        self.assertEqual(mi.unique_to_each(*iterables), iterables)
+
+    def test_duplicates(self):
+        """When there are duplicates in any of the input iterables that aren't
+        in the rest, those duplicates should be emitted."""
+        iterables = ["mississippi", "missouri"]
+        self.assertEqual(
+            mi.unique_to_each(*iterables), [['p', 'p'], ['o', 'u', 'r']]
+        )
+
+    def test_mixed(self):
+        """When the input iterables contain different types the function should
+        still behave properly"""
+        iterables = ['x', (i for i in range(3)), [1, 2, 3], tuple()]
+        self.assertEqual(mi.unique_to_each(*iterables), [['x'], [0], [3], []])
+
+
+class WindowedTests(TestCase):
+    """Tests for ``windowed()``"""
+
+    def test_basic(self):
+        actual = list(mi.windowed([1, 2, 3, 4, 5], 3))
+        expected = [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
+        self.assertEqual(actual, expected)
+
+    def test_large_size(self):
+        """
+        When the window size is larger than the iterable, and no fill value is
+        given,``None`` should be filled in.
+        """
+        actual = list(mi.windowed([1, 2, 3, 4, 5], 6))
+        expected = [(1, 2, 3, 4, 5, None)]
+        self.assertEqual(actual, expected)
+
+    def test_fillvalue(self):
+        """
+        When sizes don't match evenly, the given fill value should be used.
+        """
+        iterable = [1, 2, 3, 4, 5]
+
+        for n, kwargs, expected in [
+            (6, {}, [(1, 2, 3, 4, 5, '!')]),  # n > len(iterable)
+            (3, {'step': 3}, [(1, 2, 3), (4, 5, '!')]),  # using ``step``
+        ]:
+            actual = list(mi.windowed(iterable, n, fillvalue='!', **kwargs))
+            self.assertEqual(actual, expected)
+
+    def test_zero(self):
+        """When the window size is zero, an empty tuple should be emitted."""
+        actual = list(mi.windowed([1, 2, 3, 4, 5], 0))
+        expected = [tuple()]
+        self.assertEqual(actual, expected)
+
+    def test_negative(self):
+        """When the window size is negative, ValueError should be raised."""
+        with self.assertRaises(ValueError):
+            list(mi.windowed([1, 2, 3, 4, 5], -1))
+
+    def test_step(self):
+        """The window should advance by the number of steps provided"""
+        iterable = [1, 2, 3, 4, 5, 6, 7]
+        for n, step, expected in [
+            (3, 2, [(1, 2, 3), (3, 4, 5), (5, 6, 7)]),  # n > step
+            (3, 3, [(1, 2, 3), (4, 5, 6), (7, None, None)]),  # n == step
+            (3, 4, [(1, 2, 3), (5, 6, 7)]),  # line up nicely
+            (3, 5, [(1, 2, 3), (6, 7, None)]),  # off by one
+            (3, 6, [(1, 2, 3), (7, None, None)]),  # off by two
+            (3, 7, [(1, 2, 3)]),  # step past the end
+            (7, 8, [(1, 2, 3, 4, 5, 6, 7)]),  # step > len(iterable)
+        ]:
+            actual = list(mi.windowed(iterable, n, step=step))
+            self.assertEqual(actual, expected)
+
+        # Step must be greater than or equal to 1
+        with self.assertRaises(ValueError):
+            list(mi.windowed(iterable, 3, step=0))
+
+
+class BucketTests(TestCase):
+    """Tests for ``bucket()``"""
+
+    def test_basic(self):
+        iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33]
+        D = mi.bucket(iterable, key=lambda x: 10 * (x // 10))
+
+        # In-order access
+        self.assertEqual(list(D[10]), [10, 11, 12])
+
+        # Out of order access
+        self.assertEqual(list(D[30]), [30, 31, 33])
+        self.assertEqual(list(D[20]), [20, 21, 22, 23])
+
+        self.assertEqual(list(D[40]), [])  # Nothing in here!
+
+    def test_in(self):
+        iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33]
+        D = mi.bucket(iterable, key=lambda x: 10 * (x // 10))
+
+        self.assertTrue(10 in D)
+        self.assertFalse(40 in D)
+        self.assertTrue(20 in D)
+        self.assertFalse(21 in D)
+
+        # Checking in-ness shouldn't advance the iterator
+        self.assertEqual(next(D[10]), 10)
+
+    def test_validator(self):
+        iterable = count(0)
+        key = lambda x: int(str(x)[0])  # First digit of each number
+        validator = lambda x: 0 < x < 10  # No leading zeros
+        D = mi.bucket(iterable, key, validator=validator)
+        self.assertEqual(mi.take(3, D[1]), [1, 10, 11])
+        self.assertNotIn(0, D)  # Non-valid entries don't return True
+        self.assertNotIn(0, D._cache)  # Don't store non-valid entries
+        self.assertEqual(list(D[0]), [])
+
+
+class SpyTests(TestCase):
+    """Tests for ``spy()``"""
+
+    def test_basic(self):
+        original_iterable = iter('abcdefg')
+        head, new_iterable = mi.spy(original_iterable)
+        self.assertEqual(head, ['a'])
+        self.assertEqual(
+            list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+        )
+
+    def test_unpacking(self):
+        original_iterable = iter('abcdefg')
+        (first, second, third), new_iterable = mi.spy(original_iterable, 3)
+        self.assertEqual(first, 'a')
+        self.assertEqual(second, 'b')
+        self.assertEqual(third, 'c')
+        self.assertEqual(
+            list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g']
+        )
+
+    def test_too_many(self):
+        original_iterable = iter('abc')
+        head, new_iterable = mi.spy(original_iterable, 4)
+        self.assertEqual(head, ['a', 'b', 'c'])
+        self.assertEqual(list(new_iterable), ['a', 'b', 'c'])
+
+    def test_zero(self):
+        original_iterable = iter('abc')
+        head, new_iterable = mi.spy(original_iterable, 0)
+        self.assertEqual(head, [])
+        self.assertEqual(list(new_iterable), ['a', 'b', 'c'])
+
+
+class InterleaveTests(TestCase):
+    def test_even(self):
+        actual = list(mi.interleave([1, 4, 7], [2, 5, 8], [3, 6, 9]))
+        expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
+        self.assertEqual(actual, expected)
+
+    def test_short(self):
+        actual = list(mi.interleave([1, 4], [2, 5, 7], [3, 6, 8]))
+        expected = [1, 2, 3, 4, 5, 6]
+        self.assertEqual(actual, expected)
+
+    def test_mixed_types(self):
+        it_list = ['a', 'b', 'c', 'd']
+        it_str = '12345'
+        it_inf = count()
+        actual = list(mi.interleave(it_list, it_str, it_inf))
+        expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', 3]
+        self.assertEqual(actual, expected)
+
+
+class InterleaveLongestTests(TestCase):
+    def test_even(self):
+        actual = list(mi.interleave_longest([1, 4, 7], [2, 5, 8], [3, 6, 9]))
+        expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
+        self.assertEqual(actual, expected)
+
+    def test_short(self):
+        actual = list(mi.interleave_longest([1, 4], [2, 5, 7], [3, 6, 8]))
+        expected = [1, 2, 3, 4, 5, 6, 7, 8]
+        self.assertEqual(actual, expected)
+
+    def test_mixed_types(self):
+        it_list = ['a', 'b', 'c', 'd']
+        it_str = '12345'
+        it_gen = (x for x in range(3))
+        actual = list(mi.interleave_longest(it_list, it_str, it_gen))
+        expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', '5']
+        self.assertEqual(actual, expected)
+
+
+class TestCollapse(TestCase):
+    """Tests for ``collapse()``"""
+
+    def test_collapse(self):
+        l = [[1], 2, [[3], 4], [[[5]]]]
+        self.assertEqual(list(mi.collapse(l)), [1, 2, 3, 4, 5])
+
+    def test_collapse_to_string(self):
+        l = [["s1"], "s2", [["s3"], "s4"], [[["s5"]]]]
+        self.assertEqual(list(mi.collapse(l)), ["s1", "s2", "s3", "s4", "s5"])
+
+    def test_collapse_flatten(self):
+        l = [[1], [2], [[3], 4], [[[5]]]]
+        self.assertEqual(list(mi.collapse(l, levels=1)), list(mi.flatten(l)))
+
+    def test_collapse_to_level(self):
+        l = [[1], 2, [[3], 4], [[[5]]]]
+        self.assertEqual(list(mi.collapse(l, levels=2)), [1, 2, 3, 4, [5]])
+        self.assertEqual(
+            list(mi.collapse(mi.collapse(l, levels=1), levels=1)),
+            list(mi.collapse(l, levels=2))
+        )
+
+    def test_collapse_to_list(self):
+        l = (1, [2], (3, [4, (5,)], 'ab'))
+        actual = list(mi.collapse(l, base_type=list))
+        expected = [1, [2], 3, [4, (5,)], 'ab']
+        self.assertEqual(actual, expected)
+
+
+class SideEffectTests(TestCase):
+    """Tests for ``side_effect()``"""
+
+    def test_individual(self):
+        # The function increments the counter for each call
+        counter = [0]
+
+        def func(arg):
+            counter[0] += 1
+
+        result = list(mi.side_effect(func, range(10)))
+        self.assertEqual(result, list(range(10)))
+        self.assertEqual(counter[0], 10)
+
+    def test_chunked(self):
+        # The function increments the counter for each call
+        counter = [0]
+
+        def func(arg):
+            counter[0] += 1
+
+        result = list(mi.side_effect(func, range(10), 2))
+        self.assertEqual(result, list(range(10)))
+        self.assertEqual(counter[0], 5)
+
+    def test_before_after(self):
+        f = StringIO()
+        collector = []
+
+        def func(item):
+            print(item, file=f)
+            collector.append(f.getvalue())
+
+        def it():
+            yield u'a'
+            yield u'b'
+            raise RuntimeError('kaboom')
+
+        before = lambda: print('HEADER', file=f)
+        after = f.close
+
+        try:
+            mi.consume(mi.side_effect(func, it(), before=before, after=after))
+        except RuntimeError:
+            pass
+
+        # The iterable should have been written to the file
+        self.assertEqual(collector, [u'HEADER\na\n', u'HEADER\na\nb\n'])
+
+        # The file should be closed even though something bad happened
+        self.assertTrue(f.closed)
+
+    def test_before_fails(self):
+        f = StringIO()
+        func = lambda x: print(x, file=f)
+
+        def before():
+            raise RuntimeError('ouch')
+
+        try:
+            mi.consume(
+                mi.side_effect(func, u'abc', before=before, after=f.close)
+            )
+        except RuntimeError:
+            pass
+
+        # The file should be closed even though something bad happened in the
+        # before function
+        self.assertTrue(f.closed)
+
+
+class SlicedTests(TestCase):
+    """Tests for ``sliced()``"""
+
+    def test_even(self):
+        """Test when the length of the sequence is divisible by *n*"""
+        seq = 'ABCDEFGHI'
+        self.assertEqual(list(mi.sliced(seq, 3)), ['ABC', 'DEF', 'GHI'])
+
+    def test_odd(self):
+        """Test when the length of the sequence is not divisible by *n*"""
+        seq = 'ABCDEFGHI'
+        self.assertEqual(list(mi.sliced(seq, 4)), ['ABCD', 'EFGH', 'I'])
+
+    def test_not_sliceable(self):
+        seq = (x for x in 'ABCDEFGHI')
+
+        with self.assertRaises(TypeError):
+            list(mi.sliced(seq, 3))
+
+
+class SplitAtTests(TestCase):
+    """Tests for ``split()``"""
+
+    def comp_with_str_split(self, str_to_split, delim):
+        pred = lambda c: c == delim
+        actual = list(map(''.join, mi.split_at(str_to_split, pred)))
+        expected = str_to_split.split(delim)
+        self.assertEqual(actual, expected)
+
+    def test_seperators(self):
+        test_strs = ['', 'abcba', 'aaabbbcccddd', 'e']
+        for s, delim in product(test_strs, 'abcd'):
+            self.comp_with_str_split(s, delim)
+
+
+class SplitBeforeTest(TestCase):
+    """Tests for ``split_before()``"""
+
+    def test_starts_with_sep(self):
+        actual = list(mi.split_before('xooxoo', lambda c: c == 'x'))
+        expected = [['x', 'o', 'o'], ['x', 'o', 'o']]
+        self.assertEqual(actual, expected)
+
+    def test_ends_with_sep(self):
+        actual = list(mi.split_before('ooxoox', lambda c: c == 'x'))
+        expected = [['o', 'o'], ['x', 'o', 'o'], ['x']]
+        self.assertEqual(actual, expected)
+
+    def test_no_sep(self):
+        actual = list(mi.split_before('ooo', lambda c: c == 'x'))
+        expected = [['o', 'o', 'o']]
+        self.assertEqual(actual, expected)
+
+
+class SplitAfterTest(TestCase):
+    """Tests for ``split_after()``"""
+
+    def test_starts_with_sep(self):
+        actual = list(mi.split_after('xooxoo', lambda c: c == 'x'))
+        expected = [['x'], ['o', 'o', 'x'], ['o', 'o']]
+        self.assertEqual(actual, expected)
+
+    def test_ends_with_sep(self):
+        actual = list(mi.split_after('ooxoox', lambda c: c == 'x'))
+        expected = [['o', 'o', 'x'], ['o', 'o', 'x']]
+        self.assertEqual(actual, expected)
+
+    def test_no_sep(self):
+        actual = list(mi.split_after('ooo', lambda c: c == 'x'))
+        expected = [['o', 'o', 'o']]
+        self.assertEqual(actual, expected)
+
+
+class PaddedTest(TestCase):
+    """Tests for ``padded()``"""
+
+    def test_no_n(self):
+        seq = [1, 2, 3]
+
+        # No fillvalue
+        self.assertEqual(mi.take(5, mi.padded(seq)), [1, 2, 3, None, None])
+
+        # With fillvalue
+        self.assertEqual(
+            mi.take(5, mi.padded(seq, fillvalue='')), [1, 2, 3, '', '']
+        )
+
+    def test_invalid_n(self):
+        self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=-1)))
+        self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=0)))
+
+    def test_valid_n(self):
+        seq = [1, 2, 3, 4, 5]
+
+        # No need for padding: len(seq) <= n
+        self.assertEqual(list(mi.padded(seq, n=4)), [1, 2, 3, 4, 5])
+        self.assertEqual(list(mi.padded(seq, n=5)), [1, 2, 3, 4, 5])
+
+        # No fillvalue
+        self.assertEqual(
+            list(mi.padded(seq, n=7)), [1, 2, 3, 4, 5, None, None]
+        )
+
+        # With fillvalue
+        self.assertEqual(
+            list(mi.padded(seq, fillvalue='', n=7)), [1, 2, 3, 4, 5, '', '']
+        )
+
+    def test_next_multiple(self):
+        seq = [1, 2, 3, 4, 5, 6]
+
+        # No need for padding: len(seq) % n == 0
+        self.assertEqual(
+            list(mi.padded(seq, n=3, next_multiple=True)), [1, 2, 3, 4, 5, 6]
+        )
+
+        # Padding needed: len(seq) < n
+        self.assertEqual(
+            list(mi.padded(seq, n=8, next_multiple=True)),
+            [1, 2, 3, 4, 5, 6, None, None]
+        )
+
+        # No padding needed: len(seq) == n
+        self.assertEqual(
+            list(mi.padded(seq, n=6, next_multiple=True)), [1, 2, 3, 4, 5, 6]
+        )
+
+        # Padding needed: len(seq) > n
+        self.assertEqual(
+            list(mi.padded(seq, n=4, next_multiple=True)),
+            [1, 2, 3, 4, 5, 6, None, None]
+        )
+
+        # With fillvalue
+        self.assertEqual(
+            list(mi.padded(seq, fillvalue='', n=4, next_multiple=True)),
+            [1, 2, 3, 4, 5, 6, '', '']
+        )
+
+
+class DistributeTest(TestCase):
+    """Tests for distribute()"""
+
+    def test_invalid_n(self):
+        self.assertRaises(ValueError, lambda: mi.distribute(-1, [1, 2, 3]))
+        self.assertRaises(ValueError, lambda: mi.distribute(0, [1, 2, 3]))
+
+    def test_basic(self):
+        iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+        for n, expected in [
+            (1, [iterable]),
+            (2, [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]),
+            (3, [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]),
+            (10, [[n] for n in range(1, 10 + 1)]),
+        ]:
+            self.assertEqual(
+                [list(x) for x in mi.distribute(n, iterable)], expected
+            )
+
+    def test_large_n(self):
+        iterable = [1, 2, 3, 4]
+        self.assertEqual(
+            [list(x) for x in mi.distribute(6, iterable)],
+            [[1], [2], [3], [4], [], []]
+        )
+
+
+class StaggerTest(TestCase):
+    """Tests for ``stagger()``"""
+
+    def test_default(self):
+        iterable = [0, 1, 2, 3]
+        actual = list(mi.stagger(iterable))
+        expected = [(None, 0, 1), (0, 1, 2), (1, 2, 3)]
+        self.assertEqual(actual, expected)
+
+    def test_offsets(self):
+        iterable = [0, 1, 2, 3]
+        for offsets, expected in [
+            ((-2, 0, 2), [('', 0, 2), ('', 1, 3)]),
+            ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3)]),
+            ((1, 2), [(1, 2), (2, 3)]),
+        ]:
+            all_groups = mi.stagger(iterable, offsets=offsets, fillvalue='')
+            self.assertEqual(list(all_groups), expected)
+
+    def test_longest(self):
+        iterable = [0, 1, 2, 3]
+        for offsets, expected in [
+            (
+                (-1, 0, 1),
+                [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, ''), (3, '', '')]
+            ),
+            ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3), (3, '')]),
+            ((1, 2), [(1, 2), (2, 3), (3, '')]),
+        ]:
+            all_groups = mi.stagger(
+                iterable, offsets=offsets, fillvalue='', longest=True
+            )
+            self.assertEqual(list(all_groups), expected)
+
+
+class ZipOffsetTest(TestCase):
+    """Tests for ``zip_offset()``"""
+
+    def test_shortest(self):
+        a_1 = [0, 1, 2, 3]
+        a_2 = [0, 1, 2, 3, 4, 5]
+        a_3 = [0, 1, 2, 3, 4, 5, 6, 7]
+        actual = list(
+            mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), fillvalue='')
+        )
+        expected = [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)]
+        self.assertEqual(actual, expected)
+
+    def test_longest(self):
+        a_1 = [0, 1, 2, 3]
+        a_2 = [0, 1, 2, 3, 4, 5]
+        a_3 = [0, 1, 2, 3, 4, 5, 6, 7]
+        actual = list(
+            mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), longest=True)
+        )
+        expected = [
+            (None, 0, 1),
+            (0, 1, 2),
+            (1, 2, 3),
+            (2, 3, 4),
+            (3, 4, 5),
+            (None, 5, 6),
+            (None, None, 7),
+        ]
+        self.assertEqual(actual, expected)
+
+    def test_mismatch(self):
+        iterables = [0, 1, 2], [2, 3, 4]
+        offsets = (-1, 0, 1)
+        self.assertRaises(
+            ValueError,
+            lambda: list(mi.zip_offset(*iterables, offsets=offsets))
+        )
+
+
+class SortTogetherTest(TestCase):
+    """Tests for sort_together()"""
+
+    def test_key_list(self):
+        """tests `key_list` including default, iterables include duplicates"""
+        iterables = [
+            ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
+            ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+            [97, 20, 100, 70, 100, 20]
+        ]
+
+        self.assertEqual(
+            mi.sort_together(iterables),
+            [
+                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+                ('June', 'July', 'July', 'May', 'Aug.', 'May'),
+                (70, 100, 20, 97, 20, 100)
+            ]
+        )
+
+        self.assertEqual(
+            mi.sort_together(iterables, key_list=(0, 1)),
+            [
+                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+                ('July', 'July', 'June', 'Aug.', 'May', 'May'),
+                (100, 20, 70, 20, 97, 100)
+            ]
+        )
+
+        self.assertEqual(
+            mi.sort_together(iterables, key_list=(0, 1, 2)),
+            [
+                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+                ('July', 'July', 'June', 'Aug.', 'May', 'May'),
+                (20, 100, 70, 20, 97, 100)
+            ]
+        )
+
+        self.assertEqual(
+            mi.sort_together(iterables, key_list=(2,)),
+            [
+                ('GA', 'CT', 'CT', 'GA', 'GA', 'CT'),
+                ('Aug.', 'July', 'June', 'May', 'May', 'July'),
+                (20, 20, 70, 97, 100, 100)
+            ]
+        )
+
+    def test_invalid_key_list(self):
+        """tests `key_list` for indexes not available in `iterables`"""
+        iterables = [
+            ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
+            ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+            [97, 20, 100, 70, 100, 20]
+        ]
+
+        self.assertRaises(
+            IndexError, lambda: mi.sort_together(iterables, key_list=(5,))
+        )
+
+    def test_reverse(self):
+        """tests `reverse` to ensure a reverse sort for `key_list` iterables"""
+        iterables = [
+            ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
+            ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+            [97, 20, 100, 70, 100, 20]
+        ]
+
+        self.assertEqual(
+            mi.sort_together(iterables, key_list=(0, 1, 2), reverse=True),
+            [('GA', 'GA', 'GA', 'CT', 'CT', 'CT'),
+             ('May', 'May', 'Aug.', 'June', 'July', 'July'),
+             (100, 97, 20, 70, 100, 20)]
+        )
+
+    def test_uneven_iterables(self):
+        """tests trimming of iterables to the shortest length before sorting"""
+        iterables = [['GA', 'GA', 'GA', 'CT', 'CT', 'CT', 'MA'],
+                     ['May', 'Aug.', 'May', 'June', 'July', 'July'],
+                     [97, 20, 100, 70, 100, 20, 0]]
+
+        self.assertEqual(
+            mi.sort_together(iterables),
+            [
+                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
+                ('June', 'July', 'July', 'May', 'Aug.', 'May'),
+                (70, 100, 20, 97, 20, 100)
+            ]
+        )
+
+
+class DivideTest(TestCase):
+    """Tests for divide()"""
+
+    def test_invalid_n(self):
+        self.assertRaises(ValueError, lambda: mi.divide(-1, [1, 2, 3]))
+        self.assertRaises(ValueError, lambda: mi.divide(0, [1, 2, 3]))
+
+    def test_basic(self):
+        iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
+
+        for n, expected in [
+            (1, [iterable]),
+            (2, [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]),
+            (3, [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]),
+            (10, [[n] for n in range(1, 10 + 1)]),
+        ]:
+            self.assertEqual(
+                [list(x) for x in mi.divide(n, iterable)], expected
+            )
+
+    def test_large_n(self):
+        iterable = [1, 2, 3, 4]
+        self.assertEqual(
+            [list(x) for x in mi.divide(6, iterable)],
+            [[1], [2], [3], [4], [], []]
+        )
+
+
+class TestAlwaysIterable(TestCase):
+    """Tests for always_iterable()"""
+    def test_single(self):
+        self.assertEqual(list(mi.always_iterable(1)), [1])
+
+    def test_strings(self):
+        for obj in ['foo', b'bar', u'baz']:
+            actual = list(mi.always_iterable(obj))
+            expected = [obj]
+            self.assertEqual(actual, expected)
+
+    def test_base_type(self):
+        dict_obj = {'a': 1, 'b': 2}
+        str_obj = '123'
+
+        # Default: dicts are iterable like they normally are
+        default_actual = list(mi.always_iterable(dict_obj))
+        default_expected = list(dict_obj)
+        self.assertEqual(default_actual, default_expected)
+
+        # Unitary types set: dicts are not iterable
+        custom_actual = list(mi.always_iterable(dict_obj, base_type=dict))
+        custom_expected = [dict_obj]
+        self.assertEqual(custom_actual, custom_expected)
+
+        # With unitary types set, strings are iterable
+        str_actual = list(mi.always_iterable(str_obj, base_type=None))
+        str_expected = list(str_obj)
+        self.assertEqual(str_actual, str_expected)
+
+    def test_iterables(self):
+        self.assertEqual(list(mi.always_iterable([0, 1])), [0, 1])
+        self.assertEqual(
+            list(mi.always_iterable([0, 1], base_type=list)), [[0, 1]]
+        )
+        self.assertEqual(
+            list(mi.always_iterable(iter('foo'))), ['f', 'o', 'o']
+        )
+        self.assertEqual(list(mi.always_iterable([])), [])
+
+    def test_none(self):
+        self.assertEqual(list(mi.always_iterable(None)), [])
+
+    def test_generator(self):
+        def _gen():
+            yield 0
+            yield 1
+
+        self.assertEqual(list(mi.always_iterable(_gen())), [0, 1])
+
+
+class AdjacentTests(TestCase):
+    def test_typical(self):
+        actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10)))
+        expected = [(True, 0), (True, 1), (False, 2), (False, 3), (True, 4),
+                    (True, 5), (True, 6), (False, 7), (False, 8), (False, 9)]
+        self.assertEqual(actual, expected)
+
+    def test_empty_iterable(self):
+        actual = list(mi.adjacent(lambda x: x % 5 == 0, []))
+        expected = []
+        self.assertEqual(actual, expected)
+
+    def test_length_one(self):
+        actual = list(mi.adjacent(lambda x: x % 5 == 0, [0]))
+        expected = [(True, 0)]
+        self.assertEqual(actual, expected)
+
+        actual = list(mi.adjacent(lambda x: x % 5 == 0, [1]))
+        expected = [(False, 1)]
+        self.assertEqual(actual, expected)
+
+    def test_consecutive_true(self):
+        """Test that when the predicate matches multiple consecutive elements
+        it doesn't repeat elements in the output"""
+        actual = list(mi.adjacent(lambda x: x % 5 < 2, range(10)))
+        expected = [(True, 0), (True, 1), (True, 2), (False, 3), (True, 4),
+                    (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)]
+        self.assertEqual(actual, expected)
+
+    def test_distance(self):
+        actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=2))
+        expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4),
+                    (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)]
+        self.assertEqual(actual, expected)
+
+        actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=3))
+        expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4),
+                    (True, 5), (True, 6), (True, 7), (True, 8), (False, 9)]
+        self.assertEqual(actual, expected)
+
+    def test_large_distance(self):
+        """Test distance larger than the length of the iterable"""
+        iterable = range(10)
+        actual = list(mi.adjacent(lambda x: x % 5 == 4, iterable, distance=20))
+        expected = list(zip(repeat(True), iterable))
+        self.assertEqual(actual, expected)
+
+        actual = list(mi.adjacent(lambda x: False, iterable, distance=20))
+        expected = list(zip(repeat(False), iterable))
+        self.assertEqual(actual, expected)
+
+    def test_zero_distance(self):
+        """Test that adjacent() reduces to zip+map when distance is 0"""
+        iterable = range(1000)
+        predicate = lambda x: x % 4 == 2
+        actual = mi.adjacent(predicate, iterable, 0)
+        expected = zip(map(predicate, iterable), iterable)
+        self.assertTrue(all(a == e for a, e in zip(actual, expected)))
+
+    def test_negative_distance(self):
+        """Test that adjacent() raises an error with negative distance"""
+        pred = lambda x: x
+        self.assertRaises(
+            ValueError, lambda: mi.adjacent(pred, range(1000), -1)
+        )
+        self.assertRaises(
+            ValueError, lambda: mi.adjacent(pred, range(10), -10)
+        )
+
+    def test_grouping(self):
+        """Test interaction of adjacent() with groupby_transform()"""
+        iterable = mi.adjacent(lambda x: x % 5 == 0, range(10))
+        grouper = mi.groupby_transform(iterable, itemgetter(0), itemgetter(1))
+        actual = [(k, list(g)) for k, g in grouper]
+        expected = [
+            (True, [0, 1]),
+            (False, [2, 3]),
+            (True, [4, 5, 6]),
+            (False, [7, 8, 9]),
+        ]
+        self.assertEqual(actual, expected)
+
+    def test_call_once(self):
+        """Test that the predicate is only called once per item."""
+        already_seen = set()
+        iterable = range(10)
+
+        def predicate(item):
+            self.assertNotIn(item, already_seen)
+            already_seen.add(item)
+            return True
+
+        actual = list(mi.adjacent(predicate, iterable))
+        expected = [(True, x) for x in iterable]
+        self.assertEqual(actual, expected)
+
+
+class GroupByTransformTests(TestCase):
+    def assertAllGroupsEqual(self, groupby1, groupby2):
+        """Compare two groupby objects for equality, both keys and groups."""
+        for a, b in zip(groupby1, groupby2):
+            key1, group1 = a
+            key2, group2 = b
+            self.assertEqual(key1, key2)
+            self.assertListEqual(list(group1), list(group2))
+        self.assertRaises(StopIteration, lambda: next(groupby1))
+        self.assertRaises(StopIteration, lambda: next(groupby2))
+
+    def test_default_funcs(self):
+        """Test that groupby_transform() with default args mimics groupby()"""
+        iterable = [(x // 5, x) for x in range(1000)]
+        actual = mi.groupby_transform(iterable)
+        expected = groupby(iterable)
+        self.assertAllGroupsEqual(actual, expected)
+
+    def test_valuefunc(self):
+        iterable = [(int(x / 5), int(x / 3), x) for x in range(10)]
+
+        # Test the standard usage of grouping one iterable using another's keys
+        grouper = mi.groupby_transform(
+            iterable, keyfunc=itemgetter(0), valuefunc=itemgetter(-1)
+        )
+        actual = [(k, list(g)) for k, g in grouper]
+        expected = [(0, [0, 1, 2, 3, 4]), (1, [5, 6, 7, 8, 9])]
+        self.assertEqual(actual, expected)
+
+        grouper = mi.groupby_transform(
+            iterable, keyfunc=itemgetter(1), valuefunc=itemgetter(-1)
+        )
+        actual = [(k, list(g)) for k, g in grouper]
+        expected = [(0, [0, 1, 2]), (1, [3, 4, 5]), (2, [6, 7, 8]), (3, [9])]
+        self.assertEqual(actual, expected)
+
+        # and now for something a little different
+        d = dict(zip(range(10), 'abcdefghij'))
+        grouper = mi.groupby_transform(
+            range(10), keyfunc=lambda x: x // 5, valuefunc=d.get
+        )
+        actual = [(k, ''.join(g)) for k, g in grouper]
+        expected = [(0, 'abcde'), (1, 'fghij')]
+        self.assertEqual(actual, expected)
+
+    def test_no_valuefunc(self):
+        iterable = range(1000)
+
+        def key(x):
+            return x // 5
+
+        actual = mi.groupby_transform(iterable, key, valuefunc=None)
+        expected = groupby(iterable, key)
+        self.assertAllGroupsEqual(actual, expected)
+
+        actual = mi.groupby_transform(iterable, key)  # default valuefunc
+        expected = groupby(iterable, key)
+        self.assertAllGroupsEqual(actual, expected)
+
+
+class NumericRangeTests(TestCase):
+    def test_basic(self):
+        for args, expected in [
+            ((4,), [0, 1, 2, 3]),
+            ((4.0,), [0.0, 1.0, 2.0, 3.0]),
+            ((1.0, 4), [1.0, 2.0, 3.0]),
+            ((1, 4.0), [1, 2, 3]),
+            ((1.0, 5), [1.0, 2.0, 3.0, 4.0]),
+            ((0, 20, 5), [0, 5, 10, 15]),
+            ((0, 20, 5.0), [0.0, 5.0, 10.0, 15.0]),
+            ((0, 10, 3), [0, 3, 6, 9]),
+            ((0, 10, 3.0), [0.0, 3.0, 6.0, 9.0]),
+            ((0, -5, -1), [0, -1, -2, -3, -4]),
+            ((0.0, -5, -1), [0.0, -1.0, -2.0, -3.0, -4.0]),
+            ((1, 2, Fraction(1, 2)), [Fraction(1, 1), Fraction(3, 2)]),
+            ((0,), []),
+            ((0.0,), []),
+            ((1, 0), []),
+            ((1.0, 0.0), []),
+            ((Fraction(2, 1),), [Fraction(0, 1), Fraction(1, 1)]),
+            ((Decimal('2.0'),), [Decimal('0.0'), Decimal('1.0')]),
+        ]:
+            actual = list(mi.numeric_range(*args))
+            self.assertEqual(actual, expected)
+            self.assertTrue(
+                all(type(a) == type(e) for a, e in zip(actual, expected))
+            )
+
+    def test_arg_count(self):
+        self.assertRaises(TypeError, lambda: list(mi.numeric_range()))
+        self.assertRaises(
+            TypeError, lambda: list(mi.numeric_range(0, 1, 2, 3))
+        )
+
+    def test_zero_step(self):
+        self.assertRaises(
+            ValueError, lambda: list(mi.numeric_range(1, 2, 0))
+        )
+
+
+class CountCycleTests(TestCase):
+    def test_basic(self):
+        expected = [
+            (0, 'a'), (0, 'b'), (0, 'c'),
+            (1, 'a'), (1, 'b'), (1, 'c'),
+            (2, 'a'), (2, 'b'), (2, 'c'),
+        ]
+        for actual in [
+            mi.take(9, mi.count_cycle('abc')),  # n=None
+            list(mi.count_cycle('abc', 3)),  # n=3
+        ]:
+            self.assertEqual(actual, expected)
+
+    def test_empty(self):
+        self.assertEqual(list(mi.count_cycle('')), [])
+        self.assertEqual(list(mi.count_cycle('', 2)), [])
+
+    def test_negative(self):
+        self.assertEqual(list(mi.count_cycle('abc', -3)), [])
+
+
+class LocateTests(TestCase):
+    def test_default_pred(self):
+        iterable = [0, 1, 1, 0, 1, 0, 0]
+        actual = list(mi.locate(iterable))
+        expected = [1, 2, 4]
+        self.assertEqual(actual, expected)
+
+    def test_no_matches(self):
+        iterable = [0, 0, 0]
+        actual = list(mi.locate(iterable))
+        expected = []
+        self.assertEqual(actual, expected)
+
+    def test_custom_pred(self):
+        iterable = ['0', 1, 1, '0', 1, '0', '0']
+        pred = lambda x: x == '0'
+        actual = list(mi.locate(iterable, pred))
+        expected = [0, 3, 5, 6]
+        self.assertEqual(actual, expected)
+
+    def test_window_size(self):
+        iterable = ['0', 1, 1, '0', 1, '0', '0']
+        pred = lambda *args: args == ('0', 1)
+        actual = list(mi.locate(iterable, pred, window_size=2))
+        expected = [0, 3]
+        self.assertEqual(actual, expected)
+
+    def test_window_size_large(self):
+        iterable = [1, 2, 3, 4]
+        pred = lambda a, b, c, d, e: True
+        actual = list(mi.locate(iterable, pred, window_size=5))
+        expected = [0]
+        self.assertEqual(actual, expected)
+
+    def test_window_size_zero(self):
+        iterable = [1, 2, 3, 4]
+        pred = lambda: True
+        with self.assertRaises(ValueError):
+            list(mi.locate(iterable, pred, window_size=0))
+
+
+class StripFunctionTests(TestCase):
+    def test_hashable(self):
+        iterable = list('www.example.com')
+        pred = lambda x: x in set('cmowz.')
+
+        self.assertEqual(list(mi.lstrip(iterable, pred)), list('example.com'))
+        self.assertEqual(list(mi.rstrip(iterable, pred)), list('www.example'))
+        self.assertEqual(list(mi.strip(iterable, pred)), list('example'))
+
+    def test_not_hashable(self):
+        iterable = [
+            list('http://'), list('www'), list('.example'), list('.com')
+        ]
+        pred = lambda x: x in [list('http://'), list('www'), list('.com')]
+
+        self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[2:])
+        self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:3])
+        self.assertEqual(list(mi.strip(iterable, pred)), iterable[2: 3])
+
+    def test_math(self):
+        iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2]
+        pred = lambda x: x <= 2
+
+        self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[3:])
+        self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:-3])
+        self.assertEqual(list(mi.strip(iterable, pred)), iterable[3:-3])
+
+
+class IsliceExtendedTests(TestCase):
+    def test_all(self):
+        iterable = ['0', '1', '2', '3', '4', '5']
+        indexes = list(range(-4, len(iterable) + 4)) + [None]
+        steps = [1, 2, 3, 4, -1, -2, -3, 4]
+        for slice_args in product(indexes, indexes, steps):
+            try:
+                actual = list(mi.islice_extended(iterable, *slice_args))
+            except Exception as e:
+                self.fail((slice_args, e))
+
+            expected = iterable[slice(*slice_args)]
+            self.assertEqual(actual, expected, slice_args)
+
+    def test_zero_step(self):
+        with self.assertRaises(ValueError):
+            list(mi.islice_extended([1, 2, 3], 0, 1, 0))
+
+
+class ConsecutiveGroupsTest(TestCase):
+    def test_numbers(self):
+        iterable = [-10, -8, -7, -6, 1, 2, 4, 5, -1, 7]
+        actual = [list(g) for g in mi.consecutive_groups(iterable)]
+        expected = [[-10], [-8, -7, -6], [1, 2], [4, 5], [-1], [7]]
+        self.assertEqual(actual, expected)
+
+    def test_custom_ordering(self):
+        iterable = ['1', '10', '11', '20', '21', '22', '30', '31']
+        ordering = lambda x: int(x)
+        actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)]
+        expected = [['1'], ['10', '11'], ['20', '21', '22'], ['30', '31']]
+        self.assertEqual(actual, expected)
+
+    def test_exotic_ordering(self):
+        iterable = [
+            ('a', 'b', 'c', 'd'),
+            ('a', 'c', 'b', 'd'),
+            ('a', 'c', 'd', 'b'),
+            ('a', 'd', 'b', 'c'),
+            ('d', 'b', 'c', 'a'),
+            ('d', 'c', 'a', 'b'),
+        ]
+        ordering = list(permutations('abcd')).index
+        actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)]
+        expected = [
+            [('a', 'b', 'c', 'd')],
+            [('a', 'c', 'b', 'd'), ('a', 'c', 'd', 'b'), ('a', 'd', 'b', 'c')],
+            [('d', 'b', 'c', 'a'), ('d', 'c', 'a', 'b')],
+        ]
+        self.assertEqual(actual, expected)
+
+
+class DifferenceTest(TestCase):
+    def test_normal(self):
+        iterable = [10, 20, 30, 40, 50]
+        actual = list(mi.difference(iterable))
+        expected = [10, 10, 10, 10, 10]
+        self.assertEqual(actual, expected)
+
+    def test_custom(self):
+        iterable = [10, 20, 30, 40, 50]
+        actual = list(mi.difference(iterable, add))
+        expected = [10, 30, 50, 70, 90]
+        self.assertEqual(actual, expected)
+
+    def test_roundtrip(self):
+        original = list(range(100))
+        accumulated = mi.accumulate(original)
+        actual = list(mi.difference(accumulated))
+        self.assertEqual(actual, original)
+
+    def test_one(self):
+        self.assertEqual(list(mi.difference([0])), [0])
+
+    def test_empty(self):
+        self.assertEqual(list(mi.difference([])), [])
+
+
+class SeekableTest(TestCase):
+    def test_exhaustion_reset(self):
+        iterable = [str(n) for n in range(10)]
+
+        s = mi.seekable(iterable)
+        self.assertEqual(list(s), iterable)  # Normal iteration
+        self.assertEqual(list(s), [])  # Iterable is exhausted
+
+        s.seek(0)
+        self.assertEqual(list(s), iterable)  # Back in action
+
+    def test_partial_reset(self):
+        iterable = [str(n) for n in range(10)]
+
+        s = mi.seekable(iterable)
+        self.assertEqual(mi.take(5, s), iterable[:5])  # Normal iteration
+
+        s.seek(1)
+        self.assertEqual(list(s), iterable[1:])  # Get the rest of the iterable
+
+    def test_forward(self):
+        iterable = [str(n) for n in range(10)]
+
+        s = mi.seekable(iterable)
+        self.assertEqual(mi.take(1, s), iterable[:1])  # Normal iteration
+
+        s.seek(3)  # Skip over index 2
+        self.assertEqual(list(s), iterable[3:])  # Result is similar to slicing
+
+        s.seek(0)  # Back to 0
+        self.assertEqual(list(s), iterable)  # No difference in result
+
+    def test_past_end(self):
+        iterable = [str(n) for n in range(10)]
+
+        s = mi.seekable(iterable)
+        self.assertEqual(mi.take(1, s), iterable[:1])  # Normal iteration
+
+        s.seek(20)
+        self.assertEqual(list(s), [])  # Iterable is exhausted
+
+        s.seek(0)  # Back to 0
+        self.assertEqual(list(s), iterable)  # No difference in result
+
+    def test_elements(self):
+        iterable = map(str, count())
+
+        s = mi.seekable(iterable)
+        mi.take(10, s)
+
+        elements = s.elements()
+        self.assertEqual(
+            [elements[i] for i in range(10)], [str(n) for n in range(10)]
+        )
+        self.assertEqual(len(elements), 10)
+
+        mi.take(10, s)
+        self.assertEqual(list(elements), [str(n) for n in range(20)])
+
+
+class SequenceViewTests(TestCase):
+    def test_init(self):
+        view = mi.SequenceView((1, 2, 3))
+        self.assertEqual(repr(view), "SequenceView((1, 2, 3))")
+        self.assertRaises(TypeError, lambda: mi.SequenceView({}))
+
+    def test_update(self):
+        seq = [1, 2, 3]
+        view = mi.SequenceView(seq)
+        self.assertEqual(len(view), 3)
+        self.assertEqual(repr(view), "SequenceView([1, 2, 3])")
+
+        seq.pop()
+        self.assertEqual(len(view), 2)
+        self.assertEqual(repr(view), "SequenceView([1, 2])")
+
+    def test_indexing(self):
+        seq = ('a', 'b', 'c', 'd', 'e', 'f')
+        view = mi.SequenceView(seq)
+        for i in range(-len(seq), len(seq)):
+            self.assertEqual(view[i], seq[i])
+
+    def test_slicing(self):
+        seq = ('a', 'b', 'c', 'd', 'e', 'f')
+        view = mi.SequenceView(seq)
+        n = len(seq)
+        indexes = list(range(-n - 1, n + 1)) + [None]
+        steps = list(range(-n, n + 1))
+        steps.remove(0)
+        for slice_args in product(indexes, indexes, steps):
+            i = slice(*slice_args)
+            self.assertEqual(view[i], seq[i])
+
+    def test_abc_methods(self):
+        # collections.Sequence should provide all of this functionality
+        seq = ('a', 'b', 'c', 'd', 'e', 'f', 'f')
+        view = mi.SequenceView(seq)
+
+        # __contains__
+        self.assertIn('b', view)
+        self.assertNotIn('g', view)
+
+        # __iter__
+        self.assertEqual(list(iter(view)), list(seq))
+
+        # __reversed__
+        self.assertEqual(list(reversed(view)), list(reversed(seq)))
+
+        # index
+        self.assertEqual(view.index('b'), 1)
+
+        # count
+        self.assertEqual(seq.count('f'), 2)
+
+
+class RunLengthTest(TestCase):
+    def test_encode(self):
+        iterable = (int(str(n)[0]) for n in count(800))
+        actual = mi.take(4, mi.run_length.encode(iterable))
+        expected = [(8, 100), (9, 100), (1, 1000), (2, 1000)]
+        self.assertEqual(actual, expected)
+
+    def test_decode(self):
+        iterable = [('d', 4), ('c', 3), ('b', 2), ('a', 1)]
+        actual = ''.join(mi.run_length.decode(iterable))
+        expected = 'ddddcccbba'
+        self.assertEqual(actual, expected)
+
+
+class ExactlyNTests(TestCase):
+    """Tests for ``exactly_n()``"""
+
+    def test_true(self):
+        """Iterable has ``n`` ``True`` elements"""
+        self.assertTrue(mi.exactly_n([True, False, True], 2))
+        self.assertTrue(mi.exactly_n([1, 1, 1, 0], 3))
+        self.assertTrue(mi.exactly_n([False, False], 0))
+        self.assertTrue(mi.exactly_n(range(100), 10, lambda x: x < 10))
+
+    def test_false(self):
+        """Iterable does not have ``n`` ``True`` elements"""
+        self.assertFalse(mi.exactly_n([True, False, False], 2))
+        self.assertFalse(mi.exactly_n([True, True, False], 1))
+        self.assertFalse(mi.exactly_n([False], 1))
+        self.assertFalse(mi.exactly_n([True], -1))
+        self.assertFalse(mi.exactly_n(repeat(True), 100))
+
+    def test_empty(self):
+        """Return ``True`` if the iterable is empty and ``n`` is 0"""
+        self.assertTrue(mi.exactly_n([], 0))
+        self.assertFalse(mi.exactly_n([], 1))
+
+
+class AlwaysReversibleTests(TestCase):
+    """Tests for ``always_reversible()``"""
+
+    def test_regular_reversed(self):
+        self.assertEqual(list(reversed(range(10))),
+                         list(mi.always_reversible(range(10))))
+        self.assertEqual(list(reversed([1, 2, 3])),
+                         list(mi.always_reversible([1, 2, 3])))
+        self.assertEqual(reversed([1, 2, 3]).__class__,
+                         mi.always_reversible([1, 2, 3]).__class__)
+
+    def test_nonseq_reversed(self):
+        # Create a non-reversible generator from a sequence
+        with self.assertRaises(TypeError):
+            reversed(x for x in range(10))
+
+        self.assertEqual(list(reversed(range(10))),
+                         list(mi.always_reversible(x for x in range(10))))
+        self.assertEqual(list(reversed([1, 2, 3])),
+                         list(mi.always_reversible(x for x in [1, 2, 3])))
+        self.assertNotEqual(reversed((1, 2)).__class__,
+                            mi.always_reversible(x for x in (1, 2)).__class__)
+
+
+class CircularShiftsTests(TestCase):
+    def test_empty(self):
+        # empty iterable -> empty list
+        self.assertEqual(list(mi.circular_shifts([])), [])
+
+    def test_simple_circular_shifts(self):
+        # test the a simple iterator case
+        self.assertEqual(
+            mi.circular_shifts(range(4)),
+            [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)]
+        )
+
+    def test_duplicates(self):
+        # test non-distinct entries
+        self.assertEqual(
+            mi.circular_shifts([0, 1, 0, 1]),
+            [(0, 1, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 0, 1, 0)]
+        )
+
+
+class MakeDecoratorTests(TestCase):
+    def test_basic(self):
+        slicer = mi.make_decorator(islice)
+
+        @slicer(1, 10, 2)
+        def user_function(arg_1, arg_2, kwarg_1=None):
+            self.assertEqual(arg_1, 'arg_1')
+            self.assertEqual(arg_2, 'arg_2')
+            self.assertEqual(kwarg_1, 'kwarg_1')
+            return map(str, count())
+
+        it = user_function('arg_1', 'arg_2', kwarg_1='kwarg_1')
+        actual = list(it)
+        expected = ['1', '3', '5', '7', '9']
+        self.assertEqual(actual, expected)
+
+    def test_result_index(self):
+        def stringify(*args, **kwargs):
+            self.assertEqual(args[0], 'arg_0')
+            iterable = args[1]
+            self.assertEqual(args[2], 'arg_2')
+            self.assertEqual(kwargs['kwarg_1'], 'kwarg_1')
+            return map(str, iterable)
+
+        stringifier = mi.make_decorator(stringify, result_index=1)
+
+        @stringifier('arg_0', 'arg_2', kwarg_1='kwarg_1')
+        def user_function(n):
+            return count(n)
+
+        it = user_function(1)
+        actual = mi.take(5, it)
+        expected = ['1', '2', '3', '4', '5']
+        self.assertEqual(actual, expected)
+
+    def test_wrap_class(self):
+        seeker = mi.make_decorator(mi.seekable)
+
+        @seeker()
+        def user_function(n):
+            return map(str, range(n))
+
+        it = user_function(5)
+        self.assertEqual(list(it), ['0', '1', '2', '3', '4'])
+
+        it.seek(0)
+        self.assertEqual(list(it), ['0', '1', '2', '3', '4'])
+
+
+class MapReduceTests(TestCase):
+    def test_default(self):
+        iterable = (str(x) for x in range(5))
+        keyfunc = lambda x: int(x) // 2
+        actual = sorted(mi.map_reduce(iterable, keyfunc).items())
+        expected = [(0, ['0', '1']), (1, ['2', '3']), (2, ['4'])]
+        self.assertEqual(actual, expected)
+
+    def test_valuefunc(self):
+        iterable = (str(x) for x in range(5))
+        keyfunc = lambda x: int(x) // 2
+        valuefunc = int
+        actual = sorted(mi.map_reduce(iterable, keyfunc, valuefunc).items())
+        expected = [(0, [0, 1]), (1, [2, 3]), (2, [4])]
+        self.assertEqual(actual, expected)
+
+    def test_reducefunc(self):
+        iterable = (str(x) for x in range(5))
+        keyfunc = lambda x: int(x) // 2
+        valuefunc = int
+        reducefunc = lambda value_list: reduce(mul, value_list, 1)
+        actual = sorted(
+            mi.map_reduce(iterable, keyfunc, valuefunc, reducefunc).items()
+        )
+        expected = [(0, 0), (1, 6), (2, 4)]
+        self.assertEqual(actual, expected)
+
+    def test_ret(self):
+        d = mi.map_reduce([1, 0, 2, 0, 1, 0], bool)
+        self.assertEqual(d, {False: [0, 0, 0], True: [1, 2, 1]})
+        self.assertRaises(KeyError, lambda: d[None].append(1))
+
+
+class RlocateTests(TestCase):
+    def test_default_pred(self):
+        iterable = [0, 1, 1, 0, 1, 0, 0]
+        for it in (iterable[:], iter(iterable)):
+            actual = list(mi.rlocate(it))
+            expected = [4, 2, 1]
+            self.assertEqual(actual, expected)
+
+    def test_no_matches(self):
+        iterable = [0, 0, 0]
+        for it in (iterable[:], iter(iterable)):
+            actual = list(mi.rlocate(it))
+            expected = []
+            self.assertEqual(actual, expected)
+
+    def test_custom_pred(self):
+        iterable = ['0', 1, 1, '0', 1, '0', '0']
+        pred = lambda x: x == '0'
+        for it in (iterable[:], iter(iterable)):
+            actual = list(mi.rlocate(it, pred))
+            expected = [6, 5, 3, 0]
+            self.assertEqual(actual, expected)
+
+    def test_efficient_reversal(self):
+        iterable = range(10 ** 10)  # Is efficiently reversible
+        target = 10 ** 10 - 2
+        pred = lambda x: x == target  # Find-able from the right
+        actual = next(mi.rlocate(iterable, pred))
+        self.assertEqual(actual, target)
+
+    def test_window_size(self):
+        iterable = ['0', 1, 1, '0', 1, '0', '0']
+        pred = lambda *args: args == ('0', 1)
+        for it in (iterable, iter(iterable)):
+            actual = list(mi.rlocate(it, pred, window_size=2))
+            expected = [3, 0]
+            self.assertEqual(actual, expected)
+
+    def test_window_size_large(self):
+        iterable = [1, 2, 3, 4]
+        pred = lambda a, b, c, d, e: True
+        for it in (iterable, iter(iterable)):
+            actual = list(mi.rlocate(iterable, pred, window_size=5))
+            expected = [0]
+            self.assertEqual(actual, expected)
+
+    def test_window_size_zero(self):
+        iterable = [1, 2, 3, 4]
+        pred = lambda: True
+        for it in (iterable, iter(iterable)):
+            with self.assertRaises(ValueError):
+                list(mi.locate(iterable, pred, window_size=0))
+
+
+class ReplaceTests(TestCase):
+    def test_basic(self):
+        iterable = range(10)
+        pred = lambda x: x % 2 == 0
+        substitutes = []
+        actual = list(mi.replace(iterable, pred, substitutes))
+        expected = [1, 3, 5, 7, 9]
+        self.assertEqual(actual, expected)
+
+    def test_count(self):
+        iterable = range(10)
+        pred = lambda x: x % 2 == 0
+        substitutes = []
+        actual = list(mi.replace(iterable, pred, substitutes, count=4))
+        expected = [1, 3, 5, 7, 8, 9]
+        self.assertEqual(actual, expected)
+
+    def test_window_size(self):
+        iterable = range(10)
+        pred = lambda *args: args == (0, 1, 2)
+        substitutes = []
+        actual = list(mi.replace(iterable, pred, substitutes, window_size=3))
+        expected = [3, 4, 5, 6, 7, 8, 9]
+        self.assertEqual(actual, expected)
+
+    def test_window_size_end(self):
+        iterable = range(10)
+        pred = lambda *args: args == (7, 8, 9)
+        substitutes = []
+        actual = list(mi.replace(iterable, pred, substitutes, window_size=3))
+        expected = [0, 1, 2, 3, 4, 5, 6]
+        self.assertEqual(actual, expected)
+
+    def test_window_size_count(self):
+        iterable = range(10)
+        pred = lambda *args: (args == (0, 1, 2)) or (args == (7, 8, 9))
+        substitutes = []
+        actual = list(
+            mi.replace(iterable, pred, substitutes, count=1, window_size=3)
+        )
+        expected = [3, 4, 5, 6, 7, 8, 9]
+        self.assertEqual(actual, expected)
+
+    def test_window_size_large(self):
+        iterable = range(4)
+        pred = lambda a, b, c, d, e: True
+        substitutes = [5, 6, 7]
+        actual = list(mi.replace(iterable, pred, substitutes, window_size=5))
+        expected = [5, 6, 7]
+        self.assertEqual(actual, expected)
+
+    def test_window_size_zero(self):
+        iterable = range(10)
+        pred = lambda *args: True
+        substitutes = []
+        with self.assertRaises(ValueError):
+            list(mi.replace(iterable, pred, substitutes, window_size=0))
+
+    def test_iterable_substitutes(self):
+        iterable = range(5)
+        pred = lambda x: x % 2 == 0
+        substitutes = iter('__')
+        actual = list(mi.replace(iterable, pred, substitutes))
+        expected = ['_', '_', 1, '_', '_', 3, '_', '_']
+        self.assertEqual(actual, expected)
diff --git a/libraries/more_itertools/tests/test_recipes.py b/libraries/more_itertools/tests/test_recipes.py
new file mode 100644
index 00000000..98981fe8
--- /dev/null
+++ b/libraries/more_itertools/tests/test_recipes.py
@@ -0,0 +1,616 @@
+from doctest import DocTestSuite
+from unittest import TestCase
+
+from itertools import combinations
+from six.moves import range
+
+import more_itertools as mi
+
+
+def load_tests(loader, tests, ignore):
+    # Add the doctests
+    tests.addTests(DocTestSuite('more_itertools.recipes'))
+    return tests
+
+
+class AccumulateTests(TestCase):
+    """Tests for ``accumulate()``"""
+
+    def test_empty(self):
+        """Test that an empty input returns an empty output"""
+        self.assertEqual(list(mi.accumulate([])), [])
+
+    def test_default(self):
+        """Test accumulate with the default function (addition)"""
+        self.assertEqual(list(mi.accumulate([1, 2, 3])), [1, 3, 6])
+
+    def test_bogus_function(self):
+        """Test accumulate with an invalid function"""
+        with self.assertRaises(TypeError):
+            list(mi.accumulate([1, 2, 3], func=lambda x: x))
+
+    def test_custom_function(self):
+        """Test accumulate with a custom function"""
+        self.assertEqual(
+            list(mi.accumulate((1, 2, 3, 2, 1), func=max)), [1, 2, 3, 3, 3]
+        )
+
+
+class TakeTests(TestCase):
+    """Tests for ``take()``"""
+
+    def test_simple_take(self):
+        """Test basic usage"""
+        t = mi.take(5, range(10))
+        self.assertEqual(t, [0, 1, 2, 3, 4])
+
+    def test_null_take(self):
+        """Check the null case"""
+        t = mi.take(0, range(10))
+        self.assertEqual(t, [])
+
+    def test_negative_take(self):
+        """Make sure taking negative items results in a ValueError"""
+        self.assertRaises(ValueError, lambda: mi.take(-3, range(10)))
+
+    def test_take_too_much(self):
+        """Taking more than an iterator has remaining should return what the
+        iterator has remaining.
+
+        """
+        t = mi.take(10, range(5))
+        self.assertEqual(t, [0, 1, 2, 3, 4])
+
+
+class TabulateTests(TestCase):
+    """Tests for ``tabulate()``"""
+
+    def test_simple_tabulate(self):
+        """Test the happy path"""
+        t = mi.tabulate(lambda x: x)
+        f = tuple([next(t) for _ in range(3)])
+        self.assertEqual(f, (0, 1, 2))
+
+    def test_count(self):
+        """Ensure tabulate accepts specific count"""
+        t = mi.tabulate(lambda x: 2 * x, -1)
+        f = (next(t), next(t), next(t))
+        self.assertEqual(f, (-2, 0, 2))
+
+
+class TailTests(TestCase):
+    """Tests for ``tail()``"""
+
+    def test_greater(self):
+        """Length of iterable is greather than requested tail"""
+        self.assertEqual(list(mi.tail(3, 'ABCDEFG')), ['E', 'F', 'G'])
+
+    def test_equal(self):
+        """Length of iterable is equal to the requested tail"""
+        self.assertEqual(
+            list(mi.tail(7, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G']
+        )
+
+    def test_less(self):
+        """Length of iterable is less than requested tail"""
+        self.assertEqual(
+            list(mi.tail(8, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G']
+        )
+
+
+class ConsumeTests(TestCase):
+    """Tests for ``consume()``"""
+
+    def test_sanity(self):
+        """Test basic functionality"""
+        r = (x for x in range(10))
+        mi.consume(r, 3)
+        self.assertEqual(3, next(r))
+
+    def test_null_consume(self):
+        """Check the null case"""
+        r = (x for x in range(10))
+        mi.consume(r, 0)
+        self.assertEqual(0, next(r))
+
+    def test_negative_consume(self):
+        """Check that negative consumsion throws an error"""
+        r = (x for x in range(10))
+        self.assertRaises(ValueError, lambda: mi.consume(r, -1))
+
+    def test_total_consume(self):
+        """Check that iterator is totally consumed by default"""
+        r = (x for x in range(10))
+        mi.consume(r)
+        self.assertRaises(StopIteration, lambda: next(r))
+
+
+class NthTests(TestCase):
+    """Tests for ``nth()``"""
+
+    def test_basic(self):
+        """Make sure the nth item is returned"""
+        l = range(10)
+        for i, v in enumerate(l):
+            self.assertEqual(mi.nth(l, i), v)
+
+    def test_default(self):
+        """Ensure a default value is returned when nth item not found"""
+        l = range(3)
+        self.assertEqual(mi.nth(l, 100, "zebra"), "zebra")
+
+    def test_negative_item_raises(self):
+        """Ensure asking for a negative item raises an exception"""
+        self.assertRaises(ValueError, lambda: mi.nth(range(10), -3))
+
+
+class AllEqualTests(TestCase):
+    """Tests for ``all_equal()``"""
+
+    def test_true(self):
+        """Everything is equal"""
+        self.assertTrue(mi.all_equal('aaaaaa'))
+        self.assertTrue(mi.all_equal([0, 0, 0, 0]))
+
+    def test_false(self):
+        """Not everything is equal"""
+        self.assertFalse(mi.all_equal('aaaaab'))
+        self.assertFalse(mi.all_equal([0, 0, 0, 1]))
+
+    def test_tricky(self):
+        """Not everything is identical, but everything is equal"""
+        items = [1, complex(1, 0), 1.0]
+        self.assertTrue(mi.all_equal(items))
+
+    def test_empty(self):
+        """Return True if the iterable is empty"""
+        self.assertTrue(mi.all_equal(''))
+        self.assertTrue(mi.all_equal([]))
+
+    def test_one(self):
+        """Return True if the iterable is singular"""
+        self.assertTrue(mi.all_equal('0'))
+        self.assertTrue(mi.all_equal([0]))
+
+
+class QuantifyTests(TestCase):
+    """Tests for ``quantify()``"""
+
+    def test_happy_path(self):
+        """Make sure True count is returned"""
+        q = [True, False, True]
+        self.assertEqual(mi.quantify(q), 2)
+
+    def test_custom_predicate(self):
+        """Ensure non-default predicates return as expected"""
+        q = range(10)
+        self.assertEqual(mi.quantify(q, lambda x: x % 2 == 0), 5)
+
+
+class PadnoneTests(TestCase):
+    """Tests for ``padnone()``"""
+
+    def test_happy_path(self):
+        """wrapper iterator should return None indefinitely"""
+        r = range(2)
+        p = mi.padnone(r)
+        self.assertEqual([0, 1, None, None], [next(p) for _ in range(4)])
+
+
+class NcyclesTests(TestCase):
+    """Tests for ``nyclces()``"""
+
+    def test_happy_path(self):
+        """cycle a sequence three times"""
+        r = ["a", "b", "c"]
+        n = mi.ncycles(r, 3)
+        self.assertEqual(
+            ["a", "b", "c", "a", "b", "c", "a", "b", "c"],
+            list(n)
+        )
+
+    def test_null_case(self):
+        """asking for 0 cycles should return an empty iterator"""
+        n = mi.ncycles(range(100), 0)
+        self.assertRaises(StopIteration, lambda: next(n))
+
+    def test_pathalogical_case(self):
+        """asking for negative cycles should return an empty iterator"""
+        n = mi.ncycles(range(100), -10)
+        self.assertRaises(StopIteration, lambda: next(n))
+
+
+class DotproductTests(TestCase):
+    """Tests for ``dotproduct()``'"""
+
+    def test_happy_path(self):
+        """simple dotproduct example"""
+        self.assertEqual(400, mi.dotproduct([10, 10], [20, 20]))
+
+
+class FlattenTests(TestCase):
+    """Tests for ``flatten()``"""
+
+    def test_basic_usage(self):
+        """ensure list of lists is flattened one level"""
+        f = [[0, 1, 2], [3, 4, 5]]
+        self.assertEqual(list(range(6)), list(mi.flatten(f)))
+
+    def test_single_level(self):
+        """ensure list of lists is flattened only one level"""
+        f = [[0, [1, 2]], [[3, 4], 5]]
+        self.assertEqual([0, [1, 2], [3, 4], 5], list(mi.flatten(f)))
+
+
+class RepeatfuncTests(TestCase):
+    """Tests for ``repeatfunc()``"""
+
+    def test_simple_repeat(self):
+        """test simple repeated functions"""
+        r = mi.repeatfunc(lambda: 5)
+        self.assertEqual([5, 5, 5, 5, 5], [next(r) for _ in range(5)])
+
+    def test_finite_repeat(self):
+        """ensure limited repeat when times is provided"""
+        r = mi.repeatfunc(lambda: 5, times=5)
+        self.assertEqual([5, 5, 5, 5, 5], list(r))
+
+    def test_added_arguments(self):
+        """ensure arguments are applied to the function"""
+        r = mi.repeatfunc(lambda x: x, 2, 3)
+        self.assertEqual([3, 3], list(r))
+
+    def test_null_times(self):
+        """repeat 0 should return an empty iterator"""
+        r = mi.repeatfunc(range, 0, 3)
+        self.assertRaises(StopIteration, lambda: next(r))
+
+
+class PairwiseTests(TestCase):
+    """Tests for ``pairwise()``"""
+
+    def test_base_case(self):
+        """ensure an iterable will return pairwise"""
+        p = mi.pairwise([1, 2, 3])
+        self.assertEqual([(1, 2), (2, 3)], list(p))
+
+    def test_short_case(self):
+        """ensure an empty iterator if there's not enough values to pair"""
+        p = mi.pairwise("a")
+        self.assertRaises(StopIteration, lambda: next(p))
+
+
+class GrouperTests(TestCase):
+    """Tests for ``grouper()``"""
+
+    def test_even(self):
+        """Test when group size divides evenly into the length of
+        the iterable.
+
+        """
+        self.assertEqual(
+            list(mi.grouper(3, 'ABCDEF')), [('A', 'B', 'C'), ('D', 'E', 'F')]
+        )
+
+    def test_odd(self):
+        """Test when group size does not divide evenly into the length of the
+        iterable.
+
+        """
+        self.assertEqual(
+            list(mi.grouper(3, 'ABCDE')), [('A', 'B', 'C'), ('D', 'E', None)]
+        )
+
+    def test_fill_value(self):
+        """Test that the fill value is used to pad the final group"""
+        self.assertEqual(
+            list(mi.grouper(3, 'ABCDE', 'x')),
+            [('A', 'B', 'C'), ('D', 'E', 'x')]
+        )
+
+
+class RoundrobinTests(TestCase):
+    """Tests for ``roundrobin()``"""
+
+    def test_even_groups(self):
+        """Ensure ordered output from evenly populated iterables"""
+        self.assertEqual(
+            list(mi.roundrobin('ABC', [1, 2, 3], range(3))),
+            ['A', 1, 0, 'B', 2, 1, 'C', 3, 2]
+        )
+
+    def test_uneven_groups(self):
+        """Ensure ordered output from unevenly populated iterables"""
+        self.assertEqual(
+            list(mi.roundrobin('ABCD', [1, 2], range(0))),
+            ['A', 1, 'B', 2, 'C', 'D']
+        )
+
+
+class PartitionTests(TestCase):
+    """Tests for ``partition()``"""
+
+    def test_bool(self):
+        """Test when pred() returns a boolean"""
+        lesser, greater = mi.partition(lambda x: x > 5, range(10))
+        self.assertEqual(list(lesser), [0, 1, 2, 3, 4, 5])
+        self.assertEqual(list(greater), [6, 7, 8, 9])
+
+    def test_arbitrary(self):
+        """Test when pred() returns an integer"""
+        divisibles, remainders = mi.partition(lambda x: x % 3, range(10))
+        self.assertEqual(list(divisibles), [0, 3, 6, 9])
+        self.assertEqual(list(remainders), [1, 2, 4, 5, 7, 8])
+
+
+class PowersetTests(TestCase):
+    """Tests for ``powerset()``"""
+
+    def test_combinatorics(self):
+        """Ensure a proper enumeration"""
+        p = mi.powerset([1, 2, 3])
+        self.assertEqual(
+            list(p),
+            [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
+        )
+
+
+class UniqueEverseenTests(TestCase):
+    """Tests for ``unique_everseen()``"""
+
+    def test_everseen(self):
+        """ensure duplicate elements are ignored"""
+        u = mi.unique_everseen('AAAABBBBCCDAABBB')
+        self.assertEqual(
+            ['A', 'B', 'C', 'D'],
+            list(u)
+        )
+
+    def test_custom_key(self):
+        """ensure the custom key comparison works"""
+        u = mi.unique_everseen('aAbACCc', key=str.lower)
+        self.assertEqual(list('abC'), list(u))
+
+    def test_unhashable(self):
+        """ensure things work for unhashable items"""
+        iterable = ['a', [1, 2, 3], [1, 2, 3], 'a']
+        u = mi.unique_everseen(iterable)
+        self.assertEqual(list(u), ['a', [1, 2, 3]])
+
+    def test_unhashable_key(self):
+        """ensure things work for unhashable items with a custom key"""
+        iterable = ['a', [1, 2, 3], [1, 2, 3], 'a']
+        u = mi.unique_everseen(iterable, key=lambda x: x)
+        self.assertEqual(list(u), ['a', [1, 2, 3]])
+
+
+class UniqueJustseenTests(TestCase):
+    """Tests for ``unique_justseen()``"""
+
+    def test_justseen(self):
+        """ensure only last item is remembered"""
+        u = mi.unique_justseen('AAAABBBCCDABB')
+        self.assertEqual(list('ABCDAB'), list(u))
+
+    def test_custom_key(self):
+        """ensure the custom key comparison works"""
+        u = mi.unique_justseen('AABCcAD', str.lower)
+        self.assertEqual(list('ABCAD'), list(u))
+
+
+class IterExceptTests(TestCase):
+    """Tests for ``iter_except()``"""
+
+    def test_exact_exception(self):
+        """ensure the exact specified exception is caught"""
+        l = [1, 2, 3]
+        i = mi.iter_except(l.pop, IndexError)
+        self.assertEqual(list(i), [3, 2, 1])
+
+    def test_generic_exception(self):
+        """ensure the generic exception can be caught"""
+        l = [1, 2]
+        i = mi.iter_except(l.pop, Exception)
+        self.assertEqual(list(i), [2, 1])
+
+    def test_uncaught_exception_is_raised(self):
+        """ensure a non-specified exception is raised"""
+        l = [1, 2, 3]
+        i = mi.iter_except(l.pop, KeyError)
+        self.assertRaises(IndexError, lambda: list(i))
+
+    def test_first(self):
+        """ensure first is run before the function"""
+        l = [1, 2, 3]
+        f = lambda: 25
+        i = mi.iter_except(l.pop, IndexError, f)
+        self.assertEqual(list(i), [25, 3, 2, 1])
+
+
+class FirstTrueTests(TestCase):
+    """Tests for ``first_true()``"""
+
+    def test_something_true(self):
+        """Test with no keywords"""
+        self.assertEqual(mi.first_true(range(10)), 1)
+
+    def test_nothing_true(self):
+        """Test default return value."""
+        self.assertEqual(mi.first_true([0, 0, 0]), False)
+
+    def test_default(self):
+        """Test with a default keyword"""
+        self.assertEqual(mi.first_true([0, 0, 0], default='!'), '!')
+
+    def test_pred(self):
+        """Test with a custom predicate"""
+        self.assertEqual(
+            mi.first_true([2, 4, 6], pred=lambda x: x % 3 == 0), 6
+        )
+
+
+class RandomProductTests(TestCase):
+    """Tests for ``random_product()``
+
+    Since random.choice() has different results with the same seed across
+    python versions 2.x and 3.x, these tests use highly probably events to
+    create predictable outcomes across platforms.
+    """
+
+    def test_simple_lists(self):
+        """Ensure that one item is chosen from each list in each pair.
+        Also ensure that each item from each list eventually appears in
+        the chosen combinations.
+
+        Odds are roughly 1 in 7.1 * 10e16 that one item from either list will
+        not be chosen after 100 samplings of one item from each list. Just to
+        be safe, better use a known random seed, too.
+
+        """
+        nums = [1, 2, 3]
+        lets = ['a', 'b', 'c']
+        n, m = zip(*[mi.random_product(nums, lets) for _ in range(100)])
+        n, m = set(n), set(m)
+        self.assertEqual(n, set(nums))
+        self.assertEqual(m, set(lets))
+        self.assertEqual(len(n), len(nums))
+        self.assertEqual(len(m), len(lets))
+
+    def test_list_with_repeat(self):
+        """ensure multiple items are chosen, and that they appear to be chosen
+        from one list then the next, in proper order.
+
+        """
+        nums = [1, 2, 3]
+        lets = ['a', 'b', 'c']
+        r = list(mi.random_product(nums, lets, repeat=100))
+        self.assertEqual(2 * 100, len(r))
+        n, m = set(r[::2]), set(r[1::2])
+        self.assertEqual(n, set(nums))
+        self.assertEqual(m, set(lets))
+        self.assertEqual(len(n), len(nums))
+        self.assertEqual(len(m), len(lets))
+
+
+class RandomPermutationTests(TestCase):
+    """Tests for ``random_permutation()``"""
+
+    def test_full_permutation(self):
+        """ensure every item from the iterable is returned in a new ordering
+
+        15 elements have a 1 in 1.3 * 10e12 of appearing in sorted order, so
+        we fix a seed value just to be sure.
+
+        """
+        i = range(15)
+        r = mi.random_permutation(i)
+        self.assertEqual(set(i), set(r))
+        if i == r:
+            raise AssertionError("Values were not permuted")
+
+    def test_partial_permutation(self):
+        """ensure all returned items are from the iterable, that the returned
+        permutation is of the desired length, and that all items eventually
+        get returned.
+
+        Sampling 100 permutations of length 5 from a set of 15 leaves a
+        (2/3)^100 chance that an item will not be chosen. Multiplied by 15
+        items, there is a 1 in 2.6e16 chance that at least 1 item will not
+        show up in the resulting output. Using a random seed will fix that.
+
+        """
+        items = range(15)
+        item_set = set(items)
+        all_items = set()
+        for _ in range(100):
+            permutation = mi.random_permutation(items, 5)
+            self.assertEqual(len(permutation), 5)
+            permutation_set = set(permutation)
+            self.assertLessEqual(permutation_set, item_set)
+            all_items |= permutation_set
+        self.assertEqual(all_items, item_set)
+
+
+class RandomCombinationTests(TestCase):
+    """Tests for ``random_combination()``"""
+
+    def test_psuedorandomness(self):
+        """ensure different subsets of the iterable get returned over many
+        samplings of random combinations"""
+        items = range(15)
+        all_items = set()
+        for _ in range(50):
+            combination = mi.random_combination(items, 5)
+            all_items |= set(combination)
+        self.assertEqual(all_items, set(items))
+
+    def test_no_replacement(self):
+        """ensure that elements are sampled without replacement"""
+        items = range(15)
+        for _ in range(50):
+            combination = mi.random_combination(items, len(items))
+            self.assertEqual(len(combination), len(set(combination)))
+        self.assertRaises(
+            ValueError, lambda: mi.random_combination(items, len(items) + 1)
+        )
+
+
+class RandomCombinationWithReplacementTests(TestCase):
+    """Tests for ``random_combination_with_replacement()``"""
+
+    def test_replacement(self):
+        """ensure that elements are sampled with replacement"""
+        items = range(5)
+        combo = mi.random_combination_with_replacement(items, len(items) * 2)
+        self.assertEqual(2 * len(items), len(combo))
+        if len(set(combo)) == len(combo):
+            raise AssertionError("Combination contained no duplicates")
+
+    def test_pseudorandomness(self):
+        """ensure different subsets of the iterable get returned over many
+        samplings of random combinations"""
+        items = range(15)
+        all_items = set()
+        for _ in range(50):
+            combination = mi.random_combination_with_replacement(items, 5)
+            all_items |= set(combination)
+        self.assertEqual(all_items, set(items))
+
+
+class NthCombinationTests(TestCase):
+    def test_basic(self):
+        iterable = 'abcdefg'
+        r = 4
+        for index, expected in enumerate(combinations(iterable, r)):
+            actual = mi.nth_combination(iterable, r, index)
+            self.assertEqual(actual, expected)
+
+    def test_long(self):
+        actual = mi.nth_combination(range(180), 4, 2000000)
+        expected = (2, 12, 35, 126)
+        self.assertEqual(actual, expected)
+
+    def test_invalid_r(self):
+        for r in (-1, 3):
+            with self.assertRaises(ValueError):
+                mi.nth_combination([], r, 0)
+
+    def test_invalid_index(self):
+        with self.assertRaises(IndexError):
+            mi.nth_combination('abcdefg', 3, -36)
+
+
+class PrependTests(TestCase):
+    def test_basic(self):
+        value = 'a'
+        iterator = iter('bcdefg')
+        actual = list(mi.prepend(value, iterator))
+        expected = list('abcdefg')
+        self.assertEqual(actual, expected)
+
+    def test_multiple(self):
+        value = 'ab'
+        iterator = iter('cdefg')
+        actual = tuple(mi.prepend(value, iterator))
+        expected = ('ab',) + tuple('cdefg')
+        self.assertEqual(actual, expected)
diff --git a/libraries/portend.py b/libraries/portend.py
new file mode 100644
index 00000000..4c393806
--- /dev/null
+++ b/libraries/portend.py
@@ -0,0 +1,212 @@
+# -*- coding: utf-8 -*-
+
+"""
+A simple library for managing the availability of ports.
+"""
+
+from __future__ import print_function, division
+
+import time
+import socket
+import argparse
+import sys
+import itertools
+import contextlib
+import collections
+import platform
+
+from tempora import timing
+
+
+def client_host(server_host):
+	"""Return the host on which a client can connect to the given listener."""
+	if server_host == '0.0.0.0':
+		# 0.0.0.0 is INADDR_ANY, which should answer on localhost.
+		return '127.0.0.1'
+	if server_host in ('::', '::0', '::0.0.0.0'):
+		# :: is IN6ADDR_ANY, which should answer on localhost.
+		# ::0 and ::0.0.0.0 are non-canonical but common
+		# ways to write IN6ADDR_ANY.
+		return '::1'
+	return server_host
+
+
+class Checker(object):
+	def __init__(self, timeout=1.0):
+		self.timeout = timeout
+
+	def assert_free(self, host, port=None):
+		"""
+		Assert that the given addr is free
+		in that all attempts to connect fail within the timeout
+		or raise a PortNotFree exception.
+
+		>>> free_port = find_available_local_port()
+
+		>>> Checker().assert_free('localhost', free_port)
+		>>> Checker().assert_free('127.0.0.1', free_port)
+		>>> Checker().assert_free('::1', free_port)
+
+		Also accepts an addr tuple
+
+		>>> addr = '::1', free_port, 0, 0
+		>>> Checker().assert_free(addr)
+
+		Host might refer to a server bind address like '::', which
+		should use localhost to perform the check.
+
+		>>> Checker().assert_free('::', free_port)
+		"""
+		if port is None and isinstance(host, collections.Sequence):
+			host, port = host[:2]
+		if platform.system() == 'Windows':
+			host = client_host(host)
+		info = socket.getaddrinfo(
+			host, port, socket.AF_UNSPEC, socket.SOCK_STREAM,
+		)
+		list(itertools.starmap(self._connect, info))
+
+	def _connect(self, af, socktype, proto, canonname, sa):
+		s = socket.socket(af, socktype, proto)
+		# fail fast with a small timeout
+		s.settimeout(self.timeout)
+
+		with contextlib.closing(s):
+			try:
+				s.connect(sa)
+			except socket.error:
+				return
+
+		# the connect succeeded, so the port isn't free
+		port, host = sa[:2]
+		tmpl = "Port {port} is in use on {host}."
+		raise PortNotFree(tmpl.format(**locals()))
+
+
+class Timeout(IOError):
+	pass
+
+
+class PortNotFree(IOError):
+	pass
+
+
+def free(host, port, timeout=float('Inf')):
+	"""
+	Wait for the specified port to become free (dropping or rejecting
+	requests). Return when the port is free or raise a Timeout if timeout has
+	elapsed.
+
+	Timeout may be specified in seconds or as a timedelta.
+	If timeout is None or ∞, the routine will run indefinitely.
+
+	>>> free('localhost', find_available_local_port())
+	"""
+	if not host:
+		raise ValueError("Host values of '' or None are not allowed.")
+
+	timer = timing.Timer(timeout)
+
+	while not timer.expired():
+		try:
+			# Expect a free port, so use a small timeout
+			Checker(timeout=0.1).assert_free(host, port)
+			return
+		except PortNotFree:
+			# Politely wait.
+			time.sleep(0.1)
+
+	raise Timeout("Port {port} not free on {host}.".format(**locals()))
+wait_for_free_port = free
+
+
+def occupied(host, port, timeout=float('Inf')):
+	"""
+	Wait for the specified port to become occupied (accepting requests).
+	Return when the port is occupied or raise a Timeout if timeout has
+	elapsed.
+
+	Timeout may be specified in seconds or as a timedelta.
+	If timeout is None or ∞, the routine will run indefinitely.
+
+	>>> occupied('localhost', find_available_local_port(), .1) # doctest: +IGNORE_EXCEPTION_DETAIL
+	Traceback (most recent call last):
+	    ...
+	Timeout: Port ... not bound on localhost.
+	"""
+	if not host:
+		raise ValueError("Host values of '' or None are not allowed.")
+
+	timer = timing.Timer(timeout)
+
+	while not timer.expired():
+		try:
+			Checker(timeout=.5).assert_free(host, port)
+			# Politely wait
+			time.sleep(0.1)
+		except PortNotFree:
+			# port is occupied
+			return
+
+	raise Timeout("Port {port} not bound on {host}.".format(**locals()))
+wait_for_occupied_port = occupied
+
+
+def find_available_local_port():
+	"""
+	Find a free port on localhost.
+
+	>>> 0 < find_available_local_port() < 65536
+	True
+	"""
+	sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
+	addr = '', 0
+	sock.bind(addr)
+	addr, port = sock.getsockname()[:2]
+	sock.close()
+	return port
+
+
+class HostPort(str):
+	"""
+	A simple representation of a host/port pair as a string
+
+	>>> hp = HostPort('localhost:32768')
+
+	>>> hp.host
+	'localhost'
+
+	>>> hp.port
+	32768
+
+	>>> len(hp)
+	15
+	"""
+
+	@property
+	def host(self):
+		host, sep, port = self.partition(':')
+		return host
+
+	@property
+	def port(self):
+		host, sep, port = self.partition(':')
+		return int(port)
+
+
+def _main():
+	parser = argparse.ArgumentParser()
+	global_lookup = lambda key: globals()[key]
+	parser.add_argument('target', metavar='host:port', type=HostPort)
+	parser.add_argument('func', metavar='state', type=global_lookup)
+	parser.add_argument('-t', '--timeout', default=None, type=float)
+	args = parser.parse_args()
+	try:
+		args.func(args.target.host, args.target.port, timeout=args.timeout)
+	except Timeout as timeout:
+		print(timeout, file=sys.stderr)
+		raise SystemExit(1)
+
+
+if __name__ == '__main__':
+	_main()
diff --git a/libraries/tempora/__init__.py b/libraries/tempora/__init__.py
new file mode 100644
index 00000000..e0cdead0
--- /dev/null
+++ b/libraries/tempora/__init__.py
@@ -0,0 +1,505 @@
+# -*- coding: UTF-8 -*-
+
+"Objects and routines pertaining to date and time (tempora)"
+
+from __future__ import division, unicode_literals
+
+import datetime
+import time
+import re
+import numbers
+import functools
+
+import six
+
+__metaclass__ = type
+
+
+class Parser:
+	"""
+	Datetime parser: parses a date-time string using multiple possible
+	formats.
+
+	>>> p = Parser(('%H%M', '%H:%M'))
+	>>> tuple(p.parse('1319'))
+	(1900, 1, 1, 13, 19, 0, 0, 1, -1)
+	>>> dateParser = Parser(('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y'))
+	>>> tuple(dateParser.parse('2003-12-20'))
+	(2003, 12, 20, 0, 0, 0, 5, 354, -1)
+	>>> tuple(dateParser.parse('16-Dec-1994'))
+	(1994, 12, 16, 0, 0, 0, 4, 350, -1)
+	>>> tuple(dateParser.parse('5/19/2003'))
+	(2003, 5, 19, 0, 0, 0, 0, 139, -1)
+	>>> dtParser = Parser(('%Y-%m-%d %H:%M:%S', '%a %b %d %H:%M:%S %Y'))
+	>>> tuple(dtParser.parse('2003-12-20 19:13:26'))
+	(2003, 12, 20, 19, 13, 26, 5, 354, -1)
+	>>> tuple(dtParser.parse('Tue Jan 20 16:19:33 2004'))
+	(2004, 1, 20, 16, 19, 33, 1, 20, -1)
+
+	Be forewarned, a ValueError will be raised if more than one format
+	matches:
+
+	>>> Parser(('%H%M', '%H%M%S')).parse('732')
+	Traceback (most recent call last):
+		...
+	ValueError: More than one format string matched target 732.
+	"""
+
+	formats = ('%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%d-%b-%Y', '%d-%b-%y')
+	"some common default formats"
+
+	def __init__(self, formats=None):
+		if formats:
+			self.formats = formats
+
+	def parse(self, target):
+		self.target = target
+		results = tuple(filter(None, map(self._parse, self.formats)))
+		del self.target
+		if not results:
+			tmpl = "No format strings matched the target {target}."
+			raise ValueError(tmpl.format(**locals()))
+		if not len(results) == 1:
+			tmpl = "More than one format string matched target {target}."
+			raise ValueError(tmpl.format(**locals()))
+		return results[0]
+
+	def _parse(self, format):
+		try:
+			result = time.strptime(self.target, format)
+		except ValueError:
+			result = False
+		return result
+
+
+# some useful constants
+osc_per_year = 290091329207984000
+"""
+mean vernal equinox year expressed in oscillations of atomic cesium at the
+year 2000 (see http://webexhibits.org/calendars/timeline.html for more info).
+"""
+osc_per_second = 9192631770
+seconds_per_second = 1
+seconds_per_year = 31556940
+seconds_per_minute = 60
+minutes_per_hour = 60
+hours_per_day = 24
+seconds_per_hour = seconds_per_minute * minutes_per_hour
+seconds_per_day = seconds_per_hour * hours_per_day
+days_per_year = seconds_per_year / seconds_per_day
+thirty_days = datetime.timedelta(days=30)
+# these values provide useful averages
+six_months = datetime.timedelta(days=days_per_year / 2)
+seconds_per_month = seconds_per_year / 12
+hours_per_month = hours_per_day * days_per_year / 12
+
+
+def strftime(fmt, t):
+	"""A class to replace the strftime in datetime package or time module.
+	Identical to strftime behavior in those modules except supports any
+	year.
+	Also supports datetime.datetime times.
+	Also supports milliseconds using %s
+	Also supports microseconds using %u"""
+	if isinstance(t, (time.struct_time, tuple)):
+		t = datetime.datetime(*t[:6])
+	assert isinstance(t, (datetime.datetime, datetime.time, datetime.date))
+	try:
+		year = t.year
+		if year < 1900:
+			t = t.replace(year=1900)
+	except AttributeError:
+		year = 1900
+	subs = (
+		('%Y', '%04d' % year),
+		('%y', '%02d' % (year % 100)),
+		('%s', '%03d' % (t.microsecond // 1000)),
+		('%u', '%03d' % (t.microsecond % 1000))
+	)
+
+	def doSub(s, sub):
+		return s.replace(*sub)
+
+	def doSubs(s):
+		return functools.reduce(doSub, subs, s)
+
+	fmt = '%%'.join(map(doSubs, fmt.split('%%')))
+	return t.strftime(fmt)
+
+
+def strptime(s, fmt, tzinfo=None):
+	"""
+	A function to replace strptime in the time module.  Should behave
+	identically to the strptime function except it returns a datetime.datetime
+	object instead of a time.struct_time object.
+	Also takes an optional tzinfo parameter which is a time zone info object.
+	"""
+	res = time.strptime(s, fmt)
+	return datetime.datetime(tzinfo=tzinfo, *res[:6])
+
+
+class DatetimeConstructor:
+	"""
+	>>> cd = DatetimeConstructor.construct_datetime
+	>>> cd(datetime.datetime(2011,1,1))
+	datetime.datetime(2011, 1, 1, 0, 0)
+	"""
+	@classmethod
+	def construct_datetime(cls, *args, **kwargs):
+		"""Construct a datetime.datetime from a number of different time
+		types found in python and pythonwin"""
+		if len(args) == 1:
+			arg = args[0]
+			method = cls.__get_dt_constructor(
+				type(arg).__module__,
+				type(arg).__name__,
+			)
+			result = method(arg)
+			try:
+				result = result.replace(tzinfo=kwargs.pop('tzinfo'))
+			except KeyError:
+				pass
+			if kwargs:
+				first_key = kwargs.keys()[0]
+				tmpl = (
+					"{first_key} is an invalid keyword "
+					"argument for this function."
+				)
+				raise TypeError(tmpl.format(**locals()))
+		else:
+			result = datetime.datetime(*args, **kwargs)
+		return result
+
+	@classmethod
+	def __get_dt_constructor(cls, moduleName, name):
+		try:
+			method_name = '__dt_from_{moduleName}_{name}__'.format(**locals())
+			return getattr(cls, method_name)
+		except AttributeError:
+			tmpl = (
+				"No way to construct datetime.datetime from "
+				"{moduleName}.{name}"
+			)
+			raise TypeError(tmpl.format(**locals()))
+
+	@staticmethod
+	def __dt_from_datetime_datetime__(source):
+		dtattrs = (
+			'year', 'month', 'day', 'hour', 'minute', 'second',
+			'microsecond', 'tzinfo',
+		)
+		attrs = map(lambda a: getattr(source, a), dtattrs)
+		return datetime.datetime(*attrs)
+
+	@staticmethod
+	def __dt_from___builtin___time__(pyt):
+		"Construct a datetime.datetime from a pythonwin time"
+		fmtString = '%Y-%m-%d %H:%M:%S'
+		result = strptime(pyt.Format(fmtString), fmtString)
+		# get milliseconds and microseconds.  The only way to do this is
+		#  to use the __float__ attribute of the time, which is in days.
+		microseconds_per_day = seconds_per_day * 1000000
+		microseconds = float(pyt) * microseconds_per_day
+		microsecond = int(microseconds % 1000000)
+		result = result.replace(microsecond=microsecond)
+		return result
+
+	@staticmethod
+	def __dt_from_timestamp__(timestamp):
+		return datetime.datetime.utcfromtimestamp(timestamp)
+	__dt_from___builtin___float__ = __dt_from_timestamp__
+	__dt_from___builtin___long__ = __dt_from_timestamp__
+	__dt_from___builtin___int__ = __dt_from_timestamp__
+
+	@staticmethod
+	def __dt_from_time_struct_time__(s):
+		return datetime.datetime(*s[:6])
+
+
+def datetime_mod(dt, period, start=None):
+	"""
+	Find the time which is the specified date/time truncated to the time delta
+	relative to the start date/time.
+	By default, the start time is midnight of the same day as the specified
+	date/time.
+
+	>>> datetime_mod(datetime.datetime(2004, 1, 2, 3),
+	...     datetime.timedelta(days = 1.5),
+	...     start = datetime.datetime(2004, 1, 1))
+	datetime.datetime(2004, 1, 1, 0, 0)
+	>>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
+	...     datetime.timedelta(days = 1.5),
+	...     start = datetime.datetime(2004, 1, 1))
+	datetime.datetime(2004, 1, 2, 12, 0)
+	>>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
+	...     datetime.timedelta(days = 7),
+	...     start = datetime.datetime(2004, 1, 1))
+	datetime.datetime(2004, 1, 1, 0, 0)
+	>>> datetime_mod(datetime.datetime(2004, 1, 10, 13),
+	...     datetime.timedelta(days = 7),
+	...     start = datetime.datetime(2004, 1, 1))
+	datetime.datetime(2004, 1, 8, 0, 0)
+	"""
+	if start is None:
+		# use midnight of the same day
+		start = datetime.datetime.combine(dt.date(), datetime.time())
+	# calculate the difference between the specified time and the start date.
+	delta = dt - start
+
+	# now aggregate the delta and the period into microseconds
+	# Use microseconds because that's the highest precision of these time
+	# pieces.  Also, using microseconds ensures perfect precision (no floating
+	# point errors).
+	def get_time_delta_microseconds(td):
+		return (td.days * seconds_per_day + td.seconds) * 1000000 + td.microseconds
+	delta, period = map(get_time_delta_microseconds, (delta, period))
+	offset = datetime.timedelta(microseconds=delta % period)
+	# the result is the original specified time minus the offset
+	result = dt - offset
+	return result
+
+
+def datetime_round(dt, period, start=None):
+	"""
+	Find the nearest even period for the specified date/time.
+
+	>>> datetime_round(datetime.datetime(2004, 11, 13, 8, 11, 13),
+	...     datetime.timedelta(hours = 1))
+	datetime.datetime(2004, 11, 13, 8, 0)
+	>>> datetime_round(datetime.datetime(2004, 11, 13, 8, 31, 13),
+	...     datetime.timedelta(hours = 1))
+	datetime.datetime(2004, 11, 13, 9, 0)
+	>>> datetime_round(datetime.datetime(2004, 11, 13, 8, 30),
+	...     datetime.timedelta(hours = 1))
+	datetime.datetime(2004, 11, 13, 9, 0)
+	"""
+	result = datetime_mod(dt, period, start)
+	if abs(dt - result) >= period // 2:
+		result += period
+	return result
+
+
+def get_nearest_year_for_day(day):
+	"""
+	Returns the nearest year to now inferred from a Julian date.
+	"""
+	now = time.gmtime()
+	result = now.tm_year
+	# if the day is far greater than today, it must be from last year
+	if day - now.tm_yday > 365 // 2:
+		result -= 1
+	# if the day is far less than today, it must be for next year.
+	if now.tm_yday - day > 365 // 2:
+		result += 1
+	return result
+
+
+def gregorian_date(year, julian_day):
+	"""
+	Gregorian Date is defined as a year and a julian day (1-based
+	index into the days of the year).
+
+	>>> gregorian_date(2007, 15)
+	datetime.date(2007, 1, 15)
+	"""
+	result = datetime.date(year, 1, 1)
+	result += datetime.timedelta(days=julian_day - 1)
+	return result
+
+
+def get_period_seconds(period):
+	"""
+	return the number of seconds in the specified period
+
+	>>> get_period_seconds('day')
+	86400
+	>>> get_period_seconds(86400)
+	86400
+	>>> get_period_seconds(datetime.timedelta(hours=24))
+	86400
+	>>> get_period_seconds('day + os.system("rm -Rf *")')
+	Traceback (most recent call last):
+	...
+	ValueError: period not in (second, minute, hour, day, month, year)
+	"""
+	if isinstance(period, six.string_types):
+		try:
+			name = 'seconds_per_' + period.lower()
+			result = globals()[name]
+		except KeyError:
+			msg = "period not in (second, minute, hour, day, month, year)"
+			raise ValueError(msg)
+	elif isinstance(period, numbers.Number):
+		result = period
+	elif isinstance(period, datetime.timedelta):
+		result = period.days * get_period_seconds('day') + period.seconds
+	else:
+		raise TypeError('period must be a string or integer')
+	return result
+
+
+def get_date_format_string(period):
+	"""
+	For a given period (e.g. 'month', 'day', or some numeric interval
+	such as 3600 (in secs)), return the format string that can be
+	used with strftime to format that time to specify the times
+	across that interval, but no more detailed.
+	For example,
+
+	>>> get_date_format_string('month')
+	'%Y-%m'
+	>>> get_date_format_string(3600)
+	'%Y-%m-%d %H'
+	>>> get_date_format_string('hour')
+	'%Y-%m-%d %H'
+	>>> get_date_format_string(None)
+	Traceback (most recent call last):
+		...
+	TypeError: period must be a string or integer
+	>>> get_date_format_string('garbage')
+	Traceback (most recent call last):
+		...
+	ValueError: period not in (second, minute, hour, day, month, year)
+	"""
+	# handle the special case of 'month' which doesn't have
+	#  a static interval in seconds
+	if isinstance(period, six.string_types) and period.lower() == 'month':
+		return '%Y-%m'
+	file_period_secs = get_period_seconds(period)
+	format_pieces = ('%Y', '-%m-%d', ' %H', '-%M', '-%S')
+	seconds_per_second = 1
+	intervals = (
+		seconds_per_year,
+		seconds_per_day,
+		seconds_per_hour,
+		seconds_per_minute,
+		seconds_per_second,
+	)
+	mods = list(map(lambda interval: file_period_secs % interval, intervals))
+	format_pieces = format_pieces[: mods.index(0) + 1]
+	return ''.join(format_pieces)
+
+
+def divide_timedelta_float(td, divisor):
+	"""
+	Divide a timedelta by a float value
+
+	>>> one_day = datetime.timedelta(days=1)
+	>>> half_day = datetime.timedelta(days=.5)
+	>>> divide_timedelta_float(one_day, 2.0) == half_day
+	True
+	>>> divide_timedelta_float(one_day, 2) == half_day
+	True
+	"""
+	# td is comprised of days, seconds, microseconds
+	dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')]
+	dsm = map(lambda elem: elem / divisor, dsm)
+	return datetime.timedelta(*dsm)
+
+
+def calculate_prorated_values():
+	"""
+	A utility function to prompt for a rate (a string in units per
+	unit time), and return that same rate for various time periods.
+	"""
+	rate = six.moves.input("Enter the rate (3/hour, 50/month)> ")
+	res = re.match('(?P<value>[\d.]+)/(?P<period>\w+)$', rate).groupdict()
+	value = float(res['value'])
+	value_per_second = value / get_period_seconds(res['period'])
+	for period in ('minute', 'hour', 'day', 'month', 'year'):
+		period_value = value_per_second * get_period_seconds(period)
+		print("per {period}: {period_value}".format(**locals()))
+
+
+def parse_timedelta(str):
+	"""
+	Take a string representing a span of time and parse it to a time delta.
+	Accepts any string of comma-separated numbers each with a unit indicator.
+
+	>>> parse_timedelta('1 day')
+	datetime.timedelta(days=1)
+
+	>>> parse_timedelta('1 day, 30 seconds')
+	datetime.timedelta(days=1, seconds=30)
+
+	>>> parse_timedelta('47.32 days, 20 minutes, 15.4 milliseconds')
+	datetime.timedelta(days=47, seconds=28848, microseconds=15400)
+
+	Supports weeks, months, years
+
+	>>> parse_timedelta('1 week')
+	datetime.timedelta(days=7)
+
+	>>> parse_timedelta('1 year, 1 month')
+	datetime.timedelta(days=395, seconds=58685)
+
+	Note that months and years strict intervals, not aligned
+	to a calendar:
+
+	>>> now = datetime.datetime.now()
+	>>> later = now + parse_timedelta('1 year')
+	>>> later.replace(year=now.year) - now
+	datetime.timedelta(seconds=20940)
+	"""
+	deltas = (_parse_timedelta_part(part.strip()) for part in str.split(','))
+	return sum(deltas, datetime.timedelta())
+
+
+def _parse_timedelta_part(part):
+	match = re.match('(?P<value>[\d.]+) (?P<unit>\w+)', part)
+	if not match:
+		msg = "Unable to parse {part!r} as a time delta".format(**locals())
+		raise ValueError(msg)
+	unit = match.group('unit').lower()
+	if not unit.endswith('s'):
+		unit += 's'
+	value = float(match.group('value'))
+	if unit == 'months':
+		unit = 'years'
+		value = value / 12
+	if unit == 'years':
+		unit = 'days'
+		value = value * days_per_year
+	return datetime.timedelta(**{unit: value})
+
+
+def divide_timedelta(td1, td2):
+	"""
+	Get the ratio of two timedeltas
+
+	>>> one_day = datetime.timedelta(days=1)
+	>>> one_hour = datetime.timedelta(hours=1)
+	>>> divide_timedelta(one_hour, one_day) == 1 / 24
+	True
+	"""
+	try:
+		return td1 / td2
+	except TypeError:
+		# Python 3.2 gets division
+		# http://bugs.python.org/issue2706
+		return td1.total_seconds() / td2.total_seconds()
+
+
+def date_range(start=None, stop=None, step=None):
+	"""
+	Much like the built-in function range, but works with dates
+
+	>>> range_items = date_range(
+	...     datetime.datetime(2005,12,21),
+	...     datetime.datetime(2005,12,25),
+	... )
+	>>> my_range = tuple(range_items)
+	>>> datetime.datetime(2005,12,21) in my_range
+	True
+	>>> datetime.datetime(2005,12,22) in my_range
+	True
+	>>> datetime.datetime(2005,12,25) in my_range
+	False
+	"""
+	if step is None:
+		step = datetime.timedelta(days=1)
+	if start is None:
+		start = datetime.datetime.now()
+	while start < stop:
+		yield start
+		start += step
diff --git a/libraries/tempora/schedule.py b/libraries/tempora/schedule.py
new file mode 100644
index 00000000..1ad093b2
--- /dev/null
+++ b/libraries/tempora/schedule.py
@@ -0,0 +1,202 @@
+# -*- coding: utf-8 -*-
+
+"""
+Classes for calling functions a schedule.
+"""
+
+from __future__ import absolute_import
+
+import datetime
+import numbers
+import abc
+import bisect
+
+import pytz
+
+__metaclass__ = type
+
+
+def now():
+    """
+    Provide the current timezone-aware datetime.
+
+    A client may override this function to change the default behavior,
+    such as to use local time or timezone-naïve times.
+    """
+    return datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
+
+
+def from_timestamp(ts):
+    """
+    Convert a numeric timestamp to a timezone-aware datetime.
+
+    A client may override this function to change the default behavior,
+    such as to use local time or timezone-naïve times.
+    """
+    return datetime.datetime.utcfromtimestamp(ts).replace(tzinfo=pytz.utc)
+
+
+class DelayedCommand(datetime.datetime):
+    """
+    A command to be executed after some delay (seconds or timedelta).
+    """
+
+    @classmethod
+    def from_datetime(cls, other):
+        return cls(
+            other.year, other.month, other.day, other.hour,
+            other.minute, other.second, other.microsecond,
+            other.tzinfo,
+        )
+
+    @classmethod
+    def after(cls, delay, target):
+        if not isinstance(delay, datetime.timedelta):
+            delay = datetime.timedelta(seconds=delay)
+        due_time = now() + delay
+        cmd = cls.from_datetime(due_time)
+        cmd.delay = delay
+        cmd.target = target
+        return cmd
+
+    @staticmethod
+    def _from_timestamp(input):
+        """
+        If input is a real number, interpret it as a Unix timestamp
+        (seconds sinc Epoch in UTC) and return a timezone-aware
+        datetime object. Otherwise return input unchanged.
+        """
+        if not isinstance(input, numbers.Real):
+            return input
+        return from_timestamp(input)
+
+    @classmethod
+    def at_time(cls, at, target):
+        """
+        Construct a DelayedCommand to come due at `at`, where `at` may be
+        a datetime or timestamp.
+        """
+        at = cls._from_timestamp(at)
+        cmd = cls.from_datetime(at)
+        cmd.delay = at - now()
+        cmd.target = target
+        return cmd
+
+    def due(self):
+        return now() >= self
+
+
+class PeriodicCommand(DelayedCommand):
+    """
+    Like a delayed command, but expect this command to run every delay
+    seconds.
+    """
+    def _next_time(self):
+        """
+        Add delay to self, localized
+        """
+        return self._localize(self + self.delay)
+
+    @staticmethod
+    def _localize(dt):
+        """
+        Rely on pytz.localize to ensure new result honors DST.
+        """
+        try:
+            tz = dt.tzinfo
+            return tz.localize(dt.replace(tzinfo=None))
+        except AttributeError:
+            return dt
+
+    def next(self):
+        cmd = self.__class__.from_datetime(self._next_time())
+        cmd.delay = self.delay
+        cmd.target = self.target
+        return cmd
+
+    def __setattr__(self, key, value):
+        if key == 'delay' and not value > datetime.timedelta():
+            raise ValueError(
+                "A PeriodicCommand must have a positive, "
+                "non-zero delay."
+            )
+        super(PeriodicCommand, self).__setattr__(key, value)
+
+
+class PeriodicCommandFixedDelay(PeriodicCommand):
+    """
+    Like a periodic command, but don't calculate the delay based on
+    the current time. Instead use a fixed delay following the initial
+    run.
+    """
+
+    @classmethod
+    def at_time(cls, at, delay, target):
+        at = cls._from_timestamp(at)
+        cmd = cls.from_datetime(at)
+        if isinstance(delay, numbers.Number):
+            delay = datetime.timedelta(seconds=delay)
+        cmd.delay = delay
+        cmd.target = target
+        return cmd
+
+    @classmethod
+    def daily_at(cls, at, target):
+        """
+        Schedule a command to run at a specific time each day.
+        """
+        daily = datetime.timedelta(days=1)
+        # convert when to the next datetime matching this time
+        when = datetime.datetime.combine(datetime.date.today(), at)
+        if when < now():
+            when += daily
+        return cls.at_time(cls._localize(when), daily, target)
+
+
+class Scheduler:
+    """
+    A rudimentary abstract scheduler accepting DelayedCommands
+    and dispatching them on schedule.
+    """
+    def __init__(self):
+        self.queue = []
+
+    def add(self, command):
+        assert isinstance(command, DelayedCommand)
+        bisect.insort(self.queue, command)
+
+    def run_pending(self):
+        while self.queue:
+            command = self.queue[0]
+            if not command.due():
+                break
+            self.run(command)
+            if isinstance(command, PeriodicCommand):
+                self.add(command.next())
+            del self.queue[0]
+
+    @abc.abstractmethod
+    def run(self, command):
+        """
+        Run the command
+        """
+
+
+class InvokeScheduler(Scheduler):
+    """
+    Command targets are functions to be invoked on schedule.
+    """
+    def run(self, command):
+        command.target()
+
+
+class CallbackScheduler(Scheduler):
+    """
+    Command targets are passed to a dispatch callable on schedule.
+    """
+    def __init__(self, dispatch):
+        super(CallbackScheduler, self).__init__()
+        self.dispatch = dispatch
+
+    def run(self, command):
+        self.dispatch(command.target)
diff --git a/libraries/tempora/tests/test_schedule.py b/libraries/tempora/tests/test_schedule.py
new file mode 100644
index 00000000..38eb8dc9
--- /dev/null
+++ b/libraries/tempora/tests/test_schedule.py
@@ -0,0 +1,118 @@
+import time
+import random
+import datetime
+
+import pytest
+import pytz
+import freezegun
+
+from tempora import schedule
+
+__metaclass__ = type
+
+
+@pytest.fixture
+def naive_times(monkeypatch):
+	monkeypatch.setattr(
+		'irc.schedule.from_timestamp',
+		datetime.datetime.fromtimestamp)
+	monkeypatch.setattr('irc.schedule.now', datetime.datetime.now)
+
+
+do_nothing = type(None)
+try:
+	do_nothing()
+except TypeError:
+	# Python 2 compat
+	def do_nothing():
+		return None
+
+
+def test_delayed_command_order():
+	"""
+	delayed commands should be sorted by delay time
+	"""
+	delays = [random.randint(0, 99) for x in range(5)]
+	cmds = sorted([
+		schedule.DelayedCommand.after(delay, do_nothing)
+		for delay in delays
+	])
+	assert [c.delay.seconds for c in cmds] == sorted(delays)
+
+
+def test_periodic_command_delay():
+	"A PeriodicCommand must have a positive, non-zero delay."
+	with pytest.raises(ValueError) as exc_info:
+		schedule.PeriodicCommand.after(0, None)
+	assert str(exc_info.value) == test_periodic_command_delay.__doc__
+
+
+def test_periodic_command_fixed_delay():
+	"""
+	Test that we can construct a periodic command with a fixed initial
+	delay.
+	"""
+	fd = schedule.PeriodicCommandFixedDelay.at_time(
+		at=schedule.now(),
+		delay=datetime.timedelta(seconds=2),
+		target=lambda: None,
+	)
+	assert fd.due() is True
+	assert fd.next().due() is False
+
+
+class TestCommands:
+	def test_delayed_command_from_timestamp(self):
+		"""
+		Ensure a delayed command can be constructed from a timestamp.
+		"""
+		t = time.time()
+		schedule.DelayedCommand.at_time(t, do_nothing)
+
+	def test_command_at_noon(self):
+		"""
+		Create a periodic command that's run at noon every day.
+		"""
+		when = datetime.time(12, 0, tzinfo=pytz.utc)
+		cmd = schedule.PeriodicCommandFixedDelay.daily_at(when, target=None)
+		assert cmd.due() is False
+		next_cmd = cmd.next()
+		daily = datetime.timedelta(days=1)
+		day_from_now = schedule.now() + daily
+		two_days_from_now = day_from_now + daily
+		assert day_from_now < next_cmd < two_days_from_now
+
+
+class TestTimezones:
+	def test_alternate_timezone_west(self):
+		target_tz = pytz.timezone('US/Pacific')
+		target = schedule.now().astimezone(target_tz)
+		cmd = schedule.DelayedCommand.at_time(target, target=None)
+		assert cmd.due()
+
+	def test_alternate_timezone_east(self):
+		target_tz = pytz.timezone('Europe/Amsterdam')
+		target = schedule.now().astimezone(target_tz)
+		cmd = schedule.DelayedCommand.at_time(target, target=None)
+		assert cmd.due()
+
+	def test_daylight_savings(self):
+		"""
+		A command at 9am should always be 9am regardless of
+		a DST boundary.
+		"""
+		with freezegun.freeze_time('2018-03-10 08:00:00'):
+			target_tz = pytz.timezone('US/Eastern')
+			target_time = datetime.time(9, tzinfo=target_tz)
+			cmd = schedule.PeriodicCommandFixedDelay.daily_at(
+				target_time,
+				target=lambda: None,
+			)
+
+		def naive(dt):
+			return dt.replace(tzinfo=None)
+
+		assert naive(cmd) == datetime.datetime(2018, 3, 10, 9, 0, 0)
+		next_ = cmd.next()
+		assert naive(next_) == datetime.datetime(2018, 3, 11, 9, 0, 0)
+		assert next_ - cmd == datetime.timedelta(hours=23)
diff --git a/libraries/tempora/timing.py b/libraries/tempora/timing.py
new file mode 100644
index 00000000..03c22454
--- /dev/null
+++ b/libraries/tempora/timing.py
@@ -0,0 +1,219 @@
+# -*- coding: utf-8 -*-
+
+from __future__ import unicode_literals, absolute_import
+
+import datetime
+import functools
+import numbers
+import time
+
+__metaclass__ = type
+
+
+class Stopwatch:
+	"""
+	A simple stopwatch which starts automatically.
+
+	>>> w = Stopwatch()
+	>>> _1_sec = datetime.timedelta(seconds=1)
+	>>> w.split() < _1_sec
+	True
+	>>> import time
+	>>> time.sleep(1.0)
+	>>> w.split() >= _1_sec
+	True
+	>>> w.stop() >= _1_sec
+	True
+	>>> w.reset()
+	>>> w.start()
+	>>> w.split() < _1_sec
+	True
+
+	It should be possible to launch the Stopwatch in a context:
+
+	>>> with Stopwatch() as watch:
+	...     assert isinstance(watch.split(), datetime.timedelta)
+
+	In that case, the watch is stopped when the context is exited,
+	so to read the elapsed time::
+
+	>>> watch.elapsed
+	datetime.timedelta(...)
+	>>> watch.elapsed.seconds
+	0
+	"""
+	def __init__(self):
+		self.reset()
+		self.start()
+
+	def reset(self):
+		self.elapsed = datetime.timedelta(0)
+		if hasattr(self, 'start_time'):
+			del self.start_time
+
+	def start(self):
+		self.start_time = datetime.datetime.utcnow()
+
+	def stop(self):
+		stop_time = datetime.datetime.utcnow()
+		self.elapsed += stop_time - self.start_time
+		del self.start_time
+		return self.elapsed
+
+	def split(self):
+		local_duration = datetime.datetime.utcnow() - self.start_time
+		return self.elapsed + local_duration
+
+	# context manager support
+	def __enter__(self):
+		self.start()
+		return self
+
+	def __exit__(self, exc_type, exc_value, traceback):
+		self.stop()
+
+
+class IntervalGovernor:
+	"""
+	Decorate a function to only allow it to be called once per
+	min_interval. Otherwise, it returns None.
+	"""
+	def __init__(self, min_interval):
+		if isinstance(min_interval, numbers.Number):
+			min_interval = datetime.timedelta(seconds=min_interval)
+		self.min_interval = min_interval
+		self.last_call = None
+
+	def decorate(self, func):
+		@functools.wraps(func)
+		def wrapper(*args, **kwargs):
+			allow = (
+				not self.last_call
+				or self.last_call.split() > self.min_interval
+			)
+			if allow:
+				self.last_call = Stopwatch()
+				return func(*args, **kwargs)
+		return wrapper
+
+	__call__ = decorate
+
+
+class Timer(Stopwatch):
+	"""
+	Watch for a target elapsed time.
+
+	>>> t = Timer(0.1)
+	>>> t.expired()
+	False
+	>>> __import__('time').sleep(0.15)
+	>>> t.expired()
+	True
+	"""
+	def __init__(self, target=float('Inf')):
+		self.target = self._accept(target)
+		super(Timer, self).__init__()
+
+	def _accept(self, target):
+		"Accept None or ∞ or datetime or numeric for target"
+		if isinstance(target, datetime.timedelta):
+			target = target.total_seconds()
+
+		if target is None:
+			# treat None as infinite target
+			target = float('Inf')
+
+		return target
+
+	def expired(self):
+		return self.split().total_seconds() > self.target
+
+
+class BackoffDelay:
+	"""
+	Exponential backoff delay.
+
+	Useful for defining delays between retries. Consider for use
+	with ``jaraco.functools.retry_call`` as the cleanup.
+
+	Default behavior has no effect; a delay or jitter must
+	be supplied for the call to be non-degenerate.
+
+	>>> bd = BackoffDelay()
+	>>> bd()
+	>>> bd()
+
+	The following instance will delay 10ms for the first call,
+	20ms for the second, etc.
+
+	>>> bd = BackoffDelay(delay=0.01, factor=2)
+	>>> bd()
+	>>> bd()
+
+	Inspect and adjust the state of the delay anytime.
+
+	>>> bd.delay
+	0.04
+	>>> bd.delay = 0.01
+
+	Set limit to prevent the delay from exceeding bounds.
+
+	>>> bd = BackoffDelay(delay=0.01, factor=2, limit=0.015)
+	>>> bd()
+	>>> bd.delay
+	0.015
+
+	Limit may be a callable taking a number and returning
+	the limited number.
+
+	>>> at_least_one = lambda n: max(n, 1)
+	>>> bd = BackoffDelay(delay=0.01, factor=2, limit=at_least_one)
+	>>> bd()
+	>>> bd.delay
+	1
+
+	Pass a jitter to add or subtract seconds to the delay.
+
+	>>> bd = BackoffDelay(jitter=0.01)
+	>>> bd()
+	>>> bd.delay
+	0.01
+
+	Jitter may be a callable. To supply a non-deterministic jitter
+	between -0.5 and 0.5, consider:
+
+	>>> import random
+	>>> jitter=functools.partial(random.uniform, -0.5, 0.5)
+	>>> bd = BackoffDelay(jitter=jitter)
+	>>> bd()
+	>>> 0 <= bd.delay <= 0.5
+	True
+	"""
+
+	delay = 0
+
+	factor = 1
+	"Multiplier applied to delay"
+
+	jitter = 0
+	"Number or callable returning extra seconds to add to delay"
+
+	def __init__(self, delay=0, factor=1, limit=float('inf'), jitter=0):
+		self.delay = delay
+		self.factor = factor
+		if isinstance(limit, numbers.Number):
+			limit_ = limit
+
+			def limit(n):
+				return max(0, min(limit_, n))
+		self.limit = limit
+		if isinstance(jitter, numbers.Number):
+			jitter_ = jitter
+
+			def jitter():
+				return jitter_
+		self.jitter = jitter
+
+	def __call__(self):
+		time.sleep(self.delay)
+		self.delay = self.limit(self.delay * self.factor + self.jitter())
diff --git a/libraries/tempora/utc.py b/libraries/tempora/utc.py
new file mode 100644
index 00000000..35bfdb06
--- /dev/null
+++ b/libraries/tempora/utc.py
@@ -0,0 +1,36 @@
+"""
+Facilities for common time operations in UTC.
+
+Inspired by the `utc project <https://pypi.org/project/utc>`_.
+
+>>> dt = now()
+>>> dt == fromtimestamp(dt.timestamp())
+True
+>>> dt.tzinfo
+datetime.timezone.utc
+
+>>> from time import time as timestamp
+>>> now().timestamp() - timestamp() < 0.1
+True
+
+>>> datetime(2018, 6, 26, 0).tzinfo
+datetime.timezone.utc
+
+>>> time(0, 0).tzinfo
+datetime.timezone.utc
+"""
+
+import datetime as std
+import functools
+
+
+__all__ = ['now', 'fromtimestamp', 'datetime', 'time']
+
+
+now = functools.partial(std.datetime.now, std.timezone.utc)
+fromtimestamp = functools.partial(
+	std.datetime.fromtimestamp,
+	tz=std.timezone.utc,
+)
+datetime = functools.partial(std.datetime, tzinfo=std.timezone.utc)
+time = functools.partial(std.time, tzinfo=std.timezone.utc)
diff --git a/libraries/zc/__init__.py b/libraries/zc/__init__.py
new file mode 100644
index 00000000..146c3362
--- /dev/null
+++ b/libraries/zc/__init__.py
@@ -0,0 +1 @@
+__namespace__ = 'zc'
\ No newline at end of file
diff --git a/libraries/zc/lockfile/README.txt b/libraries/zc/lockfile/README.txt
new file mode 100644
index 00000000..89ef33e9
--- /dev/null
+++ b/libraries/zc/lockfile/README.txt
@@ -0,0 +1,70 @@
+Lock file support
+=================
+
+The ZODB lock_file module provides support for creating file system
+locks.  These are locks that are implemented with lock files and
+OS-provided locking facilities.  To create a lock, instantiate a
+LockFile object with a file name:
+
+    >>> import zc.lockfile
+    >>> lock = zc.lockfile.LockFile('lock')
+
+If we try to lock the same name, we'll get a lock error:
+
+    >>> import zope.testing.loggingsupport
+    >>> handler = zope.testing.loggingsupport.InstalledHandler('zc.lockfile')
+    >>> try:
+    ...     zc.lockfile.LockFile('lock')
+    ... except zc.lockfile.LockError:
+    ...     print("Can't lock file")
+    Can't lock file
+
+.. We don't log failure to acquire.
+
+    >>> for record in handler.records: # doctest: +ELLIPSIS
+    ...     print(record.levelname+' '+record.getMessage())
+
+To release the lock, use it's close method:
+
+    >>> lock.close()
+
+The lock file is not removed.  It is left behind:
+
+    >>> import os
+    >>> os.path.exists('lock')
+    True
+
+Of course, now that we've released the lock, we can create it again:
+
+    >>> lock = zc.lockfile.LockFile('lock')
+    >>> lock.close()
+
+.. Cleanup
+
+    >>> import os
+    >>> os.remove('lock')
+
+Hostname in lock file
+=====================
+
+In a container environment (e.g. Docker), the PID is typically always
+identical even if multiple containers are running under the same operating
+system instance.
+
+Clearly, inspecting lock files doesn't then help much in debugging. To identify
+the container which created the lock file, we need information about the
+container in the lock file. Since Docker uses the container identifier or name
+as the hostname, this information can be stored in the lock file in addition to
+or instead of the PID.
+
+Use the ``content_template`` keyword argument to ``LockFile`` to specify a
+custom lock file content format:
+
+    >>> lock = zc.lockfile.LockFile('lock', content_template='{pid};{hostname}')
+    >>> lock.close()
+
+If you now inspected the lock file, you would see e.g.:
+
+    $ cat lock
+     123;myhostname
+
diff --git a/libraries/zc/lockfile/__init__.py b/libraries/zc/lockfile/__init__.py
new file mode 100644
index 00000000..a0ac2ff1
--- /dev/null
+++ b/libraries/zc/lockfile/__init__.py
@@ -0,0 +1,104 @@
+##############################################################################
+#
+# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE
+#
+##############################################################################
+
+import os
+import errno
+import logging
+logger = logging.getLogger("zc.lockfile")
+
+class LockError(Exception):
+    """Couldn't get a lock
+    """
+
+try:
+    import fcntl
+except ImportError:
+    try:
+        import msvcrt
+    except ImportError:
+        def _lock_file(file):
+            raise TypeError('No file-locking support on this platform')
+        def _unlock_file(file):
+            raise TypeError('No file-locking support on this platform')
+
+    else:
+        # Windows
+        def _lock_file(file):
+            # Lock just the first byte
+            try:
+                msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1)
+            except IOError:
+                raise LockError("Couldn't lock %r" % file.name)
+
+        def _unlock_file(file):
+            try:
+                file.seek(0)
+                msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
+            except IOError:
+                raise LockError("Couldn't unlock %r" % file.name)
+
+else:
+    # Unix
+    _flags = fcntl.LOCK_EX | fcntl.LOCK_NB
+
+    def _lock_file(file):
+        try:
+            fcntl.flock(file.fileno(), _flags)
+        except IOError:
+            raise LockError("Couldn't lock %r" % file.name)
+
+    def _unlock_file(file):
+        fcntl.flock(file.fileno(), fcntl.LOCK_UN)
+
+class LazyHostName(object):
+    """Avoid importing socket and calling gethostname() unnecessarily"""
+    def __str__(self):
+        import socket
+        return socket.gethostname()
+
+
+class LockFile:
+
+    _fp = None
+
+    def __init__(self, path, content_template='{pid}'):
+        self._path = path
+        try:
+            # Try to open for writing without truncation:
+            fp = open(path, 'r+')
+        except IOError:
+            # If the file doesn't exist, we'll get an IO error, try a+
+            # Note that there may be a race here. Multiple processes
+            # could fail on the r+ open and open the file a+, but only
+            # one will get the the lock and write a pid.
+            fp = open(path, 'a+')
+
+        try:
+            _lock_file(fp)
+        except:
+            fp.close()
+            raise
+
+        # We got the lock, record info in the file.
+        self._fp = fp
+        fp.write(" %s\n" % content_template.format(pid=os.getpid(),
+                                                   hostname=LazyHostName()))
+        fp.truncate()
+        fp.flush()
+
+    def close(self):
+        if self._fp is not None:
+            _unlock_file(self._fp)
+            self._fp.close()
+            self._fp = None
diff --git a/libraries/zc/lockfile/tests.py b/libraries/zc/lockfile/tests.py
new file mode 100644
index 00000000..e9fcbff3
--- /dev/null
+++ b/libraries/zc/lockfile/tests.py
@@ -0,0 +1,193 @@
+##############################################################################
+#
+# Copyright (c) 2004 Zope Foundation and Contributors.
+# All Rights Reserved.
+#
+# This software is subject to the provisions of the Zope Public License,
+# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
+# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
+# FOR A PARTICULAR PURPOSE.
+#
+##############################################################################
+import os, re, sys, unittest, doctest
+import zc.lockfile, time, threading
+from zope.testing import renormalizing, setupstack
+import tempfile
+try:
+    from unittest.mock import Mock, patch
+except ImportError:
+    from mock import Mock, patch
+
+checker = renormalizing.RENormalizing([
+    # Python 3 adds module path to error class name.
+    (re.compile("zc\.lockfile\.LockError:"),
+     r"LockError:"),
+    ])
+
+def inc():
+    while 1:
+        try:
+            lock = zc.lockfile.LockFile('f.lock')
+        except zc.lockfile.LockError:
+            continue
+        else:
+            break
+    f = open('f', 'r+b')
+    v = int(f.readline().strip())
+    time.sleep(0.01)
+    v += 1
+    f.seek(0)
+    f.write(('%d\n' % v).encode('ASCII'))
+    f.close()
+    lock.close()
+
+def many_threads_read_and_write():
+    r"""
+    >>> with open('f', 'w+b') as file:
+    ...     _ = file.write(b'0\n')
+    >>> with open('f.lock', 'w+b') as file:
+    ...     _ = file.write(b'0\n')
+
+    >>> n = 50
+    >>> threads = [threading.Thread(target=inc) for i in range(n)]
+    >>> _ = [thread.start() for thread in threads]
+    >>> _ = [thread.join() for thread in threads]
+    >>> with open('f', 'rb') as file:
+    ...     saved = int(file.read().strip())
+    >>> saved == n
+    True
+
+    >>> os.remove('f')
+
+    We should only have one pid in the lock file:
+
+    >>> f = open('f.lock')
+    >>> len(f.read().strip().split())
+    1
+    >>> f.close()
+
+    >>> os.remove('f.lock')
+
+    """
+
+def pid_in_lockfile():
+    r"""
+    >>> import os, zc.lockfile
+    >>> pid = os.getpid()
+    >>> lock = zc.lockfile.LockFile("f.lock")
+    >>> f = open("f.lock")
+    >>> _ = f.seek(1)
+    >>> f.read().strip() == str(pid)
+    True
+    >>> f.close()
+
+    Make sure that locking twice does not overwrite the old pid:
+
+    >>> lock = zc.lockfile.LockFile("f.lock")
+    Traceback (most recent call last):
+      ...
+    LockError: Couldn't lock 'f.lock'
+
+    >>> f = open("f.lock")
+    >>> _ = f.seek(1)
+    >>> f.read().strip() == str(pid)
+    True
+    >>> f.close()
+
+    >>> lock.close()
+    """
+
+
+def hostname_in_lockfile():
+    r"""
+    hostname is correctly written into the lock file when it's included in the
+    lock file content template
+
+    >>> import zc.lockfile
+    >>> with patch('socket.gethostname', Mock(return_value='myhostname')):
+    ...     lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}')
+    >>> f = open("f.lock")
+    >>> _ = f.seek(1)
+    >>> f.read().rstrip()
+    'myhostname'
+    >>> f.close()
+
+    Make sure that locking twice does not overwrite the old hostname:
+
+    >>> lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}')
+    Traceback (most recent call last):
+      ...
+    LockError: Couldn't lock 'f.lock'
+
+    >>> f = open("f.lock")
+    >>> _ = f.seek(1)
+    >>> f.read().rstrip()
+    'myhostname'
+    >>> f.close()
+
+    >>> lock.close()
+    """
+
+
+class TestLogger(object):
+    def __init__(self):
+        self.log_entries = []
+
+    def exception(self, msg, *args):
+        self.log_entries.append((msg,) + args)
+
+
+class LockFileLogEntryTestCase(unittest.TestCase):
+    """Tests for logging in case of lock failure"""
+    def setUp(self):
+        self.here = os.getcwd()
+        self.tmp = tempfile.mkdtemp(prefix='zc.lockfile-test-')
+        os.chdir(self.tmp)
+
+    def tearDown(self):
+        os.chdir(self.here)
+        setupstack.rmtree(self.tmp)
+
+    def test_log_formatting(self):
+        # PID and hostname are parsed and logged from lock file on failure
+        with patch('os.getpid', Mock(return_value=123)):
+            with patch('socket.gethostname', Mock(return_value='myhostname')):
+                lock = zc.lockfile.LockFile('f.lock',
+                                            content_template='{pid}/{hostname}')
+                with open('f.lock') as f:
+                    self.assertEqual(' 123/myhostname\n', f.read())
+
+                lock.close()
+
+    def test_unlock_and_lock_while_multiprocessing_process_running(self):
+        import multiprocessing
+
+        lock = zc.lockfile.LockFile('l')
+        q = multiprocessing.Queue()
+        p = multiprocessing.Process(target=q.get)
+        p.daemon = True
+        p.start()
+
+        # release and re-acquire should work (obviously)
+        lock.close()
+        lock = zc.lockfile.LockFile('l')
+        self.assertTrue(p.is_alive())
+
+        q.put(0)
+        lock.close()
+        p.join()
+
+
+def test_suite():
+    suite = unittest.TestSuite()
+    suite.addTest(doctest.DocFileSuite(
+        'README.txt', checker=checker,
+        setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown))
+    suite.addTest(doctest.DocTestSuite(
+        setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown,
+        checker=checker))
+    # Add unittest test cases from this module
+    suite.addTest(unittest.defaultTestLoader.loadTestsFromName(__name__))
+    return suite
diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py
index 34826af0..7f692846 100644
--- a/resources/lib/webservice.py
+++ b/resources/lib/webservice.py
@@ -2,17 +2,14 @@
 
 #################################################################################################
 
-import SimpleHTTPServer
-import BaseHTTPServer
 import logging
-import httplib
 import threading
-import urlparse
-import urllib
 
 import xbmc
 import xbmcvfs
 
+import cherrypy
+
 #################################################################################################
 
 PORT = 57578
@@ -21,181 +18,59 @@ LOG = logging.getLogger("EMBY."+__name__)
 #################################################################################################
 
 
+class Root(object):
+
+    @cherrypy.expose
+    def default(self, *args, **kwargs):
+
+        try:
+            if not kwargs.get('Id').isdigit():
+                raise IndexError("Incorrect Id format: %s" % kwargs.get('Id'))
+
+            LOG.info("Webservice called with params: %s", kwargs)
+
+            return ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s"
+                    % (kwargs.get('Id'), kwargs.get('KodiId'), kwargs.get('Name'), kwargs.get('transcode') or False))
+
+        except IndexError as error:
+            LOG.error(error)
+
+            raise cherrypy.HTTPError(404, error)
+
+        except Exception as error:
+            LOG.exception(error)
+
+            raise cherrypy.HTTPError(500, "Exception occurred: %s" % error)
+
 class WebService(threading.Thread):
 
-    ''' Run a webservice to trigger playback.
-        Inspired from script.skin.helper.service by marcelveldt.
-    '''
-    stop_thread = False
+    root = None
 
     def __init__(self):
+
+        self.root = Root()
+        cherrypy.config.update({
+            'engine.autoreload.on' : False,
+            'log.screen': False,
+            'engine.timeout_monitor.frequency': 5,
+            'server.shutdown_timeout': 1,
+        })
         threading.Thread.__init__(self)
 
+    def run(self):
+        
+        LOG.info("--->[ webservice/%s ]", PORT)
+        conf = {
+            'global': {
+                'server.socket_host': '0.0.0.0',
+                'server.socket_port': PORT
+            }, '/': {}
+        }
+        cherrypy.quickstart(self.root, '/', conf)
+
     def stop(self):
 
-        ''' Called when the thread needs to stop
-        '''
-        try:
-            conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT)
-            conn.request("QUIT", "/")
-            conn.getresponse()
-            self.stop_thread = True
-        except Exception as error:
-            LOG.exception(error)
+        cherrypy.engine.exit()
+        self.join(0)
 
-    def run(self):
-
-        ''' Called to start the webservice.
-        '''
-        LOG.info("--->[ webservice/%s ]", PORT)
-
-        try:
-            server = StoppableHttpServer(('127.0.0.1', PORT), StoppableHttpRequestHandler)
-            server.serve_forever()
-        except Exception as error:
-
-            if '10053' not in error: # ignore host diconnected errors
-                LOG.exception(error)
-
-        LOG.info("---<[ webservice ]")
-
-
-class Request(object):
-
-    ''' Attributes from urlsplit that this class also sets
-    '''
-    uri_attrs = ('scheme', 'netloc', 'path', 'query', 'fragment')
-
-    def __init__(self, uri, headers, rfile=None):
-
-        self.uri = uri
-        self.headers = headers
-        parsed = urlparse.urlsplit(uri)
-
-        for i, attr in enumerate(self.uri_attrs):
-            setattr(self, attr, parsed[i])
-
-        try:
-            body_len = int(self.headers.get('Content-length', 0))
-        except ValueError:
-            body_len = 0
-
-        self.body = rfile.read(body_len) if body_len and rfile else None
-
-
-class StoppableHttpServer(BaseHTTPServer.HTTPServer):
-
-    ''' Http server that reacts to self.stop flag.
-    '''
-    def serve_forever(self):
-
-        ''' Handle one request at a time until stopped.
-        '''
-        self.stop = False
-
-        while not self.stop:
-
-            self.handle_request()
-            xbmc.sleep(100)
-
-
-class StoppableHttpRequestHandler(SimpleHTTPServer.SimpleHTTPRequestHandler):
-
-    ''' http request handler with QUIT stopping the server
-    '''
-    raw_requestline = ""
-
-    def __init__(self, request, client_address, server):
-        try:
-            SimpleHTTPServer.SimpleHTTPRequestHandler.__init__(self, request, client_address, server)
-        except Exception:
-            pass
-
-    def log_message(self, format, *args):
-
-        ''' Mute the webservice requests.
-        '''
-        pass
-
-    def do_QUIT(self):
-
-        ''' send 200 OK response, and set server.stop to True
-        '''
-        self.send_response(200)
-        self.end_headers()
-        self.server.stop = True
-
-    def parse_request(self):
-
-        ''' Modify here to workaround unencoded requests.
-        '''
-        retval = SimpleHTTPServer.SimpleHTTPRequestHandler.parse_request(self)
-        self.request = Request(self.path, self.headers, self.rfile)
-
-        return retval
-
-    def do_HEAD(self):
-
-        ''' Called on HEAD requests
-        '''
-        self.handle_request(True)
-
-        return
-
-    def get_params(self):
-
-        ''' Get the params
-        '''
-        try:
-            path = self.path[1:]
-
-            if '?' in path:
-                path = path.split('?', 1)[1]
-
-            params = dict(urlparse.parse_qsl(path))
-        except Exception:
-            params = {}
-
-        return params
-
-    def handle_request(self, headers_only=False):
-
-        ''' Send headers and reponse
-        '''
-        try:
-            params = self.get_params()
-
-            if not params.get('Id').isdigit():
-                raise IndexError("Incorrect Id format: %s" % params.get('Id'))
-            
-            LOG.info("Webservice called with params: %s", params)
-
-            path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s"
-                    % (params.get('Id'), params.get('KodiId'), params.get('Name'), params.get('transcode') or False))
-            
-            self.send_response(200)
-            self.send_header('Content-type', 'text/html')
-            self.send_header('Content-Length', len(path))
-            self.end_headers()
-
-            if not headers_only:
-                self.wfile.write(path)
-        except IndexError as error:
-
-            LOG.error(error)
-            self.send_error(403)
-
-        except Exception as error:
-
-            LOG.exception(error)
-            self.send_error(500, "Exception occurred: %s" % error)
-
-        return
-
-    def do_GET(self):
-
-        ''' Called on GET requests
-        '''
-        self.handle_request()
-
-        return
+        del self.root

From c367deed4a7f918eb99a0bb2c5f7ab355dbc1aec Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Wed, 30 Jan 2019 06:43:24 -0600
Subject: [PATCH 07/12] Update translation

Dutch
---
 .../language/resource.language.nl_nl/strings.po   | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/resources/language/resource.language.nl_nl/strings.po b/resources/language/resource.language.nl_nl/strings.po
index ce8e5eb5..724b877c 100644
--- a/resources/language/resource.language.nl_nl/strings.po
+++ b/resources/language/resource.language.nl_nl/strings.po
@@ -3,14 +3,14 @@
 # Addon id: plugin.video.emby
 # Addon Provider: angelblue05
 # Translators:
-# inSmithereens, 2019
+# 63ac71fcbd0581bb567b1f0d798c7970, 2019
 # 
 msgid ""
 msgstr ""
 "Project-Id-Version: Emby for Kodi\n"
 "POT-Creation-Date: YEAR-MO-DA HO:MI+ZONE\n"
 "PO-Revision-Date: 2018-09-07 20:10+0000\n"
-"Last-Translator: inSmithereens, 2019\n"
+"Last-Translator: 63ac71fcbd0581bb567b1f0d798c7970, 2019\n"
 "Language-Team: Dutch (https://www.transifex.com/emby-for-kodi/teams/91090/nl/)\n"
 "MIME-Version: 1.0\n"
 "Content-Type: text/plain; charset=UTF-8\n"
@@ -227,7 +227,7 @@ msgid "Verify connection"
 msgstr "Verbinding controleren"
 
 msgctxt "#30504"
-msgid "Use altername device name"
+msgid "Use alternate device name"
 msgstr "Alternatieve apparaat naam gebruiken"
 
 msgctxt "#30506"
@@ -1056,8 +1056,15 @@ msgstr "Bibliotheken beheren"
 
 msgctxt "#33195"
 msgid "Enable Emby for Kodi"
-msgstr ""
+msgstr "Emby for Kodi inschakelen"
 
 msgctxt "#33196"
 msgid "Advanced options"
+msgstr "Geavanceerde opties"
+
+msgctxt "#33197"
+msgid ""
+"A sync is already running, please wait until it completes and try again."
 msgstr ""
+"Synchronisatie is al in voortgang, wacht tot deze compleet is en probeer het"
+" nog eens."

From 0e30a28dcb86f3457c91377a430ff2c470ce8ef5 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Thu, 31 Jan 2019 04:06:46 -0600
Subject: [PATCH 08/12] Fix webservice being stalled

This time, it should work. Logging was causing the stall?!
---
 libraries/backports/__init__.py               |    1 -
 libraries/backports/functools_lru_cache.py    |  184 --
 libraries/cheroot/__init__.py                 |    6 -
 libraries/cheroot/__main__.py                 |    6 -
 libraries/cheroot/_compat.py                  |   66 -
 libraries/cheroot/cli.py                      |  233 --
 libraries/cheroot/errors.py                   |   58 -
 libraries/cheroot/makefile.py                 |  387 ---
 libraries/cheroot/server.py                   | 2001 ---------------
 libraries/cheroot/ssl/__init__.py             |   51 -
 libraries/cheroot/ssl/builtin.py              |  162 --
 libraries/cheroot/ssl/pyopenssl.py            |  267 --
 libraries/cheroot/test/__init__.py            |    1 -
 libraries/cheroot/test/conftest.py            |   27 -
 libraries/cheroot/test/helper.py              |  169 --
 libraries/cheroot/test/test.pem               |   38 -
 libraries/cheroot/test/test__compat.py        |   49 -
 libraries/cheroot/test/test_conn.py           |  897 -------
 libraries/cheroot/test/test_core.py           |  405 ---
 libraries/cheroot/test/test_server.py         |  193 --
 libraries/cheroot/test/webtest.py             |  581 -----
 libraries/cheroot/testing.py                  |  144 --
 libraries/cheroot/workers/__init__.py         |    1 -
 libraries/cheroot/workers/threadpool.py       |  271 --
 libraries/cheroot/wsgi.py                     |  423 ----
 libraries/cherrypy/__init__.py                |  362 ---
 libraries/cherrypy/__main__.py                |    5 -
 libraries/cherrypy/_cpchecker.py              |  325 ---
 libraries/cherrypy/_cpcompat.py               |  162 --
 libraries/cherrypy/_cpconfig.py               |  300 ---
 libraries/cherrypy/_cpdispatch.py             |  686 -----
 libraries/cherrypy/_cperror.py                |  619 -----
 libraries/cherrypy/_cplogging.py              |  482 ----
 libraries/cherrypy/_cpmodpy.py                |  356 ---
 libraries/cherrypy/_cpnative_server.py        |  160 --
 libraries/cherrypy/_cpreqbody.py              | 1000 --------
 libraries/cherrypy/_cprequest.py              |  930 -------
 libraries/cherrypy/_cpserver.py               |  252 --
 libraries/cherrypy/_cptools.py                |  509 ----
 libraries/cherrypy/_cptree.py                 |  313 ---
 libraries/cherrypy/_cpwsgi.py                 |  467 ----
 libraries/cherrypy/_cpwsgi_server.py          |  110 -
 libraries/cherrypy/_helper.py                 |  344 ---
 libraries/cherrypy/daemon.py                  |  107 -
 libraries/cherrypy/favicon.ico                |  Bin 1406 -> 0 bytes
 libraries/cherrypy/lib/__init__.py            |   96 -
 libraries/cherrypy/lib/auth_basic.py          |  120 -
 libraries/cherrypy/lib/auth_digest.py         |  464 ----
 libraries/cherrypy/lib/caching.py             |  482 ----
 libraries/cherrypy/lib/covercp.py             |  391 ---
 libraries/cherrypy/lib/cpstats.py             |  696 ------
 libraries/cherrypy/lib/cptools.py             |  640 -----
 libraries/cherrypy/lib/encoding.py            |  436 ----
 libraries/cherrypy/lib/gctools.py             |  218 --
 libraries/cherrypy/lib/httputil.py            |  581 -----
 libraries/cherrypy/lib/jsontools.py           |   88 -
 libraries/cherrypy/lib/locking.py             |   47 -
 libraries/cherrypy/lib/profiler.py            |  221 --
 libraries/cherrypy/lib/reprconf.py            |  514 ----
 libraries/cherrypy/lib/sessions.py            |  919 -------
 libraries/cherrypy/lib/static.py              |  390 ---
 libraries/cherrypy/lib/xmlrpcutil.py          |   61 -
 libraries/cherrypy/process/__init__.py        |   17 -
 libraries/cherrypy/process/plugins.py         |  752 ------
 libraries/cherrypy/process/servers.py         |  416 ----
 libraries/cherrypy/process/win32.py           |  183 --
 libraries/cherrypy/process/wspbus.py          |  590 -----
 libraries/cherrypy/scaffold/__init__.py       |   63 -
 libraries/cherrypy/scaffold/apache-fcgi.conf  |   22 -
 libraries/cherrypy/scaffold/example.conf      |    3 -
 libraries/cherrypy/scaffold/site.conf         |   14 -
 .../static/made_with_cherrypy_small.png       |  Bin 6347 -> 0 bytes
 libraries/cherrypy/test/__init__.py           |   24 -
 libraries/cherrypy/test/_test_decorators.py   |   39 -
 libraries/cherrypy/test/_test_states_demo.py  |   69 -
 libraries/cherrypy/test/benchmark.py          |  425 ----
 libraries/cherrypy/test/checkerdemo.py        |   49 -
 libraries/cherrypy/test/fastcgi.conf          |   18 -
 libraries/cherrypy/test/fcgi.conf             |   14 -
 libraries/cherrypy/test/helper.py             |  542 ----
 libraries/cherrypy/test/logtest.py            |  228 --
 libraries/cherrypy/test/modfastcgi.py         |  136 -
 libraries/cherrypy/test/modfcgid.py           |  124 -
 libraries/cherrypy/test/modpy.py              |  164 --
 libraries/cherrypy/test/modwsgi.py            |  154 --
 libraries/cherrypy/test/sessiondemo.py        |  161 --
 libraries/cherrypy/test/static/404.html       |    5 -
 libraries/cherrypy/test/static/dirback.jpg    |  Bin 16585 -> 0 bytes
 libraries/cherrypy/test/static/index.html     |    1 -
 libraries/cherrypy/test/style.css             |    1 -
 libraries/cherrypy/test/test.pem              |   38 -
 libraries/cherrypy/test/test_auth_basic.py    |  135 -
 libraries/cherrypy/test/test_auth_digest.py   |  134 -
 libraries/cherrypy/test/test_bus.py           |  274 --
 libraries/cherrypy/test/test_caching.py       |  392 ---
 libraries/cherrypy/test/test_compat.py        |   34 -
 libraries/cherrypy/test/test_config.py        |  303 ---
 libraries/cherrypy/test/test_config_server.py |  126 -
 libraries/cherrypy/test/test_conn.py          |  873 -------
 libraries/cherrypy/test/test_core.py          |  823 ------
 .../test/test_dynamicobjectmapping.py         |  424 ----
 libraries/cherrypy/test/test_encoding.py      |  426 ----
 libraries/cherrypy/test/test_etags.py         |   84 -
 libraries/cherrypy/test/test_http.py          |  307 ---
 libraries/cherrypy/test/test_httputil.py      |   80 -
 libraries/cherrypy/test/test_iterator.py      |  196 --
 libraries/cherrypy/test/test_json.py          |  102 -
 libraries/cherrypy/test/test_logging.py       |  209 --
 libraries/cherrypy/test/test_mime.py          |  134 -
 libraries/cherrypy/test/test_misc_tools.py    |  210 --
 libraries/cherrypy/test/test_native.py        |   35 -
 libraries/cherrypy/test/test_objectmapping.py |  430 ----
 libraries/cherrypy/test/test_params.py        |   61 -
 libraries/cherrypy/test/test_plugins.py       |   14 -
 libraries/cherrypy/test/test_proxy.py         |  154 --
 libraries/cherrypy/test/test_refleaks.py      |   66 -
 libraries/cherrypy/test/test_request_obj.py   |  932 -------
 libraries/cherrypy/test/test_routes.py        |   80 -
 libraries/cherrypy/test/test_session.py       |  512 ----
 .../cherrypy/test/test_sessionauthenticate.py |   61 -
 libraries/cherrypy/test/test_states.py        |  473 ----
 libraries/cherrypy/test/test_static.py        |  434 ----
 libraries/cherrypy/test/test_tools.py         |  468 ----
 libraries/cherrypy/test/test_tutorials.py     |  210 --
 libraries/cherrypy/test/test_virtualhost.py   |  113 -
 libraries/cherrypy/test/test_wsgi_ns.py       |   93 -
 .../cherrypy/test/test_wsgi_unix_socket.py    |   93 -
 libraries/cherrypy/test/test_wsgi_vhost.py    |   35 -
 libraries/cherrypy/test/test_wsgiapps.py      |  120 -
 libraries/cherrypy/test/test_xmlrpc.py        |  183 --
 libraries/cherrypy/test/webtest.py            |   11 -
 libraries/cherrypy/tutorial/README.rst        |   16 -
 libraries/cherrypy/tutorial/__init__.py       |    3 -
 libraries/cherrypy/tutorial/custom_error.html |   14 -
 libraries/cherrypy/tutorial/pdf_file.pdf      |  Bin 85698 -> 0 bytes
 .../cherrypy/tutorial/tut01_helloworld.py     |   34 -
 .../cherrypy/tutorial/tut02_expose_methods.py |   32 -
 .../cherrypy/tutorial/tut03_get_and_post.py   |   51 -
 .../cherrypy/tutorial/tut04_complex_site.py   |  103 -
 .../tutorial/tut05_derived_objects.py         |   80 -
 .../cherrypy/tutorial/tut06_default_method.py |   61 -
 libraries/cherrypy/tutorial/tut07_sessions.py |   41 -
 .../tutorial/tut08_generators_and_yield.py    |   44 -
 libraries/cherrypy/tutorial/tut09_files.py    |  105 -
 .../cherrypy/tutorial/tut10_http_errors.py    |   84 -
 libraries/cherrypy/tutorial/tutorial.conf     |    4 -
 libraries/contextlib2.py                      |  436 ----
 libraries/more_itertools/__init__.py          |    2 -
 libraries/more_itertools/more.py              | 2211 -----------------
 libraries/more_itertools/recipes.py           |  565 -----
 libraries/more_itertools/tests/__init__.py    |    0
 libraries/more_itertools/tests/test_more.py   | 2074 ----------------
 .../more_itertools/tests/test_recipes.py      |  616 -----
 libraries/portend.py                          |  212 --
 libraries/tempora/__init__.py                 |  505 ----
 libraries/tempora/schedule.py                 |  202 --
 libraries/tempora/tests/test_schedule.py      |  118 -
 libraries/tempora/timing.py                   |  219 --
 libraries/tempora/utc.py                      |   36 -
 libraries/zc/__init__.py                      |    1 -
 libraries/zc/lockfile/README.txt              |   70 -
 libraries/zc/lockfile/__init__.py             |  104 -
 libraries/zc/lockfile/tests.py                |  193 --
 resources/lib/webservice.py                   |  167 +-
 164 files changed, 116 insertions(+), 42857 deletions(-)
 delete mode 100644 libraries/backports/__init__.py
 delete mode 100644 libraries/backports/functools_lru_cache.py
 delete mode 100644 libraries/cheroot/__init__.py
 delete mode 100644 libraries/cheroot/__main__.py
 delete mode 100644 libraries/cheroot/_compat.py
 delete mode 100644 libraries/cheroot/cli.py
 delete mode 100644 libraries/cheroot/errors.py
 delete mode 100644 libraries/cheroot/makefile.py
 delete mode 100644 libraries/cheroot/server.py
 delete mode 100644 libraries/cheroot/ssl/__init__.py
 delete mode 100644 libraries/cheroot/ssl/builtin.py
 delete mode 100644 libraries/cheroot/ssl/pyopenssl.py
 delete mode 100644 libraries/cheroot/test/__init__.py
 delete mode 100644 libraries/cheroot/test/conftest.py
 delete mode 100644 libraries/cheroot/test/helper.py
 delete mode 100644 libraries/cheroot/test/test.pem
 delete mode 100644 libraries/cheroot/test/test__compat.py
 delete mode 100644 libraries/cheroot/test/test_conn.py
 delete mode 100644 libraries/cheroot/test/test_core.py
 delete mode 100644 libraries/cheroot/test/test_server.py
 delete mode 100644 libraries/cheroot/test/webtest.py
 delete mode 100644 libraries/cheroot/testing.py
 delete mode 100644 libraries/cheroot/workers/__init__.py
 delete mode 100644 libraries/cheroot/workers/threadpool.py
 delete mode 100644 libraries/cheroot/wsgi.py
 delete mode 100644 libraries/cherrypy/__init__.py
 delete mode 100644 libraries/cherrypy/__main__.py
 delete mode 100644 libraries/cherrypy/_cpchecker.py
 delete mode 100644 libraries/cherrypy/_cpcompat.py
 delete mode 100644 libraries/cherrypy/_cpconfig.py
 delete mode 100644 libraries/cherrypy/_cpdispatch.py
 delete mode 100644 libraries/cherrypy/_cperror.py
 delete mode 100644 libraries/cherrypy/_cplogging.py
 delete mode 100644 libraries/cherrypy/_cpmodpy.py
 delete mode 100644 libraries/cherrypy/_cpnative_server.py
 delete mode 100644 libraries/cherrypy/_cpreqbody.py
 delete mode 100644 libraries/cherrypy/_cprequest.py
 delete mode 100644 libraries/cherrypy/_cpserver.py
 delete mode 100644 libraries/cherrypy/_cptools.py
 delete mode 100644 libraries/cherrypy/_cptree.py
 delete mode 100644 libraries/cherrypy/_cpwsgi.py
 delete mode 100644 libraries/cherrypy/_cpwsgi_server.py
 delete mode 100644 libraries/cherrypy/_helper.py
 delete mode 100644 libraries/cherrypy/daemon.py
 delete mode 100644 libraries/cherrypy/favicon.ico
 delete mode 100644 libraries/cherrypy/lib/__init__.py
 delete mode 100644 libraries/cherrypy/lib/auth_basic.py
 delete mode 100644 libraries/cherrypy/lib/auth_digest.py
 delete mode 100644 libraries/cherrypy/lib/caching.py
 delete mode 100644 libraries/cherrypy/lib/covercp.py
 delete mode 100644 libraries/cherrypy/lib/cpstats.py
 delete mode 100644 libraries/cherrypy/lib/cptools.py
 delete mode 100644 libraries/cherrypy/lib/encoding.py
 delete mode 100644 libraries/cherrypy/lib/gctools.py
 delete mode 100644 libraries/cherrypy/lib/httputil.py
 delete mode 100644 libraries/cherrypy/lib/jsontools.py
 delete mode 100644 libraries/cherrypy/lib/locking.py
 delete mode 100644 libraries/cherrypy/lib/profiler.py
 delete mode 100644 libraries/cherrypy/lib/reprconf.py
 delete mode 100644 libraries/cherrypy/lib/sessions.py
 delete mode 100644 libraries/cherrypy/lib/static.py
 delete mode 100644 libraries/cherrypy/lib/xmlrpcutil.py
 delete mode 100644 libraries/cherrypy/process/__init__.py
 delete mode 100644 libraries/cherrypy/process/plugins.py
 delete mode 100644 libraries/cherrypy/process/servers.py
 delete mode 100644 libraries/cherrypy/process/win32.py
 delete mode 100644 libraries/cherrypy/process/wspbus.py
 delete mode 100644 libraries/cherrypy/scaffold/__init__.py
 delete mode 100644 libraries/cherrypy/scaffold/apache-fcgi.conf
 delete mode 100644 libraries/cherrypy/scaffold/example.conf
 delete mode 100644 libraries/cherrypy/scaffold/site.conf
 delete mode 100644 libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png
 delete mode 100644 libraries/cherrypy/test/__init__.py
 delete mode 100644 libraries/cherrypy/test/_test_decorators.py
 delete mode 100644 libraries/cherrypy/test/_test_states_demo.py
 delete mode 100644 libraries/cherrypy/test/benchmark.py
 delete mode 100644 libraries/cherrypy/test/checkerdemo.py
 delete mode 100644 libraries/cherrypy/test/fastcgi.conf
 delete mode 100644 libraries/cherrypy/test/fcgi.conf
 delete mode 100644 libraries/cherrypy/test/helper.py
 delete mode 100644 libraries/cherrypy/test/logtest.py
 delete mode 100644 libraries/cherrypy/test/modfastcgi.py
 delete mode 100644 libraries/cherrypy/test/modfcgid.py
 delete mode 100644 libraries/cherrypy/test/modpy.py
 delete mode 100644 libraries/cherrypy/test/modwsgi.py
 delete mode 100644 libraries/cherrypy/test/sessiondemo.py
 delete mode 100644 libraries/cherrypy/test/static/404.html
 delete mode 100644 libraries/cherrypy/test/static/dirback.jpg
 delete mode 100644 libraries/cherrypy/test/static/index.html
 delete mode 100644 libraries/cherrypy/test/style.css
 delete mode 100644 libraries/cherrypy/test/test.pem
 delete mode 100644 libraries/cherrypy/test/test_auth_basic.py
 delete mode 100644 libraries/cherrypy/test/test_auth_digest.py
 delete mode 100644 libraries/cherrypy/test/test_bus.py
 delete mode 100644 libraries/cherrypy/test/test_caching.py
 delete mode 100644 libraries/cherrypy/test/test_compat.py
 delete mode 100644 libraries/cherrypy/test/test_config.py
 delete mode 100644 libraries/cherrypy/test/test_config_server.py
 delete mode 100644 libraries/cherrypy/test/test_conn.py
 delete mode 100644 libraries/cherrypy/test/test_core.py
 delete mode 100644 libraries/cherrypy/test/test_dynamicobjectmapping.py
 delete mode 100644 libraries/cherrypy/test/test_encoding.py
 delete mode 100644 libraries/cherrypy/test/test_etags.py
 delete mode 100644 libraries/cherrypy/test/test_http.py
 delete mode 100644 libraries/cherrypy/test/test_httputil.py
 delete mode 100644 libraries/cherrypy/test/test_iterator.py
 delete mode 100644 libraries/cherrypy/test/test_json.py
 delete mode 100644 libraries/cherrypy/test/test_logging.py
 delete mode 100644 libraries/cherrypy/test/test_mime.py
 delete mode 100644 libraries/cherrypy/test/test_misc_tools.py
 delete mode 100644 libraries/cherrypy/test/test_native.py
 delete mode 100644 libraries/cherrypy/test/test_objectmapping.py
 delete mode 100644 libraries/cherrypy/test/test_params.py
 delete mode 100644 libraries/cherrypy/test/test_plugins.py
 delete mode 100644 libraries/cherrypy/test/test_proxy.py
 delete mode 100644 libraries/cherrypy/test/test_refleaks.py
 delete mode 100644 libraries/cherrypy/test/test_request_obj.py
 delete mode 100644 libraries/cherrypy/test/test_routes.py
 delete mode 100644 libraries/cherrypy/test/test_session.py
 delete mode 100644 libraries/cherrypy/test/test_sessionauthenticate.py
 delete mode 100644 libraries/cherrypy/test/test_states.py
 delete mode 100644 libraries/cherrypy/test/test_static.py
 delete mode 100644 libraries/cherrypy/test/test_tools.py
 delete mode 100644 libraries/cherrypy/test/test_tutorials.py
 delete mode 100644 libraries/cherrypy/test/test_virtualhost.py
 delete mode 100644 libraries/cherrypy/test/test_wsgi_ns.py
 delete mode 100644 libraries/cherrypy/test/test_wsgi_unix_socket.py
 delete mode 100644 libraries/cherrypy/test/test_wsgi_vhost.py
 delete mode 100644 libraries/cherrypy/test/test_wsgiapps.py
 delete mode 100644 libraries/cherrypy/test/test_xmlrpc.py
 delete mode 100644 libraries/cherrypy/test/webtest.py
 delete mode 100644 libraries/cherrypy/tutorial/README.rst
 delete mode 100644 libraries/cherrypy/tutorial/__init__.py
 delete mode 100644 libraries/cherrypy/tutorial/custom_error.html
 delete mode 100644 libraries/cherrypy/tutorial/pdf_file.pdf
 delete mode 100644 libraries/cherrypy/tutorial/tut01_helloworld.py
 delete mode 100644 libraries/cherrypy/tutorial/tut02_expose_methods.py
 delete mode 100644 libraries/cherrypy/tutorial/tut03_get_and_post.py
 delete mode 100644 libraries/cherrypy/tutorial/tut04_complex_site.py
 delete mode 100644 libraries/cherrypy/tutorial/tut05_derived_objects.py
 delete mode 100644 libraries/cherrypy/tutorial/tut06_default_method.py
 delete mode 100644 libraries/cherrypy/tutorial/tut07_sessions.py
 delete mode 100644 libraries/cherrypy/tutorial/tut08_generators_and_yield.py
 delete mode 100644 libraries/cherrypy/tutorial/tut09_files.py
 delete mode 100644 libraries/cherrypy/tutorial/tut10_http_errors.py
 delete mode 100644 libraries/cherrypy/tutorial/tutorial.conf
 delete mode 100644 libraries/contextlib2.py
 delete mode 100644 libraries/more_itertools/__init__.py
 delete mode 100644 libraries/more_itertools/more.py
 delete mode 100644 libraries/more_itertools/recipes.py
 delete mode 100644 libraries/more_itertools/tests/__init__.py
 delete mode 100644 libraries/more_itertools/tests/test_more.py
 delete mode 100644 libraries/more_itertools/tests/test_recipes.py
 delete mode 100644 libraries/portend.py
 delete mode 100644 libraries/tempora/__init__.py
 delete mode 100644 libraries/tempora/schedule.py
 delete mode 100644 libraries/tempora/tests/test_schedule.py
 delete mode 100644 libraries/tempora/timing.py
 delete mode 100644 libraries/tempora/utc.py
 delete mode 100644 libraries/zc/__init__.py
 delete mode 100644 libraries/zc/lockfile/README.txt
 delete mode 100644 libraries/zc/lockfile/__init__.py
 delete mode 100644 libraries/zc/lockfile/tests.py

diff --git a/libraries/backports/__init__.py b/libraries/backports/__init__.py
deleted file mode 100644
index 69e3be50..00000000
--- a/libraries/backports/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__path__ = __import__('pkgutil').extend_path(__path__, __name__)
diff --git a/libraries/backports/functools_lru_cache.py b/libraries/backports/functools_lru_cache.py
deleted file mode 100644
index 707c6c76..00000000
--- a/libraries/backports/functools_lru_cache.py
+++ /dev/null
@@ -1,184 +0,0 @@
-from __future__ import absolute_import
-
-import functools
-from collections import namedtuple
-from threading import RLock
-
-_CacheInfo = namedtuple("CacheInfo", ["hits", "misses", "maxsize", "currsize"])
-
-
-@functools.wraps(functools.update_wrapper)
-def update_wrapper(wrapper,
-                   wrapped,
-                   assigned = functools.WRAPPER_ASSIGNMENTS,
-                   updated = functools.WRAPPER_UPDATES):
-    """
-    Patch two bugs in functools.update_wrapper.
-    """
-    # workaround for http://bugs.python.org/issue3445
-    assigned = tuple(attr for attr in assigned if hasattr(wrapped, attr))
-    wrapper = functools.update_wrapper(wrapper, wrapped, assigned, updated)
-    # workaround for https://bugs.python.org/issue17482
-    wrapper.__wrapped__ = wrapped
-    return wrapper
-
-
-class _HashedSeq(list):
-    __slots__ = 'hashvalue'
-
-    def __init__(self, tup, hash=hash):
-        self[:] = tup
-        self.hashvalue = hash(tup)
-
-    def __hash__(self):
-        return self.hashvalue
-
-
-def _make_key(args, kwds, typed,
-              kwd_mark=(object(),),
-              fasttypes=set([int, str, frozenset, type(None)]),
-              sorted=sorted, tuple=tuple, type=type, len=len):
-    'Make a cache key from optionally typed positional and keyword arguments'
-    key = args
-    if kwds:
-        sorted_items = sorted(kwds.items())
-        key += kwd_mark
-        for item in sorted_items:
-            key += item
-    if typed:
-        key += tuple(type(v) for v in args)
-        if kwds:
-            key += tuple(type(v) for k, v in sorted_items)
-    elif len(key) == 1 and type(key[0]) in fasttypes:
-        return key[0]
-    return _HashedSeq(key)
-
-
-def lru_cache(maxsize=100, typed=False):
-    """Least-recently-used cache decorator.
-
-    If *maxsize* is set to None, the LRU features are disabled and the cache
-    can grow without bound.
-
-    If *typed* is True, arguments of different types will be cached separately.
-    For example, f(3.0) and f(3) will be treated as distinct calls with
-    distinct results.
-
-    Arguments to the cached function must be hashable.
-
-    View the cache statistics named tuple (hits, misses, maxsize, currsize) with
-    f.cache_info().  Clear the cache and statistics with f.cache_clear().
-    Access the underlying function with f.__wrapped__.
-
-    See:  http://en.wikipedia.org/wiki/Cache_algorithms#Least_Recently_Used
-
-    """
-
-    # Users should only access the lru_cache through its public API:
-    #       cache_info, cache_clear, and f.__wrapped__
-    # The internals of the lru_cache are encapsulated for thread safety and
-    # to allow the implementation to change (including a possible C version).
-
-    def decorating_function(user_function):
-
-        cache = dict()
-        stats = [0, 0]                  # make statistics updateable non-locally
-        HITS, MISSES = 0, 1             # names for the stats fields
-        make_key = _make_key
-        cache_get = cache.get           # bound method to lookup key or return None
-        _len = len                      # localize the global len() function
-        lock = RLock()                  # because linkedlist updates aren't threadsafe
-        root = []                       # root of the circular doubly linked list
-        root[:] = [root, root, None, None]      # initialize by pointing to self
-        nonlocal_root = [root]                  # make updateable non-locally
-        PREV, NEXT, KEY, RESULT = 0, 1, 2, 3    # names for the link fields
-
-        if maxsize == 0:
-
-            def wrapper(*args, **kwds):
-                # no caching, just do a statistics update after a successful call
-                result = user_function(*args, **kwds)
-                stats[MISSES] += 1
-                return result
-
-        elif maxsize is None:
-
-            def wrapper(*args, **kwds):
-                # simple caching without ordering or size limit
-                key = make_key(args, kwds, typed)
-                result = cache_get(key, root)   # root used here as a unique not-found sentinel
-                if result is not root:
-                    stats[HITS] += 1
-                    return result
-                result = user_function(*args, **kwds)
-                cache[key] = result
-                stats[MISSES] += 1
-                return result
-
-        else:
-
-            def wrapper(*args, **kwds):
-                # size limited caching that tracks accesses by recency
-                key = make_key(args, kwds, typed) if kwds or typed else args
-                with lock:
-                    link = cache_get(key)
-                    if link is not None:
-                        # record recent use of the key by moving it to the front of the list
-                        root, = nonlocal_root
-                        link_prev, link_next, key, result = link
-                        link_prev[NEXT] = link_next
-                        link_next[PREV] = link_prev
-                        last = root[PREV]
-                        last[NEXT] = root[PREV] = link
-                        link[PREV] = last
-                        link[NEXT] = root
-                        stats[HITS] += 1
-                        return result
-                result = user_function(*args, **kwds)
-                with lock:
-                    root, = nonlocal_root
-                    if key in cache:
-                        # getting here means that this same key was added to the
-                        # cache while the lock was released.  since the link
-                        # update is already done, we need only return the
-                        # computed result and update the count of misses.
-                        pass
-                    elif _len(cache) >= maxsize:
-                        # use the old root to store the new key and result
-                        oldroot = root
-                        oldroot[KEY] = key
-                        oldroot[RESULT] = result
-                        # empty the oldest link and make it the new root
-                        root = nonlocal_root[0] = oldroot[NEXT]
-                        oldkey = root[KEY]
-                        root[KEY] = root[RESULT] = None
-                        # now update the cache dictionary for the new links
-                        del cache[oldkey]
-                        cache[key] = oldroot
-                    else:
-                        # put result in a new link at the front of the list
-                        last = root[PREV]
-                        link = [last, root, key, result]
-                        last[NEXT] = root[PREV] = cache[key] = link
-                    stats[MISSES] += 1
-                return result
-
-        def cache_info():
-            """Report cache statistics"""
-            with lock:
-                return _CacheInfo(stats[HITS], stats[MISSES], maxsize, len(cache))
-
-        def cache_clear():
-            """Clear the cache and cache statistics"""
-            with lock:
-                cache.clear()
-                root = nonlocal_root[0]
-                root[:] = [root, root, None, None]
-                stats[:] = [0, 0]
-
-        wrapper.__wrapped__ = user_function
-        wrapper.cache_info = cache_info
-        wrapper.cache_clear = cache_clear
-        return update_wrapper(wrapper, user_function)
-
-    return decorating_function
diff --git a/libraries/cheroot/__init__.py b/libraries/cheroot/__init__.py
deleted file mode 100644
index a313660e..00000000
--- a/libraries/cheroot/__init__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""High-performance, pure-Python HTTP server used by CherryPy."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-__version__ = '6.4.0'
diff --git a/libraries/cheroot/__main__.py b/libraries/cheroot/__main__.py
deleted file mode 100644
index d2e27c10..00000000
--- a/libraries/cheroot/__main__.py
+++ /dev/null
@@ -1,6 +0,0 @@
-"""Stub for accessing the Cheroot CLI tool."""
-
-from .cli import main
-
-if __name__ == '__main__':
-    main()
diff --git a/libraries/cheroot/_compat.py b/libraries/cheroot/_compat.py
deleted file mode 100644
index e98f91f9..00000000
--- a/libraries/cheroot/_compat.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""Compatibility code for using Cheroot with various versions of Python."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import re
-
-import six
-
-if six.PY3:
-    def ntob(n, encoding='ISO-8859-1'):
-        """Return the native string as bytes in the given encoding."""
-        assert_native(n)
-        # In Python 3, the native string type is unicode
-        return n.encode(encoding)
-
-    def ntou(n, encoding='ISO-8859-1'):
-        """Return the native string as unicode with the given encoding."""
-        assert_native(n)
-        # In Python 3, the native string type is unicode
-        return n
-
-    def bton(b, encoding='ISO-8859-1'):
-        """Return the byte string as native string in the given encoding."""
-        return b.decode(encoding)
-else:
-    # Python 2
-    def ntob(n, encoding='ISO-8859-1'):
-        """Return the native string as bytes in the given encoding."""
-        assert_native(n)
-        # In Python 2, the native string type is bytes. Assume it's already
-        # in the given encoding, which for ISO-8859-1 is almost always what
-        # was intended.
-        return n
-
-    def ntou(n, encoding='ISO-8859-1'):
-        """Return the native string as unicode with the given encoding."""
-        assert_native(n)
-        # In Python 2, the native string type is bytes.
-        # First, check for the special encoding 'escape'. The test suite uses
-        # this to signal that it wants to pass a string with embedded \uXXXX
-        # escapes, but without having to prefix it with u'' for Python 2,
-        # but no prefix for Python 3.
-        if encoding == 'escape':
-            return six.u(
-                re.sub(r'\\u([0-9a-zA-Z]{4})',
-                       lambda m: six.unichr(int(m.group(1), 16)),
-                       n.decode('ISO-8859-1')))
-        # Assume it's already in the given encoding, which for ISO-8859-1
-        # is almost always what was intended.
-        return n.decode(encoding)
-
-    def bton(b, encoding='ISO-8859-1'):
-        """Return the byte string as native string in the given encoding."""
-        return b
-
-
-def assert_native(n):
-    """Check whether the input is of nativ ``str`` type.
-
-    Raises:
-        TypeError: in case of failed check
-
-    """
-    if not isinstance(n, str):
-        raise TypeError('n must be a native str (got %s)' % type(n).__name__)
diff --git a/libraries/cheroot/cli.py b/libraries/cheroot/cli.py
deleted file mode 100644
index 6d59fb5c..00000000
--- a/libraries/cheroot/cli.py
+++ /dev/null
@@ -1,233 +0,0 @@
-"""Command line tool for starting a Cheroot WSGI/HTTP server instance.
-
-Basic usage::
-
-    # Start a server on 127.0.0.1:8000 with the default settings
-    # for the WSGI app myapp/wsgi.py:application()
-    cheroot myapp.wsgi
-
-    # Start a server on 0.0.0.0:9000 with 8 threads
-    # for the WSGI app myapp/wsgi.py:main_app()
-    cheroot myapp.wsgi:main_app --bind 0.0.0.0:9000 --threads 8
-
-    # Start a server for the cheroot.server.Gateway subclass
-    # myapp/gateway.py:HTTPGateway
-    cheroot myapp.gateway:HTTPGateway
-
-    # Start a server on the UNIX socket /var/spool/myapp.sock
-    cheroot myapp.wsgi --bind /var/spool/myapp.sock
-
-    # Start a server on the abstract UNIX socket CherootServer
-    cheroot myapp.wsgi --bind @CherootServer
-"""
-
-import argparse
-from importlib import import_module
-import os
-import sys
-import contextlib
-
-import six
-
-from . import server
-from . import wsgi
-
-
-__metaclass__ = type
-
-
-class BindLocation:
-    """A class for storing the bind location for a Cheroot instance."""
-
-
-class TCPSocket(BindLocation):
-    """TCPSocket."""
-
-    def __init__(self, address, port):
-        """Initialize.
-
-        Args:
-            address (str): Host name or IP address
-            port (int): TCP port number
-        """
-        self.bind_addr = address, port
-
-
-class UnixSocket(BindLocation):
-    """UnixSocket."""
-
-    def __init__(self, path):
-        """Initialize."""
-        self.bind_addr = path
-
-
-class AbstractSocket(BindLocation):
-    """AbstractSocket."""
-
-    def __init__(self, addr):
-        """Initialize."""
-        self.bind_addr = '\0{}'.format(self.abstract_socket)
-
-
-class Application:
-    """Application."""
-
-    @classmethod
-    def resolve(cls, full_path):
-        """Read WSGI app/Gateway path string and import application module."""
-        mod_path, _, app_path = full_path.partition(':')
-        app = getattr(import_module(mod_path), app_path or 'application')
-
-        with contextlib.suppress(TypeError):
-            if issubclass(app, server.Gateway):
-                return GatewayYo(app)
-
-        return cls(app)
-
-    def __init__(self, wsgi_app):
-        """Initialize."""
-        if not callable(wsgi_app):
-            raise TypeError(
-                'Application must be a callable object or '
-                'cheroot.server.Gateway subclass'
-            )
-        self.wsgi_app = wsgi_app
-
-    def server_args(self, parsed_args):
-        """Return keyword args for Server class."""
-        args = {
-            arg: value
-            for arg, value in vars(parsed_args).items()
-            if not arg.startswith('_') and value is not None
-        }
-        args.update(vars(self))
-        return args
-
-    def server(self, parsed_args):
-        """Server."""
-        return wsgi.Server(**self.server_args(parsed_args))
-
-
-class GatewayYo:
-    """Gateway."""
-
-    def __init__(self, gateway):
-        """Init."""
-        self.gateway = gateway
-
-    def server(self, parsed_args):
-        """Server."""
-        server_args = vars(self)
-        server_args['bind_addr'] = parsed_args['bind_addr']
-        if parsed_args.max is not None:
-            server_args['maxthreads'] = parsed_args.max
-        if parsed_args.numthreads is not None:
-            server_args['minthreads'] = parsed_args.numthreads
-        return server.HTTPServer(**server_args)
-
-
-def parse_wsgi_bind_location(bind_addr_string):
-    """Convert bind address string to a BindLocation."""
-    # try and match for an IP/hostname and port
-    match = six.moves.urllib.parse.urlparse('//{}'.format(bind_addr_string))
-    try:
-        addr = match.hostname
-        port = match.port
-        if addr is not None or port is not None:
-            return TCPSocket(addr, port)
-    except ValueError:
-        pass
-
-    # else, assume a UNIX socket path
-    # if the string begins with an @ symbol, use an abstract socket
-    if bind_addr_string.startswith('@'):
-        return AbstractSocket(bind_addr_string[1:])
-    return UnixSocket(path=bind_addr_string)
-
-
-def parse_wsgi_bind_addr(bind_addr_string):
-    """Convert bind address string to bind address parameter."""
-    return parse_wsgi_bind_location(bind_addr_string).bind_addr
-
-
-_arg_spec = {
-    '_wsgi_app': dict(
-        metavar='APP_MODULE',
-        type=Application.resolve,
-        help='WSGI application callable or cheroot.server.Gateway subclass',
-    ),
-    '--bind': dict(
-        metavar='ADDRESS',
-        dest='bind_addr',
-        type=parse_wsgi_bind_addr,
-        default='[::1]:8000',
-        help='Network interface to listen on (default: [::1]:8000)',
-    ),
-    '--chdir': dict(
-        metavar='PATH',
-        type=os.chdir,
-        help='Set the working directory',
-    ),
-    '--server-name': dict(
-        dest='server_name',
-        type=str,
-        help='Web server name to be advertised via Server HTTP header',
-    ),
-    '--threads': dict(
-        metavar='INT',
-        dest='numthreads',
-        type=int,
-        help='Minimum number of worker threads',
-    ),
-    '--max-threads': dict(
-        metavar='INT',
-        dest='max',
-        type=int,
-        help='Maximum number of worker threads',
-    ),
-    '--timeout': dict(
-        metavar='INT',
-        dest='timeout',
-        type=int,
-        help='Timeout in seconds for accepted connections',
-    ),
-    '--shutdown-timeout': dict(
-        metavar='INT',
-        dest='shutdown_timeout',
-        type=int,
-        help='Time in seconds to wait for worker threads to cleanly exit',
-    ),
-    '--request-queue-size': dict(
-        metavar='INT',
-        dest='request_queue_size',
-        type=int,
-        help='Maximum number of queued connections',
-    ),
-    '--accepted-queue-size': dict(
-        metavar='INT',
-        dest='accepted_queue_size',
-        type=int,
-        help='Maximum number of active requests in queue',
-    ),
-    '--accepted-queue-timeout': dict(
-        metavar='INT',
-        dest='accepted_queue_timeout',
-        type=int,
-        help='Timeout in seconds for putting requests into queue',
-    ),
-}
-
-
-def main():
-    """Create a new Cheroot instance with arguments from the command line."""
-    parser = argparse.ArgumentParser(
-        description='Start an instance of the Cheroot WSGI/HTTP server.')
-    for arg, spec in _arg_spec.items():
-        parser.add_argument(arg, **spec)
-    raw_args = parser.parse_args()
-
-    # ensure cwd in sys.path
-    '' in sys.path or sys.path.insert(0, '')
-
-    # create a server based on the arguments provided
-    raw_args._wsgi_app.server(raw_args).safe_start()
diff --git a/libraries/cheroot/errors.py b/libraries/cheroot/errors.py
deleted file mode 100644
index 82412b42..00000000
--- a/libraries/cheroot/errors.py
+++ /dev/null
@@ -1,58 +0,0 @@
-"""Collection of exceptions raised and/or processed by Cheroot."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import errno
-import sys
-
-
-class MaxSizeExceeded(Exception):
-    """Exception raised when a client sends more data then acceptable within limit.
-
-    Depends on ``request.body.maxbytes`` config option if used within CherryPy
-    """
-
-
-class NoSSLError(Exception):
-    """Exception raised when a client speaks HTTP to an HTTPS socket."""
-
-
-class FatalSSLAlert(Exception):
-    """Exception raised when the SSL implementation signals a fatal alert."""
-
-
-def plat_specific_errors(*errnames):
-    """Return error numbers for all errors in errnames on this platform.
-
-    The 'errno' module contains different global constants depending on
-    the specific platform (OS). This function will return the list of
-    numeric values for a given list of potential names.
-    """
-    errno_names = dir(errno)
-    nums = [getattr(errno, k) for k in errnames if k in errno_names]
-    # de-dupe the list
-    return list(dict.fromkeys(nums).keys())
-
-
-socket_error_eintr = plat_specific_errors('EINTR', 'WSAEINTR')
-
-socket_errors_to_ignore = plat_specific_errors(
-    'EPIPE',
-    'EBADF', 'WSAEBADF',
-    'ENOTSOCK', 'WSAENOTSOCK',
-    'ETIMEDOUT', 'WSAETIMEDOUT',
-    'ECONNREFUSED', 'WSAECONNREFUSED',
-    'ECONNRESET', 'WSAECONNRESET',
-    'ECONNABORTED', 'WSAECONNABORTED',
-    'ENETRESET', 'WSAENETRESET',
-    'EHOSTDOWN', 'EHOSTUNREACH',
-)
-socket_errors_to_ignore.append('timed out')
-socket_errors_to_ignore.append('The read operation timed out')
-socket_errors_nonblocking = plat_specific_errors(
-    'EAGAIN', 'EWOULDBLOCK', 'WSAEWOULDBLOCK')
-
-if sys.platform == 'darwin':
-    socket_errors_to_ignore.extend(plat_specific_errors('EPROTOTYPE'))
-    socket_errors_nonblocking.extend(plat_specific_errors('EPROTOTYPE'))
diff --git a/libraries/cheroot/makefile.py b/libraries/cheroot/makefile.py
deleted file mode 100644
index a76f2eda..00000000
--- a/libraries/cheroot/makefile.py
+++ /dev/null
@@ -1,387 +0,0 @@
-"""Socket file object."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import socket
-
-try:
-    # prefer slower Python-based io module
-    import _pyio as io
-except ImportError:
-    # Python 2.6
-    import io
-
-import six
-
-from . import errors
-
-
-class BufferedWriter(io.BufferedWriter):
-    """Faux file object attached to a socket object."""
-
-    def write(self, b):
-        """Write bytes to buffer."""
-        self._checkClosed()
-        if isinstance(b, str):
-            raise TypeError("can't write str to binary stream")
-
-        with self._write_lock:
-            self._write_buf.extend(b)
-            self._flush_unlocked()
-            return len(b)
-
-    def _flush_unlocked(self):
-        self._checkClosed('flush of closed file')
-        while self._write_buf:
-            try:
-                # ssl sockets only except 'bytes', not bytearrays
-                # so perhaps we should conditionally wrap this for perf?
-                n = self.raw.write(bytes(self._write_buf))
-            except io.BlockingIOError as e:
-                n = e.characters_written
-            del self._write_buf[:n]
-
-
-def MakeFile_PY3(sock, mode='r', bufsize=io.DEFAULT_BUFFER_SIZE):
-    """File object attached to a socket object."""
-    if 'r' in mode:
-        return io.BufferedReader(socket.SocketIO(sock, mode), bufsize)
-    else:
-        return BufferedWriter(socket.SocketIO(sock, mode), bufsize)
-
-
-class MakeFile_PY2(getattr(socket, '_fileobject', object)):
-    """Faux file object attached to a socket object."""
-
-    def __init__(self, *args, **kwargs):
-        """Initialize faux file object."""
-        self.bytes_read = 0
-        self.bytes_written = 0
-        socket._fileobject.__init__(self, *args, **kwargs)
-
-    def write(self, data):
-        """Sendall for non-blocking sockets."""
-        while data:
-            try:
-                bytes_sent = self.send(data)
-                data = data[bytes_sent:]
-            except socket.error as e:
-                if e.args[0] not in errors.socket_errors_nonblocking:
-                    raise
-
-    def send(self, data):
-        """Send some part of message to the socket."""
-        bytes_sent = self._sock.send(data)
-        self.bytes_written += bytes_sent
-        return bytes_sent
-
-    def flush(self):
-        """Write all data from buffer to socket and reset write buffer."""
-        if self._wbuf:
-            buffer = ''.join(self._wbuf)
-            self._wbuf = []
-            self.write(buffer)
-
-    def recv(self, size):
-        """Receive message of a size from the socket."""
-        while True:
-            try:
-                data = self._sock.recv(size)
-                self.bytes_read += len(data)
-                return data
-            except socket.error as e:
-                what = (
-                    e.args[0] not in errors.socket_errors_nonblocking
-                    and e.args[0] not in errors.socket_error_eintr
-                )
-                if what:
-                    raise
-
-    class FauxSocket:
-        """Faux socket with the minimal interface required by pypy."""
-
-        def _reuse(self):
-            pass
-
-    _fileobject_uses_str_type = six.PY2 and isinstance(
-        socket._fileobject(FauxSocket())._rbuf, six.string_types)
-
-    # FauxSocket is no longer needed
-    del FauxSocket
-
-    if not _fileobject_uses_str_type:
-        def read(self, size=-1):
-            """Read data from the socket to buffer."""
-            # Use max, disallow tiny reads in a loop as they are very
-            # inefficient.
-            # We never leave read() with any leftover data from a new recv()
-            # call in our internal buffer.
-            rbufsize = max(self._rbufsize, self.default_bufsize)
-            # Our use of StringIO rather than lists of string objects returned
-            # by recv() minimizes memory usage and fragmentation that occurs
-            # when rbufsize is large compared to the typical return value of
-            # recv().
-            buf = self._rbuf
-            buf.seek(0, 2)  # seek end
-            if size < 0:
-                # Read until EOF
-                # reset _rbuf.  we consume it via buf.
-                self._rbuf = io.BytesIO()
-                while True:
-                    data = self.recv(rbufsize)
-                    if not data:
-                        break
-                    buf.write(data)
-                return buf.getvalue()
-            else:
-                # Read until size bytes or EOF seen, whichever comes first
-                buf_len = buf.tell()
-                if buf_len >= size:
-                    # Already have size bytes in our buffer?  Extract and
-                    # return.
-                    buf.seek(0)
-                    rv = buf.read(size)
-                    self._rbuf = io.BytesIO()
-                    self._rbuf.write(buf.read())
-                    return rv
-
-                # reset _rbuf.  we consume it via buf.
-                self._rbuf = io.BytesIO()
-                while True:
-                    left = size - buf_len
-                    # recv() will malloc the amount of memory given as its
-                    # parameter even though it often returns much less data
-                    # than that.  The returned data string is short lived
-                    # as we copy it into a StringIO and free it.  This avoids
-                    # fragmentation issues on many platforms.
-                    data = self.recv(left)
-                    if not data:
-                        break
-                    n = len(data)
-                    if n == size and not buf_len:
-                        # Shortcut.  Avoid buffer data copies when:
-                        # - We have no data in our buffer.
-                        # AND
-                        # - Our call to recv returned exactly the
-                        #   number of bytes we were asked to read.
-                        return data
-                    if n == left:
-                        buf.write(data)
-                        del data  # explicit free
-                        break
-                    assert n <= left, 'recv(%d) returned %d bytes' % (left, n)
-                    buf.write(data)
-                    buf_len += n
-                    del data  # explicit free
-                    # assert buf_len == buf.tell()
-                return buf.getvalue()
-
-        def readline(self, size=-1):
-            """Read line from the socket to buffer."""
-            buf = self._rbuf
-            buf.seek(0, 2)  # seek end
-            if buf.tell() > 0:
-                # check if we already have it in our buffer
-                buf.seek(0)
-                bline = buf.readline(size)
-                if bline.endswith('\n') or len(bline) == size:
-                    self._rbuf = io.BytesIO()
-                    self._rbuf.write(buf.read())
-                    return bline
-                del bline
-            if size < 0:
-                # Read until \n or EOF, whichever comes first
-                if self._rbufsize <= 1:
-                    # Speed up unbuffered case
-                    buf.seek(0)
-                    buffers = [buf.read()]
-                    # reset _rbuf.  we consume it via buf.
-                    self._rbuf = io.BytesIO()
-                    data = None
-                    recv = self.recv
-                    while data != '\n':
-                        data = recv(1)
-                        if not data:
-                            break
-                        buffers.append(data)
-                    return ''.join(buffers)
-
-                buf.seek(0, 2)  # seek end
-                # reset _rbuf.  we consume it via buf.
-                self._rbuf = io.BytesIO()
-                while True:
-                    data = self.recv(self._rbufsize)
-                    if not data:
-                        break
-                    nl = data.find('\n')
-                    if nl >= 0:
-                        nl += 1
-                        buf.write(data[:nl])
-                        self._rbuf.write(data[nl:])
-                        del data
-                        break
-                    buf.write(data)
-                return buf.getvalue()
-            else:
-                # Read until size bytes or \n or EOF seen, whichever comes
-                # first
-                buf.seek(0, 2)  # seek end
-                buf_len = buf.tell()
-                if buf_len >= size:
-                    buf.seek(0)
-                    rv = buf.read(size)
-                    self._rbuf = io.BytesIO()
-                    self._rbuf.write(buf.read())
-                    return rv
-                # reset _rbuf.  we consume it via buf.
-                self._rbuf = io.BytesIO()
-                while True:
-                    data = self.recv(self._rbufsize)
-                    if not data:
-                        break
-                    left = size - buf_len
-                    # did we just receive a newline?
-                    nl = data.find('\n', 0, left)
-                    if nl >= 0:
-                        nl += 1
-                        # save the excess data to _rbuf
-                        self._rbuf.write(data[nl:])
-                        if buf_len:
-                            buf.write(data[:nl])
-                            break
-                        else:
-                            # Shortcut.  Avoid data copy through buf when
-                            # returning a substring of our first recv().
-                            return data[:nl]
-                    n = len(data)
-                    if n == size and not buf_len:
-                        # Shortcut.  Avoid data copy through buf when
-                        # returning exactly all of our first recv().
-                        return data
-                    if n >= left:
-                        buf.write(data[:left])
-                        self._rbuf.write(data[left:])
-                        break
-                    buf.write(data)
-                    buf_len += n
-                    # assert buf_len == buf.tell()
-                return buf.getvalue()
-    else:
-        def read(self, size=-1):
-            """Read data from the socket to buffer."""
-            if size < 0:
-                # Read until EOF
-                buffers = [self._rbuf]
-                self._rbuf = ''
-                if self._rbufsize <= 1:
-                    recv_size = self.default_bufsize
-                else:
-                    recv_size = self._rbufsize
-
-                while True:
-                    data = self.recv(recv_size)
-                    if not data:
-                        break
-                    buffers.append(data)
-                return ''.join(buffers)
-            else:
-                # Read until size bytes or EOF seen, whichever comes first
-                data = self._rbuf
-                buf_len = len(data)
-                if buf_len >= size:
-                    self._rbuf = data[size:]
-                    return data[:size]
-                buffers = []
-                if data:
-                    buffers.append(data)
-                self._rbuf = ''
-                while True:
-                    left = size - buf_len
-                    recv_size = max(self._rbufsize, left)
-                    data = self.recv(recv_size)
-                    if not data:
-                        break
-                    buffers.append(data)
-                    n = len(data)
-                    if n >= left:
-                        self._rbuf = data[left:]
-                        buffers[-1] = data[:left]
-                        break
-                    buf_len += n
-                return ''.join(buffers)
-
-        def readline(self, size=-1):
-            """Read line from the socket to buffer."""
-            data = self._rbuf
-            if size < 0:
-                # Read until \n or EOF, whichever comes first
-                if self._rbufsize <= 1:
-                    # Speed up unbuffered case
-                    assert data == ''
-                    buffers = []
-                    while data != '\n':
-                        data = self.recv(1)
-                        if not data:
-                            break
-                        buffers.append(data)
-                    return ''.join(buffers)
-                nl = data.find('\n')
-                if nl >= 0:
-                    nl += 1
-                    self._rbuf = data[nl:]
-                    return data[:nl]
-                buffers = []
-                if data:
-                    buffers.append(data)
-                self._rbuf = ''
-                while True:
-                    data = self.recv(self._rbufsize)
-                    if not data:
-                        break
-                    buffers.append(data)
-                    nl = data.find('\n')
-                    if nl >= 0:
-                        nl += 1
-                        self._rbuf = data[nl:]
-                        buffers[-1] = data[:nl]
-                        break
-                return ''.join(buffers)
-            else:
-                # Read until size bytes or \n or EOF seen, whichever comes
-                # first
-                nl = data.find('\n', 0, size)
-                if nl >= 0:
-                    nl += 1
-                    self._rbuf = data[nl:]
-                    return data[:nl]
-                buf_len = len(data)
-                if buf_len >= size:
-                    self._rbuf = data[size:]
-                    return data[:size]
-                buffers = []
-                if data:
-                    buffers.append(data)
-                self._rbuf = ''
-                while True:
-                    data = self.recv(self._rbufsize)
-                    if not data:
-                        break
-                    buffers.append(data)
-                    left = size - buf_len
-                    nl = data.find('\n', 0, left)
-                    if nl >= 0:
-                        nl += 1
-                        self._rbuf = data[nl:]
-                        buffers[-1] = data[:nl]
-                        break
-                    n = len(data)
-                    if n >= left:
-                        self._rbuf = data[left:]
-                        buffers[-1] = data[:left]
-                        break
-                    buf_len += n
-                return ''.join(buffers)
-
-
-MakeFile = MakeFile_PY2 if six.PY2 else MakeFile_PY3
diff --git a/libraries/cheroot/server.py b/libraries/cheroot/server.py
deleted file mode 100644
index 44070490..00000000
--- a/libraries/cheroot/server.py
+++ /dev/null
@@ -1,2001 +0,0 @@
-"""
-A high-speed, production ready, thread pooled, generic HTTP server.
-
-For those of you wanting to understand internals of this module, here's the
-basic call flow. The server's listening thread runs a very tight loop,
-sticking incoming connections onto a Queue::
-
-    server = HTTPServer(...)
-    server.start()
-    ->  while True:
-            tick()
-            # This blocks until a request comes in:
-            child = socket.accept()
-            conn = HTTPConnection(child, ...)
-            server.requests.put(conn)
-
-Worker threads are kept in a pool and poll the Queue, popping off and then
-handling each connection in turn. Each connection can consist of an arbitrary
-number of requests and their responses, so we run a nested loop::
-
-    while True:
-        conn = server.requests.get()
-        conn.communicate()
-        ->  while True:
-                req = HTTPRequest(...)
-                req.parse_request()
-                ->  # Read the Request-Line, e.g. "GET /page HTTP/1.1"
-                    req.rfile.readline()
-                    read_headers(req.rfile, req.inheaders)
-                req.respond()
-                ->  response = app(...)
-                    try:
-                        for chunk in response:
-                            if chunk:
-                                req.write(chunk)
-                    finally:
-                        if hasattr(response, "close"):
-                            response.close()
-                if req.close_connection:
-                    return
-
-For running a server you can invoke :func:`start() <HTTPServer.start()>` (it
-will run the server forever) or use invoking :func:`prepare()
-<HTTPServer.prepare()>` and :func:`serve() <HTTPServer.serve()>` like this::
-
-    server = HTTPServer(...)
-    server.prepare()
-    try:
-        threading.Thread(target=server.serve).start()
-
-        # waiting/detecting some appropriate stop condition here
-        ...
-
-    finally:
-        server.stop()
-
-And now for a trivial doctest to exercise the test suite
-
->>> 'HTTPServer' in globals()
-True
-
-"""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import os
-import io
-import re
-import email.utils
-import socket
-import sys
-import time
-import traceback as traceback_
-import logging
-import platform
-import xbmc
-
-try:
-    from functools import lru_cache
-except ImportError:
-    from backports.functools_lru_cache import lru_cache
-
-import six
-from six.moves import queue
-from six.moves import urllib
-
-from . import errors, __version__
-from ._compat import bton, ntou
-from .workers import threadpool
-from .makefile import MakeFile
-
-
-__all__ = ('HTTPRequest', 'HTTPConnection', 'HTTPServer',
-           'SizeCheckWrapper', 'KnownLengthRFile', 'ChunkedRFile',
-           'Gateway', 'get_ssl_adapter_class')
-
-"""
-Special KODI case:
-Android does not have support for grp and pwd
-But Python has issues reporting that this is running on Android (it shows as Linux2).
-We're instead using xbmc library to detect that.
-"""
-IS_WINDOWS = platform.system() == 'Windows'
-IS_ANDROID = xbmc.getCondVisibility('system.platform.linux') and xbmc.getCondVisibility('system.platform.android')
-
-if not (IS_WINDOWS or IS_ANDROID):
-    import grp
-    import pwd
-    import struct
-
-
-if IS_WINDOWS and hasattr(socket, 'AF_INET6'):
-    if not hasattr(socket, 'IPPROTO_IPV6'):
-        socket.IPPROTO_IPV6 = 41
-    if not hasattr(socket, 'IPV6_V6ONLY'):
-        socket.IPV6_V6ONLY = 27
-
-
-if not hasattr(socket, 'SO_PEERCRED'):
-    """
-    NOTE: the value for SO_PEERCRED can be architecture specific, in
-    which case the getsockopt() will hopefully fail. The arch
-    specific value could be derived from platform.processor()
-    """
-    socket.SO_PEERCRED = 17
-
-
-LF = b'\n'
-CRLF = b'\r\n'
-TAB = b'\t'
-SPACE = b' '
-COLON = b':'
-SEMICOLON = b';'
-EMPTY = b''
-ASTERISK = b'*'
-FORWARD_SLASH = b'/'
-QUOTED_SLASH = b'%2F'
-QUOTED_SLASH_REGEX = re.compile(b'(?i)' + QUOTED_SLASH)
-
-
-comma_separated_headers = [
-    b'Accept', b'Accept-Charset', b'Accept-Encoding',
-    b'Accept-Language', b'Accept-Ranges', b'Allow', b'Cache-Control',
-    b'Connection', b'Content-Encoding', b'Content-Language', b'Expect',
-    b'If-Match', b'If-None-Match', b'Pragma', b'Proxy-Authenticate', b'TE',
-    b'Trailer', b'Transfer-Encoding', b'Upgrade', b'Vary', b'Via', b'Warning',
-    b'WWW-Authenticate',
-]
-
-
-if not hasattr(logging, 'statistics'):
-    logging.statistics = {}
-
-
-class HeaderReader:
-    """Object for reading headers from an HTTP request.
-
-    Interface and default implementation.
-    """
-
-    def __call__(self, rfile, hdict=None):
-        """
-        Read headers from the given stream into the given header dict.
-
-        If hdict is None, a new header dict is created. Returns the populated
-        header dict.
-
-        Headers which are repeated are folded together using a comma if their
-        specification so dictates.
-
-        This function raises ValueError when the read bytes violate the HTTP
-        spec.
-        You should probably return "400 Bad Request" if this happens.
-        """
-        if hdict is None:
-            hdict = {}
-
-        while True:
-            line = rfile.readline()
-            if not line:
-                # No more data--illegal end of headers
-                raise ValueError('Illegal end of headers.')
-
-            if line == CRLF:
-                # Normal end of headers
-                break
-            if not line.endswith(CRLF):
-                raise ValueError('HTTP requires CRLF terminators')
-
-            if line[0] in (SPACE, TAB):
-                # It's a continuation line.
-                v = line.strip()
-            else:
-                try:
-                    k, v = line.split(COLON, 1)
-                except ValueError:
-                    raise ValueError('Illegal header line.')
-                v = v.strip()
-                k = self._transform_key(k)
-                hname = k
-
-            if not self._allow_header(k):
-                continue
-
-            if k in comma_separated_headers:
-                existing = hdict.get(hname)
-                if existing:
-                    v = b', '.join((existing, v))
-            hdict[hname] = v
-
-        return hdict
-
-    def _allow_header(self, key_name):
-        return True
-
-    def _transform_key(self, key_name):
-        # TODO: what about TE and WWW-Authenticate?
-        return key_name.strip().title()
-
-
-class DropUnderscoreHeaderReader(HeaderReader):
-    """Custom HeaderReader to exclude any headers with underscores in them."""
-
-    def _allow_header(self, key_name):
-        orig = super(DropUnderscoreHeaderReader, self)._allow_header(key_name)
-        return orig and '_' not in key_name
-
-
-class SizeCheckWrapper:
-    """Wraps a file-like object, raising MaxSizeExceeded if too large."""
-
-    def __init__(self, rfile, maxlen):
-        """Initialize SizeCheckWrapper instance.
-
-        Args:
-            rfile (file): file of a limited size
-            maxlen (int): maximum length of the file being read
-        """
-        self.rfile = rfile
-        self.maxlen = maxlen
-        self.bytes_read = 0
-
-    def _check_length(self):
-        if self.maxlen and self.bytes_read > self.maxlen:
-            raise errors.MaxSizeExceeded()
-
-    def read(self, size=None):
-        """Read a chunk from rfile buffer and return it.
-
-        Args:
-            size (int): amount of data to read
-
-        Returns:
-            bytes: Chunk from rfile, limited by size if specified.
-
-        """
-        data = self.rfile.read(size)
-        self.bytes_read += len(data)
-        self._check_length()
-        return data
-
-    def readline(self, size=None):
-        """Read a single line from rfile buffer and return it.
-
-        Args:
-            size (int): minimum amount of data to read
-
-        Returns:
-            bytes: One line from rfile.
-
-        """
-        if size is not None:
-            data = self.rfile.readline(size)
-            self.bytes_read += len(data)
-            self._check_length()
-            return data
-
-        # User didn't specify a size ...
-        # We read the line in chunks to make sure it's not a 100MB line !
-        res = []
-        while True:
-            data = self.rfile.readline(256)
-            self.bytes_read += len(data)
-            self._check_length()
-            res.append(data)
-            # See https://github.com/cherrypy/cherrypy/issues/421
-            if len(data) < 256 or data[-1:] == LF:
-                return EMPTY.join(res)
-
-    def readlines(self, sizehint=0):
-        """Read all lines from rfile buffer and return them.
-
-        Args:
-            sizehint (int): hint of minimum amount of data to read
-
-        Returns:
-            list[bytes]: Lines of bytes read from rfile.
-
-        """
-        # Shamelessly stolen from StringIO
-        total = 0
-        lines = []
-        line = self.readline(sizehint)
-        while line:
-            lines.append(line)
-            total += len(line)
-            if 0 < sizehint <= total:
-                break
-            line = self.readline(sizehint)
-        return lines
-
-    def close(self):
-        """Release resources allocated for rfile."""
-        self.rfile.close()
-
-    def __iter__(self):
-        """Return file iterator."""
-        return self
-
-    def __next__(self):
-        """Generate next file chunk."""
-        data = next(self.rfile)
-        self.bytes_read += len(data)
-        self._check_length()
-        return data
-
-    next = __next__
-
-
-class KnownLengthRFile:
-    """Wraps a file-like object, returning an empty string when exhausted."""
-
-    def __init__(self, rfile, content_length):
-        """Initialize KnownLengthRFile instance.
-
-        Args:
-            rfile (file): file of a known size
-            content_length (int): length of the file being read
-
-        """
-        self.rfile = rfile
-        self.remaining = content_length
-
-    def read(self, size=None):
-        """Read a chunk from rfile buffer and return it.
-
-        Args:
-            size (int): amount of data to read
-
-        Returns:
-            bytes: Chunk from rfile, limited by size if specified.
-
-        """
-        if self.remaining == 0:
-            return b''
-        if size is None:
-            size = self.remaining
-        else:
-            size = min(size, self.remaining)
-
-        data = self.rfile.read(size)
-        self.remaining -= len(data)
-        return data
-
-    def readline(self, size=None):
-        """Read a single line from rfile buffer and return it.
-
-        Args:
-            size (int): minimum amount of data to read
-
-        Returns:
-            bytes: One line from rfile.
-
-        """
-        if self.remaining == 0:
-            return b''
-        if size is None:
-            size = self.remaining
-        else:
-            size = min(size, self.remaining)
-
-        data = self.rfile.readline(size)
-        self.remaining -= len(data)
-        return data
-
-    def readlines(self, sizehint=0):
-        """Read all lines from rfile buffer and return them.
-
-        Args:
-            sizehint (int): hint of minimum amount of data to read
-
-        Returns:
-            list[bytes]: Lines of bytes read from rfile.
-
-        """
-        # Shamelessly stolen from StringIO
-        total = 0
-        lines = []
-        line = self.readline(sizehint)
-        while line:
-            lines.append(line)
-            total += len(line)
-            if 0 < sizehint <= total:
-                break
-            line = self.readline(sizehint)
-        return lines
-
-    def close(self):
-        """Release resources allocated for rfile."""
-        self.rfile.close()
-
-    def __iter__(self):
-        """Return file iterator."""
-        return self
-
-    def __next__(self):
-        """Generate next file chunk."""
-        data = next(self.rfile)
-        self.remaining -= len(data)
-        return data
-
-    next = __next__
-
-
-class ChunkedRFile:
-    """Wraps a file-like object, returning an empty string when exhausted.
-
-    This class is intended to provide a conforming wsgi.input value for
-    request entities that have been encoded with the 'chunked' transfer
-    encoding.
-    """
-
-    def __init__(self, rfile, maxlen, bufsize=8192):
-        """Initialize ChunkedRFile instance.
-
-        Args:
-            rfile (file): file encoded with the 'chunked' transfer encoding
-            maxlen (int): maximum length of the file being read
-            bufsize (int): size of the buffer used to read the file
-        """
-        self.rfile = rfile
-        self.maxlen = maxlen
-        self.bytes_read = 0
-        self.buffer = EMPTY
-        self.bufsize = bufsize
-        self.closed = False
-
-    def _fetch(self):
-        if self.closed:
-            return
-
-        line = self.rfile.readline()
-        self.bytes_read += len(line)
-
-        if self.maxlen and self.bytes_read > self.maxlen:
-            raise errors.MaxSizeExceeded(
-                'Request Entity Too Large', self.maxlen)
-
-        line = line.strip().split(SEMICOLON, 1)
-
-        try:
-            chunk_size = line.pop(0)
-            chunk_size = int(chunk_size, 16)
-        except ValueError:
-            raise ValueError('Bad chunked transfer size: ' + repr(chunk_size))
-
-        if chunk_size <= 0:
-            self.closed = True
-            return
-
-#            if line: chunk_extension = line[0]
-
-        if self.maxlen and self.bytes_read + chunk_size > self.maxlen:
-            raise IOError('Request Entity Too Large')
-
-        chunk = self.rfile.read(chunk_size)
-        self.bytes_read += len(chunk)
-        self.buffer += chunk
-
-        crlf = self.rfile.read(2)
-        if crlf != CRLF:
-            raise ValueError(
-                "Bad chunked transfer coding (expected '\\r\\n', "
-                'got ' + repr(crlf) + ')')
-
-    def read(self, size=None):
-        """Read a chunk from rfile buffer and return it.
-
-        Args:
-            size (int): amount of data to read
-
-        Returns:
-            bytes: Chunk from rfile, limited by size if specified.
-
-        """
-        data = EMPTY
-
-        if size == 0:
-            return data
-
-        while True:
-            if size and len(data) >= size:
-                return data
-
-            if not self.buffer:
-                self._fetch()
-                if not self.buffer:
-                    # EOF
-                    return data
-
-            if size:
-                remaining = size - len(data)
-                data += self.buffer[:remaining]
-                self.buffer = self.buffer[remaining:]
-            else:
-                data += self.buffer
-                self.buffer = EMPTY
-
-    def readline(self, size=None):
-        """Read a single line from rfile buffer and return it.
-
-        Args:
-            size (int): minimum amount of data to read
-
-        Returns:
-            bytes: One line from rfile.
-
-        """
-        data = EMPTY
-
-        if size == 0:
-            return data
-
-        while True:
-            if size and len(data) >= size:
-                return data
-
-            if not self.buffer:
-                self._fetch()
-                if not self.buffer:
-                    # EOF
-                    return data
-
-            newline_pos = self.buffer.find(LF)
-            if size:
-                if newline_pos == -1:
-                    remaining = size - len(data)
-                    data += self.buffer[:remaining]
-                    self.buffer = self.buffer[remaining:]
-                else:
-                    remaining = min(size - len(data), newline_pos)
-                    data += self.buffer[:remaining]
-                    self.buffer = self.buffer[remaining:]
-            else:
-                if newline_pos == -1:
-                    data += self.buffer
-                    self.buffer = EMPTY
-                else:
-                    data += self.buffer[:newline_pos]
-                    self.buffer = self.buffer[newline_pos:]
-
-    def readlines(self, sizehint=0):
-        """Read all lines from rfile buffer and return them.
-
-        Args:
-            sizehint (int): hint of minimum amount of data to read
-
-        Returns:
-            list[bytes]: Lines of bytes read from rfile.
-
-        """
-        # Shamelessly stolen from StringIO
-        total = 0
-        lines = []
-        line = self.readline(sizehint)
-        while line:
-            lines.append(line)
-            total += len(line)
-            if 0 < sizehint <= total:
-                break
-            line = self.readline(sizehint)
-        return lines
-
-    def read_trailer_lines(self):
-        """Read HTTP headers and yield them.
-
-        Returns:
-            Generator: yields CRLF separated lines.
-
-        """
-        if not self.closed:
-            raise ValueError(
-                'Cannot read trailers until the request body has been read.')
-
-        while True:
-            line = self.rfile.readline()
-            if not line:
-                # No more data--illegal end of headers
-                raise ValueError('Illegal end of headers.')
-
-            self.bytes_read += len(line)
-            if self.maxlen and self.bytes_read > self.maxlen:
-                raise IOError('Request Entity Too Large')
-
-            if line == CRLF:
-                # Normal end of headers
-                break
-            if not line.endswith(CRLF):
-                raise ValueError('HTTP requires CRLF terminators')
-
-            yield line
-
-    def close(self):
-        """Release resources allocated for rfile."""
-        self.rfile.close()
-
-
-class HTTPRequest:
-    """An HTTP Request (and response).
-
-    A single HTTP connection may consist of multiple request/response pairs.
-    """
-
-    server = None
-    """The HTTPServer object which is receiving this request."""
-
-    conn = None
-    """The HTTPConnection object on which this request connected."""
-
-    inheaders = {}
-    """A dict of request headers."""
-
-    outheaders = []
-    """A list of header tuples to write in the response."""
-
-    ready = False
-    """When True, the request has been parsed and is ready to begin generating
-    the response. When False, signals the calling Connection that the response
-    should not be generated and the connection should close."""
-
-    close_connection = False
-    """Signals the calling Connection that the request should close. This does
-    not imply an error! The client and/or server may each request that the
-    connection be closed."""
-
-    chunked_write = False
-    """If True, output will be encoded with the "chunked" transfer-coding.
-
-    This value is set automatically inside send_headers."""
-
-    header_reader = HeaderReader()
-    """
-    A HeaderReader instance or compatible reader.
-    """
-
-    def __init__(self, server, conn, proxy_mode=False, strict_mode=True):
-        """Initialize HTTP request container instance.
-
-        Args:
-            server (HTTPServer): web server object receiving this request
-            conn (HTTPConnection): HTTP connection object for this request
-            proxy_mode (bool): whether this HTTPServer should behave as a PROXY
-            server for certain requests
-            strict_mode (bool): whether we should return a 400 Bad Request when
-            we encounter a request that a HTTP compliant client should not be
-            making
-        """
-        self.server = server
-        self.conn = conn
-
-        self.ready = False
-        self.started_request = False
-        self.scheme = b'http'
-        if self.server.ssl_adapter is not None:
-            self.scheme = b'https'
-        # Use the lowest-common protocol in case read_request_line errors.
-        self.response_protocol = 'HTTP/1.0'
-        self.inheaders = {}
-
-        self.status = ''
-        self.outheaders = []
-        self.sent_headers = False
-        self.close_connection = self.__class__.close_connection
-        self.chunked_read = False
-        self.chunked_write = self.__class__.chunked_write
-        self.proxy_mode = proxy_mode
-        self.strict_mode = strict_mode
-
-    def parse_request(self):
-        """Parse the next HTTP request start-line and message-headers."""
-        self.rfile = SizeCheckWrapper(self.conn.rfile,
-                                      self.server.max_request_header_size)
-        try:
-            success = self.read_request_line()
-        except errors.MaxSizeExceeded:
-            self.simple_response(
-                '414 Request-URI Too Long',
-                'The Request-URI sent with the request exceeds the maximum '
-                'allowed bytes.')
-            return
-        else:
-            if not success:
-                return
-
-        try:
-            success = self.read_request_headers()
-        except errors.MaxSizeExceeded:
-            self.simple_response(
-                '413 Request Entity Too Large',
-                'The headers sent with the request exceed the maximum '
-                'allowed bytes.')
-            return
-        else:
-            if not success:
-                return
-
-        self.ready = True
-
-    def read_request_line(self):
-        """Read and parse first line of the HTTP request.
-
-        Returns:
-            bool: True if the request line is valid or False if it's malformed.
-
-        """
-        # HTTP/1.1 connections are persistent by default. If a client
-        # requests a page, then idles (leaves the connection open),
-        # then rfile.readline() will raise socket.error("timed out").
-        # Note that it does this based on the value given to settimeout(),
-        # and doesn't need the client to request or acknowledge the close
-        # (although your TCP stack might suffer for it: cf Apache's history
-        # with FIN_WAIT_2).
-        request_line = self.rfile.readline()
-
-        # Set started_request to True so communicate() knows to send 408
-        # from here on out.
-        self.started_request = True
-        if not request_line:
-            return False
-
-        if request_line == CRLF:
-            # RFC 2616 sec 4.1: "...if the server is reading the protocol
-            # stream at the beginning of a message and receives a CRLF
-            # first, it should ignore the CRLF."
-            # But only ignore one leading line! else we enable a DoS.
-            request_line = self.rfile.readline()
-            if not request_line:
-                return False
-
-        if not request_line.endswith(CRLF):
-            self.simple_response(
-                '400 Bad Request', 'HTTP requires CRLF terminators')
-            return False
-
-        try:
-            method, uri, req_protocol = request_line.strip().split(SPACE, 2)
-            if not req_protocol.startswith(b'HTTP/'):
-                self.simple_response(
-                    '400 Bad Request', 'Malformed Request-Line: bad protocol'
-                )
-                return False
-            rp = req_protocol[5:].split(b'.', 1)
-            rp = tuple(map(int, rp))  # Minor.Major must be threat as integers
-            if rp > (1, 1):
-                self.simple_response(
-                    '505 HTTP Version Not Supported', 'Cannot fulfill request'
-                )
-                return False
-        except (ValueError, IndexError):
-            self.simple_response('400 Bad Request', 'Malformed Request-Line')
-            return False
-
-        self.uri = uri
-        self.method = method.upper()
-
-        if self.strict_mode and method != self.method:
-            resp = (
-                'Malformed method name: According to RFC 2616 '
-                '(section 5.1.1) and its successors '
-                'RFC 7230 (section 3.1.1) and RFC 7231 (section 4.1) '
-                'method names are case-sensitive and uppercase.'
-            )
-            self.simple_response('400 Bad Request', resp)
-            return False
-
-        try:
-            if six.PY2:  # FIXME: Figure out better way to do this
-                # Ref: https://stackoverflow.com/a/196392/595220 (like this?)
-                """This is a dummy check for unicode in URI."""
-                ntou(bton(uri, 'ascii'), 'ascii')
-            scheme, authority, path, qs, fragment = urllib.parse.urlsplit(uri)
-        except UnicodeError:
-            self.simple_response('400 Bad Request', 'Malformed Request-URI')
-            return False
-
-        if self.method == b'OPTIONS':
-            # TODO: cover this branch with tests
-            path = (uri
-                    # https://tools.ietf.org/html/rfc7230#section-5.3.4
-                    if self.proxy_mode or uri == ASTERISK
-                    else path)
-        elif self.method == b'CONNECT':
-            # TODO: cover this branch with tests
-            if not self.proxy_mode:
-                self.simple_response('405 Method Not Allowed')
-                return False
-
-            # `urlsplit()` above parses "example.com:3128" as path part of URI.
-            # this is a workaround, which makes it detect netloc correctly
-            uri_split = urllib.parse.urlsplit(b'//' + uri)
-            _scheme, _authority, _path, _qs, _fragment = uri_split
-            _port = EMPTY
-            try:
-                _port = uri_split.port
-            except ValueError:
-                pass
-
-            # FIXME: use third-party validation to make checks against RFC
-            # the validation doesn't take into account, that urllib parses
-            # invalid URIs without raising errors
-            # https://tools.ietf.org/html/rfc7230#section-5.3.3
-            invalid_path = (
-                _authority != uri
-                or not _port
-                or any((_scheme, _path, _qs, _fragment))
-            )
-            if invalid_path:
-                self.simple_response('400 Bad Request',
-                                     'Invalid path in Request-URI: request-'
-                                     'target must match authority-form.')
-                return False
-
-            authority = path = _authority
-            scheme = qs = fragment = EMPTY
-        else:
-            uri_is_absolute_form = (scheme or authority)
-
-            disallowed_absolute = (
-                self.strict_mode
-                and not self.proxy_mode
-                and uri_is_absolute_form
-            )
-            if disallowed_absolute:
-                # https://tools.ietf.org/html/rfc7230#section-5.3.2
-                # (absolute form)
-                """Absolute URI is only allowed within proxies."""
-                self.simple_response(
-                    '400 Bad Request',
-                    'Absolute URI not allowed if server is not a proxy.',
-                )
-                return False
-
-            invalid_path = (
-                self.strict_mode
-                and not uri.startswith(FORWARD_SLASH)
-                and not uri_is_absolute_form
-            )
-            if invalid_path:
-                # https://tools.ietf.org/html/rfc7230#section-5.3.1
-                # (origin_form) and
-                """Path should start with a forward slash."""
-                resp = (
-                    'Invalid path in Request-URI: request-target must contain '
-                    'origin-form which starts with absolute-path (URI '
-                    'starting with a slash "/").'
-                )
-                self.simple_response('400 Bad Request', resp)
-                return False
-
-            if fragment:
-                self.simple_response('400 Bad Request',
-                                     'Illegal #fragment in Request-URI.')
-                return False
-
-            if path is None:
-                # FIXME: It looks like this case cannot happen
-                self.simple_response('400 Bad Request',
-                                     'Invalid path in Request-URI.')
-                return False
-
-            # Unquote the path+params (e.g. "/this%20path" -> "/this path").
-            # https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2
-            #
-            # But note that "...a URI must be separated into its components
-            # before the escaped characters within those components can be
-            # safely decoded." https://www.ietf.org/rfc/rfc2396.txt, sec 2.4.2
-            # Therefore, "/this%2Fpath" becomes "/this%2Fpath", not
-            # "/this/path".
-            try:
-                # TODO: Figure out whether exception can really happen here.
-                # It looks like it's caught on urlsplit() call above.
-                atoms = [
-                    urllib.parse.unquote_to_bytes(x)
-                    for x in QUOTED_SLASH_REGEX.split(path)
-                ]
-            except ValueError as ex:
-                self.simple_response('400 Bad Request', ex.args[0])
-                return False
-            path = QUOTED_SLASH.join(atoms)
-
-        if not path.startswith(FORWARD_SLASH):
-            path = FORWARD_SLASH + path
-
-        if scheme is not EMPTY:
-            self.scheme = scheme
-        self.authority = authority
-        self.path = path
-
-        # Note that, like wsgiref and most other HTTP servers,
-        # we "% HEX HEX"-unquote the path but not the query string.
-        self.qs = qs
-
-        # Compare request and server HTTP protocol versions, in case our
-        # server does not support the requested protocol. Limit our output
-        # to min(req, server). We want the following output:
-        #     request    server     actual written   supported response
-        #     protocol   protocol  response protocol    feature set
-        # a     1.0        1.0           1.0                1.0
-        # b     1.0        1.1           1.1                1.0
-        # c     1.1        1.0           1.0                1.0
-        # d     1.1        1.1           1.1                1.1
-        # Notice that, in (b), the response will be "HTTP/1.1" even though
-        # the client only understands 1.0. RFC 2616 10.5.6 says we should
-        # only return 505 if the _major_ version is different.
-        sp = int(self.server.protocol[5]), int(self.server.protocol[7])
-
-        if sp[0] != rp[0]:
-            self.simple_response('505 HTTP Version Not Supported')
-            return False
-
-        self.request_protocol = req_protocol
-        self.response_protocol = 'HTTP/%s.%s' % min(rp, sp)
-
-        return True
-
-    def read_request_headers(self):
-        """Read self.rfile into self.inheaders. Return success."""
-        # then all the http headers
-        try:
-            self.header_reader(self.rfile, self.inheaders)
-        except ValueError as ex:
-            self.simple_response('400 Bad Request', ex.args[0])
-            return False
-
-        mrbs = self.server.max_request_body_size
-
-        try:
-            cl = int(self.inheaders.get(b'Content-Length', 0))
-        except ValueError:
-            self.simple_response(
-                '400 Bad Request',
-                'Malformed Content-Length Header.')
-            return False
-
-        if mrbs and cl > mrbs:
-            self.simple_response(
-                '413 Request Entity Too Large',
-                'The entity sent with the request exceeds the maximum '
-                'allowed bytes.')
-            return False
-
-        # Persistent connection support
-        if self.response_protocol == 'HTTP/1.1':
-            # Both server and client are HTTP/1.1
-            if self.inheaders.get(b'Connection', b'') == b'close':
-                self.close_connection = True
-        else:
-            # Either the server or client (or both) are HTTP/1.0
-            if self.inheaders.get(b'Connection', b'') != b'Keep-Alive':
-                self.close_connection = True
-
-        # Transfer-Encoding support
-        te = None
-        if self.response_protocol == 'HTTP/1.1':
-            te = self.inheaders.get(b'Transfer-Encoding')
-            if te:
-                te = [x.strip().lower() for x in te.split(b',') if x.strip()]
-
-        self.chunked_read = False
-
-        if te:
-            for enc in te:
-                if enc == b'chunked':
-                    self.chunked_read = True
-                else:
-                    # Note that, even if we see "chunked", we must reject
-                    # if there is an extension we don't recognize.
-                    self.simple_response('501 Unimplemented')
-                    self.close_connection = True
-                    return False
-
-        # From PEP 333:
-        # "Servers and gateways that implement HTTP 1.1 must provide
-        # transparent support for HTTP 1.1's "expect/continue" mechanism.
-        # This may be done in any of several ways:
-        #   1. Respond to requests containing an Expect: 100-continue request
-        #      with an immediate "100 Continue" response, and proceed normally.
-        #   2. Proceed with the request normally, but provide the application
-        #      with a wsgi.input stream that will send the "100 Continue"
-        #      response if/when the application first attempts to read from
-        #      the input stream. The read request must then remain blocked
-        #      until the client responds.
-        #   3. Wait until the client decides that the server does not support
-        #      expect/continue, and sends the request body on its own.
-        #      (This is suboptimal, and is not recommended.)
-        #
-        # We used to do 3, but are now doing 1. Maybe we'll do 2 someday,
-        # but it seems like it would be a big slowdown for such a rare case.
-        if self.inheaders.get(b'Expect', b'') == b'100-continue':
-            # Don't use simple_response here, because it emits headers
-            # we don't want. See
-            # https://github.com/cherrypy/cherrypy/issues/951
-            msg = self.server.protocol.encode('ascii')
-            msg += b' 100 Continue\r\n\r\n'
-            try:
-                self.conn.wfile.write(msg)
-            except socket.error as ex:
-                if ex.args[0] not in errors.socket_errors_to_ignore:
-                    raise
-        return True
-
-    def respond(self):
-        """Call the gateway and write its iterable output."""
-        mrbs = self.server.max_request_body_size
-        if self.chunked_read:
-            self.rfile = ChunkedRFile(self.conn.rfile, mrbs)
-        else:
-            cl = int(self.inheaders.get(b'Content-Length', 0))
-            if mrbs and mrbs < cl:
-                if not self.sent_headers:
-                    self.simple_response(
-                        '413 Request Entity Too Large',
-                        'The entity sent with the request exceeds the '
-                        'maximum allowed bytes.')
-                return
-            self.rfile = KnownLengthRFile(self.conn.rfile, cl)
-
-        self.server.gateway(self).respond()
-        self.ready and self.ensure_headers_sent()
-
-        if self.chunked_write:
-            self.conn.wfile.write(b'0\r\n\r\n')
-
-    def simple_response(self, status, msg=''):
-        """Write a simple response back to the client."""
-        status = str(status)
-        proto_status = '%s %s\r\n' % (self.server.protocol, status)
-        content_length = 'Content-Length: %s\r\n' % len(msg)
-        content_type = 'Content-Type: text/plain\r\n'
-        buf = [
-            proto_status.encode('ISO-8859-1'),
-            content_length.encode('ISO-8859-1'),
-            content_type.encode('ISO-8859-1'),
-        ]
-
-        if status[:3] in ('413', '414'):
-            # Request Entity Too Large / Request-URI Too Long
-            self.close_connection = True
-            if self.response_protocol == 'HTTP/1.1':
-                # This will not be true for 414, since read_request_line
-                # usually raises 414 before reading the whole line, and we
-                # therefore cannot know the proper response_protocol.
-                buf.append(b'Connection: close\r\n')
-            else:
-                # HTTP/1.0 had no 413/414 status nor Connection header.
-                # Emit 400 instead and trust the message body is enough.
-                status = '400 Bad Request'
-
-        buf.append(CRLF)
-        if msg:
-            if isinstance(msg, six.text_type):
-                msg = msg.encode('ISO-8859-1')
-            buf.append(msg)
-
-        try:
-            self.conn.wfile.write(EMPTY.join(buf))
-        except socket.error as ex:
-            if ex.args[0] not in errors.socket_errors_to_ignore:
-                raise
-
-    def ensure_headers_sent(self):
-        """Ensure headers are sent to the client if not already sent."""
-        if not self.sent_headers:
-            self.sent_headers = True
-            self.send_headers()
-
-    def write(self, chunk):
-        """Write unbuffered data to the client."""
-        if self.chunked_write and chunk:
-            chunk_size_hex = hex(len(chunk))[2:].encode('ascii')
-            buf = [chunk_size_hex, CRLF, chunk, CRLF]
-            self.conn.wfile.write(EMPTY.join(buf))
-        else:
-            self.conn.wfile.write(chunk)
-
-    def send_headers(self):
-        """Assert, process, and send the HTTP response message-headers.
-
-        You must set self.status, and self.outheaders before calling this.
-        """
-        hkeys = [key.lower() for key, value in self.outheaders]
-        status = int(self.status[:3])
-
-        if status == 413:
-            # Request Entity Too Large. Close conn to avoid garbage.
-            self.close_connection = True
-        elif b'content-length' not in hkeys:
-            # "All 1xx (informational), 204 (no content),
-            # and 304 (not modified) responses MUST NOT
-            # include a message-body." So no point chunking.
-            if status < 200 or status in (204, 205, 304):
-                pass
-            else:
-                needs_chunked = (
-                    self.response_protocol == 'HTTP/1.1'
-                    and self.method != b'HEAD'
-                )
-                if needs_chunked:
-                    # Use the chunked transfer-coding
-                    self.chunked_write = True
-                    self.outheaders.append((b'Transfer-Encoding', b'chunked'))
-                else:
-                    # Closing the conn is the only way to determine len.
-                    self.close_connection = True
-
-        if b'connection' not in hkeys:
-            if self.response_protocol == 'HTTP/1.1':
-                # Both server and client are HTTP/1.1 or better
-                if self.close_connection:
-                    self.outheaders.append((b'Connection', b'close'))
-            else:
-                # Server and/or client are HTTP/1.0
-                if not self.close_connection:
-                    self.outheaders.append((b'Connection', b'Keep-Alive'))
-
-        if (not self.close_connection) and (not self.chunked_read):
-            # Read any remaining request body data on the socket.
-            # "If an origin server receives a request that does not include an
-            # Expect request-header field with the "100-continue" expectation,
-            # the request includes a request body, and the server responds
-            # with a final status code before reading the entire request body
-            # from the transport connection, then the server SHOULD NOT close
-            # the transport connection until it has read the entire request,
-            # or until the client closes the connection. Otherwise, the client
-            # might not reliably receive the response message. However, this
-            # requirement is not be construed as preventing a server from
-            # defending itself against denial-of-service attacks, or from
-            # badly broken client implementations."
-            remaining = getattr(self.rfile, 'remaining', 0)
-            if remaining > 0:
-                self.rfile.read(remaining)
-
-        if b'date' not in hkeys:
-            self.outheaders.append((
-                b'Date',
-                email.utils.formatdate(usegmt=True).encode('ISO-8859-1'),
-            ))
-
-        if b'server' not in hkeys:
-            self.outheaders.append((
-                b'Server',
-                self.server.server_name.encode('ISO-8859-1'),
-            ))
-
-        proto = self.server.protocol.encode('ascii')
-        buf = [proto + SPACE + self.status + CRLF]
-        for k, v in self.outheaders:
-            buf.append(k + COLON + SPACE + v + CRLF)
-        buf.append(CRLF)
-        self.conn.wfile.write(EMPTY.join(buf))
-
-
-class HTTPConnection:
-    """An HTTP connection (active socket)."""
-
-    remote_addr = None
-    remote_port = None
-    ssl_env = None
-    rbufsize = io.DEFAULT_BUFFER_SIZE
-    wbufsize = io.DEFAULT_BUFFER_SIZE
-    RequestHandlerClass = HTTPRequest
-    peercreds_enabled = False
-    peercreds_resolve_enabled = False
-
-    def __init__(self, server, sock, makefile=MakeFile):
-        """Initialize HTTPConnection instance.
-
-        Args:
-            server (HTTPServer): web server object receiving this request
-            socket (socket._socketobject): the raw socket object (usually
-                TCP) for this connection
-            makefile (file): a fileobject class for reading from the socket
-        """
-        self.server = server
-        self.socket = sock
-        self.rfile = makefile(sock, 'rb', self.rbufsize)
-        self.wfile = makefile(sock, 'wb', self.wbufsize)
-        self.requests_seen = 0
-
-        self.peercreds_enabled = self.server.peercreds_enabled
-        self.peercreds_resolve_enabled = self.server.peercreds_resolve_enabled
-
-        # LRU cached methods:
-        # Ref: https://stackoverflow.com/a/14946506/595220
-        self.resolve_peer_creds = (
-            lru_cache(maxsize=1)(self.resolve_peer_creds)
-        )
-        self.get_peer_creds = (
-            lru_cache(maxsize=1)(self.get_peer_creds)
-        )
-
-    def communicate(self):
-        """Read each request and respond appropriately."""
-        request_seen = False
-        try:
-            while True:
-                # (re)set req to None so that if something goes wrong in
-                # the RequestHandlerClass constructor, the error doesn't
-                # get written to the previous request.
-                req = None
-                req = self.RequestHandlerClass(self.server, self)
-
-                # This order of operations should guarantee correct pipelining.
-                req.parse_request()
-                if self.server.stats['Enabled']:
-                    self.requests_seen += 1
-                if not req.ready:
-                    # Something went wrong in the parsing (and the server has
-                    # probably already made a simple_response). Return and
-                    # let the conn close.
-                    return
-
-                request_seen = True
-                req.respond()
-                if req.close_connection:
-                    return
-        except socket.error as ex:
-            errnum = ex.args[0]
-            # sadly SSL sockets return a different (longer) time out string
-            timeout_errs = 'timed out', 'The read operation timed out'
-            if errnum in timeout_errs:
-                # Don't error if we're between requests; only error
-                # if 1) no request has been started at all, or 2) we're
-                # in the middle of a request.
-                # See https://github.com/cherrypy/cherrypy/issues/853
-                if (not request_seen) or (req and req.started_request):
-                    self._conditional_error(req, '408 Request Timeout')
-            elif errnum not in errors.socket_errors_to_ignore:
-                self.server.error_log('socket.error %s' % repr(errnum),
-                                      level=logging.WARNING, traceback=True)
-                self._conditional_error(req, '500 Internal Server Error')
-        except (KeyboardInterrupt, SystemExit):
-            raise
-        except errors.FatalSSLAlert:
-            pass
-        except errors.NoSSLError:
-            self._handle_no_ssl(req)
-        except Exception as ex:
-            self.server.error_log(
-                repr(ex), level=logging.ERROR, traceback=True)
-            self._conditional_error(req, '500 Internal Server Error')
-
-    linger = False
-
-    def _handle_no_ssl(self, req):
-        if not req or req.sent_headers:
-            return
-        # Unwrap wfile
-        self.wfile = MakeFile(self.socket._sock, 'wb', self.wbufsize)
-        msg = (
-            'The client sent a plain HTTP request, but '
-            'this server only speaks HTTPS on this port.'
-        )
-        req.simple_response('400 Bad Request', msg)
-        self.linger = True
-
-    def _conditional_error(self, req, response):
-        """Respond with an error.
-
-        Don't bother writing if a response
-        has already started being written.
-        """
-        if not req or req.sent_headers:
-            return
-
-        try:
-            req.simple_response(response)
-        except errors.FatalSSLAlert:
-            pass
-        except errors.NoSSLError:
-            self._handle_no_ssl(req)
-
-    def close(self):
-        """Close the socket underlying this connection."""
-        self.rfile.close()
-
-        if not self.linger:
-            self._close_kernel_socket()
-            self.socket.close()
-        else:
-            # On the other hand, sometimes we want to hang around for a bit
-            # to make sure the client has a chance to read our entire
-            # response. Skipping the close() calls here delays the FIN
-            # packet until the socket object is garbage-collected later.
-            # Someday, perhaps, we'll do the full lingering_close that
-            # Apache does, but not today.
-            pass
-
-    def get_peer_creds(self):  # LRU cached on per-instance basis, see __init__
-        """Return the PID/UID/GID tuple of the peer socket for UNIX sockets.
-
-        This function uses SO_PEERCRED to query the UNIX PID, UID, GID
-        of the peer, which is only available if the bind address is
-        a UNIX domain socket.
-
-        Raises:
-            NotImplementedError: in case of unsupported socket type
-            RuntimeError: in case of SO_PEERCRED lookup unsupported or disabled
-
-        """
-        PEERCRED_STRUCT_DEF = '3i'
-
-        if IS_WINDOWS or self.socket.family != socket.AF_UNIX:
-            raise NotImplementedError(
-                'SO_PEERCRED is only supported in Linux kernel and WSL'
-            )
-        elif not self.peercreds_enabled:
-            raise RuntimeError(
-                'Peer creds lookup is disabled within this server'
-            )
-
-        try:
-            peer_creds = self.socket.getsockopt(
-                socket.SOL_SOCKET, socket.SO_PEERCRED,
-                struct.calcsize(PEERCRED_STRUCT_DEF)
-            )
-        except socket.error as socket_err:
-            """Non-Linux kernels don't support SO_PEERCRED.
-
-            Refs:
-            http://welz.org.za/notes/on-peer-cred.html
-            https://github.com/daveti/tcpSockHack
-            msdn.microsoft.com/en-us/commandline/wsl/release_notes#build-15025
-            """
-            six.raise_from(  # 3.6+: raise RuntimeError from socket_err
-                RuntimeError,
-                socket_err,
-            )
-        else:
-            pid, uid, gid = struct.unpack(PEERCRED_STRUCT_DEF, peer_creds)
-            return pid, uid, gid
-
-    @property
-    def peer_pid(self):
-        """Return the id of the connected peer process."""
-        pid, _, _ = self.get_peer_creds()
-        return pid
-
-    @property
-    def peer_uid(self):
-        """Return the user id of the connected peer process."""
-        _, uid, _ = self.get_peer_creds()
-        return uid
-
-    @property
-    def peer_gid(self):
-        """Return the group id of the connected peer process."""
-        _, _, gid = self.get_peer_creds()
-        return gid
-
-    def resolve_peer_creds(self):  # LRU cached on per-instance basis
-        """Return the username and group tuple of the peercreds if available.
-
-        Raises:
-            NotImplementedError: in case of unsupported OS
-            RuntimeError: in case of UID/GID lookup unsupported or disabled
-
-        """
-        if (IS_WINDOWS or IS_ANDROID):
-            raise NotImplementedError(
-                'UID/GID lookup can only be done under UNIX-like OS'
-            )
-        elif not self.peercreds_resolve_enabled:
-            raise RuntimeError(
-                'UID/GID lookup is disabled within this server'
-            )
-
-        user = pwd.getpwuid(self.peer_uid).pw_name  # [0]
-        group = grp.getgrgid(self.peer_gid).gr_name  # [0]
-
-        return user, group
-
-    @property
-    def peer_user(self):
-        """Return the username of the connected peer process."""
-        user, _ = self.resolve_peer_creds()
-        return user
-
-    @property
-    def peer_group(self):
-        """Return the group of the connected peer process."""
-        _, group = self.resolve_peer_creds()
-        return group
-
-    def _close_kernel_socket(self):
-        """Close kernel socket in outdated Python versions.
-
-        On old Python versions,
-        Python's socket module does NOT call close on the kernel
-        socket when you call socket.close(). We do so manually here
-        because we want this server to send a FIN TCP segment
-        immediately. Note this must be called *before* calling
-        socket.close(), because the latter drops its reference to
-        the kernel socket.
-        """
-        if six.PY2 and hasattr(self.socket, '_sock'):
-            self.socket._sock.close()
-
-
-def prevent_socket_inheritance(sock):
-    """Stub inheritance prevention.
-
-    Dummy function, since neither fcntl nor ctypes are available.
-    """
-    pass
-
-
-class HTTPServer:
-    """An HTTP server."""
-
-    _bind_addr = '127.0.0.1'
-    _interrupt = None
-
-    gateway = None
-    """A Gateway instance."""
-
-    minthreads = None
-    """The minimum number of worker threads to create (default 10)."""
-
-    maxthreads = None
-    """The maximum number of worker threads to create.
-
-    (default -1 = no limit)"""
-
-    server_name = None
-    """The name of the server; defaults to ``self.version``."""
-
-    protocol = 'HTTP/1.1'
-    """The version string to write in the Status-Line of all HTTP responses.
-
-    For example, "HTTP/1.1" is the default. This also limits the supported
-    features used in the response."""
-
-    request_queue_size = 5
-    """The 'backlog' arg to socket.listen(); max queued connections.
-
-    (default 5)."""
-
-    shutdown_timeout = 5
-    """The total time to wait for worker threads to cleanly exit.
-
-    Specified in seconds."""
-
-    timeout = 10
-    """The timeout in seconds for accepted connections (default 10)."""
-
-    version = 'Cheroot/' + __version__
-    """A version string for the HTTPServer."""
-
-    software = None
-    """The value to set for the SERVER_SOFTWARE entry in the WSGI environ.
-
-    If None, this defaults to ``'%s Server' % self.version``.
-    """
-
-    ready = False
-    """Internal flag which indicating the socket is accepting connections."""
-
-    max_request_header_size = 0
-    """The maximum size, in bytes, for request headers, or 0 for no limit."""
-
-    max_request_body_size = 0
-    """The maximum size, in bytes, for request bodies, or 0 for no limit."""
-
-    nodelay = True
-    """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
-
-    ConnectionClass = HTTPConnection
-    """The class to use for handling HTTP connections."""
-
-    ssl_adapter = None
-    """An instance of ssl.Adapter (or a subclass).
-
-    You must have the corresponding SSL driver library installed.
-    """
-
-    peercreds_enabled = False
-    """If True, peer cred lookup can be performed via UNIX domain socket."""
-
-    peercreds_resolve_enabled = False
-    """If True, username/group will be looked up in the OS from peercreds."""
-
-    def __init__(
-        self, bind_addr, gateway,
-        minthreads=10, maxthreads=-1, server_name=None,
-        peercreds_enabled=False, peercreds_resolve_enabled=False,
-    ):
-        """Initialize HTTPServer instance.
-
-        Args:
-            bind_addr (tuple): network interface to listen to
-            gateway (Gateway): gateway for processing HTTP requests
-            minthreads (int): minimum number of threads for HTTP thread pool
-            maxthreads (int): maximum number of threads for HTTP thread pool
-            server_name (str): web server name to be advertised via Server
-                HTTP header
-        """
-        self.bind_addr = bind_addr
-        self.gateway = gateway
-
-        self.requests = threadpool.ThreadPool(
-            self, min=minthreads or 1, max=maxthreads)
-
-        if not server_name:
-            server_name = self.version
-        self.server_name = server_name
-        self.peercreds_enabled = peercreds_enabled
-        self.peercreds_resolve_enabled = (
-            peercreds_resolve_enabled and peercreds_enabled
-        )
-        self.clear_stats()
-
-    def clear_stats(self):
-        """Reset server stat counters.."""
-        self._start_time = None
-        self._run_time = 0
-        self.stats = {
-            'Enabled': False,
-            'Bind Address': lambda s: repr(self.bind_addr),
-            'Run time': lambda s: (not s['Enabled']) and -1 or self.runtime(),
-            'Accepts': 0,
-            'Accepts/sec': lambda s: s['Accepts'] / self.runtime(),
-            'Queue': lambda s: getattr(self.requests, 'qsize', None),
-            'Threads': lambda s: len(getattr(self.requests, '_threads', [])),
-            'Threads Idle': lambda s: getattr(self.requests, 'idle', None),
-            'Socket Errors': 0,
-            'Requests': lambda s: (not s['Enabled']) and -1 or sum(
-                [w['Requests'](w) for w in s['Worker Threads'].values()], 0),
-            'Bytes Read': lambda s: (not s['Enabled']) and -1 or sum(
-                [w['Bytes Read'](w) for w in s['Worker Threads'].values()], 0),
-            'Bytes Written': lambda s: (not s['Enabled']) and -1 or sum(
-                [w['Bytes Written'](w) for w in s['Worker Threads'].values()],
-                0),
-            'Work Time': lambda s: (not s['Enabled']) and -1 or sum(
-                [w['Work Time'](w) for w in s['Worker Threads'].values()], 0),
-            'Read Throughput': lambda s: (not s['Enabled']) and -1 or sum(
-                [w['Bytes Read'](w) / (w['Work Time'](w) or 1e-6)
-                 for w in s['Worker Threads'].values()], 0),
-            'Write Throughput': lambda s: (not s['Enabled']) and -1 or sum(
-                [w['Bytes Written'](w) / (w['Work Time'](w) or 1e-6)
-                 for w in s['Worker Threads'].values()], 0),
-            'Worker Threads': {},
-        }
-        logging.statistics['Cheroot HTTPServer %d' % id(self)] = self.stats
-
-    def runtime(self):
-        """Return server uptime."""
-        if self._start_time is None:
-            return self._run_time
-        else:
-            return self._run_time + (time.time() - self._start_time)
-
-    def __str__(self):
-        """Render Server instance representing bind address."""
-        return '%s.%s(%r)' % (self.__module__, self.__class__.__name__,
-                              self.bind_addr)
-
-    @property
-    def bind_addr(self):
-        """Return the interface on which to listen for connections.
-
-        For TCP sockets, a (host, port) tuple. Host values may be any IPv4
-        or IPv6 address, or any valid hostname. The string 'localhost' is a
-        synonym for '127.0.0.1' (or '::1', if your hosts file prefers IPv6).
-        The string '0.0.0.0' is a special IPv4 entry meaning "any active
-        interface" (INADDR_ANY), and '::' is the similar IN6ADDR_ANY for
-        IPv6. The empty string or None are not allowed.
-
-        For UNIX sockets, supply the filename as a string.
-
-        Systemd socket activation is automatic and doesn't require tempering
-        with this variable.
-        """
-        return self._bind_addr
-
-    @bind_addr.setter
-    def bind_addr(self, value):
-        """Set the interface on which to listen for connections."""
-        if isinstance(value, tuple) and value[0] in ('', None):
-            # Despite the socket module docs, using '' does not
-            # allow AI_PASSIVE to work. Passing None instead
-            # returns '0.0.0.0' like we want. In other words:
-            #     host    AI_PASSIVE     result
-            #      ''         Y         192.168.x.y
-            #      ''         N         192.168.x.y
-            #     None        Y         0.0.0.0
-            #     None        N         127.0.0.1
-            # But since you can get the same effect with an explicit
-            # '0.0.0.0', we deny both the empty string and None as values.
-            raise ValueError("Host values of '' or None are not allowed. "
-                             "Use '0.0.0.0' (IPv4) or '::' (IPv6) instead "
-                             'to listen on all active interfaces.')
-        self._bind_addr = value
-
-    def safe_start(self):
-        """Run the server forever, and stop it cleanly on exit."""
-        try:
-            self.start()
-        except (KeyboardInterrupt, IOError):
-            # The time.sleep call might raise
-            # "IOError: [Errno 4] Interrupted function call" on KBInt.
-            self.error_log('Keyboard Interrupt: shutting down')
-            self.stop()
-            raise
-        except SystemExit:
-            self.error_log('SystemExit raised: shutting down')
-            self.stop()
-            raise
-
-    def prepare(self):
-        """Prepare server to serving requests.
-
-        It binds a socket's port, setups the socket to ``listen()`` and does
-        other preparing things.
-        """
-        self._interrupt = None
-
-        if self.software is None:
-            self.software = '%s Server' % self.version
-
-        # Select the appropriate socket
-        self.socket = None
-        if os.getenv('LISTEN_PID', None):
-            # systemd socket activation
-            self.socket = socket.fromfd(3, socket.AF_INET, socket.SOCK_STREAM)
-        elif isinstance(self.bind_addr, six.string_types):
-            # AF_UNIX socket
-
-            # So we can reuse the socket...
-            try:
-                os.unlink(self.bind_addr)
-            except Exception:
-                pass
-
-            # So everyone can access the socket...
-            try:
-                os.chmod(self.bind_addr, 0o777)
-            except Exception:
-                pass
-
-            info = [
-                (socket.AF_UNIX, socket.SOCK_STREAM, 0, '', self.bind_addr)]
-        else:
-            # AF_INET or AF_INET6 socket
-            # Get the correct address family for our host (allows IPv6
-            # addresses)
-            host, port = self.bind_addr
-            try:
-                info = socket.getaddrinfo(
-                    host, port, socket.AF_UNSPEC,
-                    socket.SOCK_STREAM, 0, socket.AI_PASSIVE)
-            except socket.gaierror:
-                sock_type = socket.AF_INET
-                bind_addr = self.bind_addr
-
-                if ':' in host:
-                    sock_type = socket.AF_INET6
-                    bind_addr = bind_addr + (0, 0)
-
-                info = [(sock_type, socket.SOCK_STREAM, 0, '', bind_addr)]
-
-        if not self.socket:
-            msg = 'No socket could be created'
-            for res in info:
-                af, socktype, proto, canonname, sa = res
-                try:
-                    self.bind(af, socktype, proto)
-                    break
-                except socket.error as serr:
-                    msg = '%s -- (%s: %s)' % (msg, sa, serr)
-                    if self.socket:
-                        self.socket.close()
-                    self.socket = None
-
-            if not self.socket:
-                raise socket.error(msg)
-
-        # Timeout so KeyboardInterrupt can be caught on Win32
-        self.socket.settimeout(1)
-        self.socket.listen(self.request_queue_size)
-
-        # Create worker threads
-        self.requests.start()
-
-        self.ready = True
-        self._start_time = time.time()
-
-    def serve(self):
-        """Serve requests, after invoking :func:`prepare()`."""
-        while self.ready:
-            try:
-                self.tick()
-            except (KeyboardInterrupt, SystemExit):
-                raise
-            except Exception:
-                self.error_log('Error in HTTPServer.tick', level=logging.ERROR,
-                               traceback=True)
-
-            if self.interrupt:
-                while self.interrupt is True:
-                    # Wait for self.stop() to complete. See _set_interrupt.
-                    time.sleep(0.1)
-                if self.interrupt:
-                    raise self.interrupt
-
-    def start(self):
-        """Run the server forever.
-
-        It is shortcut for invoking :func:`prepare()` then :func:`serve()`.
-        """
-        # We don't have to trap KeyboardInterrupt or SystemExit here,
-        # because cherrypy.server already does so, calling self.stop() for us.
-        # If you're using this server with another framework, you should
-        # trap those exceptions in whatever code block calls start().
-        self.prepare()
-        self.serve()
-
-    def error_log(self, msg='', level=20, traceback=False):
-        """Write error message to log.
-
-        Args:
-            msg (str): error message
-            level (int): logging level
-            traceback (bool): add traceback to output or not
-        """
-        # Override this in subclasses as desired
-        sys.stderr.write(msg + '\n')
-        sys.stderr.flush()
-        if traceback:
-            tblines = traceback_.format_exc()
-            sys.stderr.write(tblines)
-            sys.stderr.flush()
-
-    def bind(self, family, type, proto=0):
-        """Create (or recreate) the actual socket object."""
-        self.socket = socket.socket(family, type, proto)
-        prevent_socket_inheritance(self.socket)
-        if not IS_WINDOWS:
-            # Windows has different semantics for SO_REUSEADDR,
-            # so don't set it.
-            # https://msdn.microsoft.com/en-us/library/ms740621(v=vs.85).aspx
-            self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
-        if self.nodelay and not isinstance(self.bind_addr, str):
-            self.socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)
-
-        if self.ssl_adapter is not None:
-            self.socket = self.ssl_adapter.bind(self.socket)
-
-        host, port = self.bind_addr[:2]
-
-        # If listening on the IPV6 any address ('::' = IN6ADDR_ANY),
-        # activate dual-stack. See
-        # https://github.com/cherrypy/cherrypy/issues/871.
-        listening_ipv6 = (
-            hasattr(socket, 'AF_INET6')
-            and family == socket.AF_INET6
-            and host in ('::', '::0', '::0.0.0.0')
-        )
-        if listening_ipv6:
-            try:
-                self.socket.setsockopt(
-                    socket.IPPROTO_IPV6, socket.IPV6_V6ONLY, 0)
-            except (AttributeError, socket.error):
-                # Apparently, the socket option is not available in
-                # this machine's TCP stack
-                pass
-
-        self.socket.bind(self.bind_addr)
-        # TODO: keep requested bind_addr separate real bound_addr (port is
-        # different in case of ephemeral port 0)
-        self.bind_addr = self.socket.getsockname()
-        if family in (
-            # Windows doesn't have socket.AF_UNIX, so not using it in check
-            socket.AF_INET,
-            socket.AF_INET6,
-        ):
-            """UNIX domain sockets are strings or bytes.
-
-            In case of bytes with a leading null-byte it's an abstract socket.
-            """
-            self.bind_addr = self.bind_addr[:2]
-
-    def tick(self):
-        """Accept a new connection and put it on the Queue."""
-        try:
-            s, addr = self.socket.accept()
-            if self.stats['Enabled']:
-                self.stats['Accepts'] += 1
-            if not self.ready:
-                return
-
-            prevent_socket_inheritance(s)
-            if hasattr(s, 'settimeout'):
-                s.settimeout(self.timeout)
-
-            mf = MakeFile
-            ssl_env = {}
-            # if ssl cert and key are set, we try to be a secure HTTP server
-            if self.ssl_adapter is not None:
-                try:
-                    s, ssl_env = self.ssl_adapter.wrap(s)
-                except errors.NoSSLError:
-                    msg = ('The client sent a plain HTTP request, but '
-                           'this server only speaks HTTPS on this port.')
-                    buf = ['%s 400 Bad Request\r\n' % self.protocol,
-                           'Content-Length: %s\r\n' % len(msg),
-                           'Content-Type: text/plain\r\n\r\n',
-                           msg]
-
-                    sock_to_make = s if six.PY3 else s._sock
-                    wfile = mf(sock_to_make, 'wb', io.DEFAULT_BUFFER_SIZE)
-                    try:
-                        wfile.write(''.join(buf).encode('ISO-8859-1'))
-                    except socket.error as ex:
-                        if ex.args[0] not in errors.socket_errors_to_ignore:
-                            raise
-                    return
-                if not s:
-                    return
-                mf = self.ssl_adapter.makefile
-                # Re-apply our timeout since we may have a new socket object
-                if hasattr(s, 'settimeout'):
-                    s.settimeout(self.timeout)
-
-            conn = self.ConnectionClass(self, s, mf)
-
-            if not isinstance(self.bind_addr, six.string_types):
-                # optional values
-                # Until we do DNS lookups, omit REMOTE_HOST
-                if addr is None:  # sometimes this can happen
-                    # figure out if AF_INET or AF_INET6.
-                    if len(s.getsockname()) == 2:
-                        # AF_INET
-                        addr = ('0.0.0.0', 0)
-                    else:
-                        # AF_INET6
-                        addr = ('::', 0)
-                conn.remote_addr = addr[0]
-                conn.remote_port = addr[1]
-
-            conn.ssl_env = ssl_env
-
-            try:
-                self.requests.put(conn)
-            except queue.Full:
-                # Just drop the conn. TODO: write 503 back?
-                conn.close()
-                return
-        except socket.timeout:
-            # The only reason for the timeout in start() is so we can
-            # notice keyboard interrupts on Win32, which don't interrupt
-            # accept() by default
-            return
-        except socket.error as ex:
-            if self.stats['Enabled']:
-                self.stats['Socket Errors'] += 1
-            if ex.args[0] in errors.socket_error_eintr:
-                # I *think* this is right. EINTR should occur when a signal
-                # is received during the accept() call; all docs say retry
-                # the call, and I *think* I'm reading it right that Python
-                # will then go ahead and poll for and handle the signal
-                # elsewhere. See
-                # https://github.com/cherrypy/cherrypy/issues/707.
-                return
-            if ex.args[0] in errors.socket_errors_nonblocking:
-                # Just try again. See
-                # https://github.com/cherrypy/cherrypy/issues/479.
-                return
-            if ex.args[0] in errors.socket_errors_to_ignore:
-                # Our socket was closed.
-                # See https://github.com/cherrypy/cherrypy/issues/686.
-                return
-            raise
-
-    @property
-    def interrupt(self):
-        """Flag interrupt of the server."""
-        return self._interrupt
-
-    @interrupt.setter
-    def interrupt(self, interrupt):
-        """Perform the shutdown of this server and save the exception."""
-        self._interrupt = True
-        self.stop()
-        self._interrupt = interrupt
-
-    def stop(self):
-        """Gracefully shutdown a server that is serving forever."""
-        self.ready = False
-        if self._start_time is not None:
-            self._run_time += (time.time() - self._start_time)
-        self._start_time = None
-
-        sock = getattr(self, 'socket', None)
-        if sock:
-            if not isinstance(self.bind_addr, six.string_types):
-                # Touch our own socket to make accept() return immediately.
-                try:
-                    host, port = sock.getsockname()[:2]
-                except socket.error as ex:
-                    if ex.args[0] not in errors.socket_errors_to_ignore:
-                        # Changed to use error code and not message
-                        # See
-                        # https://github.com/cherrypy/cherrypy/issues/860.
-                        raise
-                else:
-                    # Note that we're explicitly NOT using AI_PASSIVE,
-                    # here, because we want an actual IP to touch.
-                    # localhost won't work if we've bound to a public IP,
-                    # but it will if we bound to '0.0.0.0' (INADDR_ANY).
-                    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
-                                                  socket.SOCK_STREAM):
-                        af, socktype, proto, canonname, sa = res
-                        s = None
-                        try:
-                            s = socket.socket(af, socktype, proto)
-                            # See
-                            # https://groups.google.com/group/cherrypy-users/
-                            #     browse_frm/thread/bbfe5eb39c904fe0
-                            s.settimeout(1.0)
-                            s.connect((host, port))
-                            s.close()
-                        except socket.error:
-                            if s:
-                                s.close()
-            if hasattr(sock, 'close'):
-                sock.close()
-            self.socket = None
-
-        self.requests.stop(self.shutdown_timeout)
-
-
-class Gateway:
-    """Base class to interface HTTPServer with other systems, such as WSGI."""
-
-    def __init__(self, req):
-        """Initialize Gateway instance with request.
-
-        Args:
-            req (HTTPRequest): current HTTP request
-        """
-        self.req = req
-
-    def respond(self):
-        """Process the current request. Must be overridden in a subclass."""
-        raise NotImplementedError
-
-
-# These may either be ssl.Adapter subclasses or the string names
-# of such classes (in which case they will be lazily loaded).
-ssl_adapters = {
-    'builtin': 'cheroot.ssl.builtin.BuiltinSSLAdapter',
-    'pyopenssl': 'cheroot.ssl.pyopenssl.pyOpenSSLAdapter',
-}
-
-
-def get_ssl_adapter_class(name='builtin'):
-    """Return an SSL adapter class for the given name."""
-    adapter = ssl_adapters[name.lower()]
-    if isinstance(adapter, six.string_types):
-        last_dot = adapter.rfind('.')
-        attr_name = adapter[last_dot + 1:]
-        mod_path = adapter[:last_dot]
-
-        try:
-            mod = sys.modules[mod_path]
-            if mod is None:
-                raise KeyError()
-        except KeyError:
-            # The last [''] is important.
-            mod = __import__(mod_path, globals(), locals(), [''])
-
-        # Let an AttributeError propagate outward.
-        try:
-            adapter = getattr(mod, attr_name)
-        except AttributeError:
-            raise AttributeError("'%s' object has no attribute '%s'"
-                                 % (mod_path, attr_name))
-
-    return adapter
diff --git a/libraries/cheroot/ssl/__init__.py b/libraries/cheroot/ssl/__init__.py
deleted file mode 100644
index ec1a0d90..00000000
--- a/libraries/cheroot/ssl/__init__.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""Implementation of the SSL adapter base interface."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-from abc import ABCMeta, abstractmethod
-
-from six import add_metaclass
-
-
-@add_metaclass(ABCMeta)
-class Adapter:
-    """Base class for SSL driver library adapters.
-
-    Required methods:
-
-        * ``wrap(sock) -> (wrapped socket, ssl environ dict)``
-        * ``makefile(sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE) ->
-          socket file object``
-    """
-
-    @abstractmethod
-    def __init__(
-            self, certificate, private_key, certificate_chain=None,
-            ciphers=None):
-        """Set up certificates, private key ciphers and reset context."""
-        self.certificate = certificate
-        self.private_key = private_key
-        self.certificate_chain = certificate_chain
-        self.ciphers = ciphers
-        self.context = None
-
-    @abstractmethod
-    def bind(self, sock):
-        """Wrap and return the given socket."""
-        return sock
-
-    @abstractmethod
-    def wrap(self, sock):
-        """Wrap and return the given socket, plus WSGI environ entries."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def get_environ(self):
-        """Return WSGI environ entries to be merged into each request."""
-        raise NotImplementedError
-
-    @abstractmethod
-    def makefile(self, sock, mode='r', bufsize=-1):
-        """Return socket file object."""
-        raise NotImplementedError
diff --git a/libraries/cheroot/ssl/builtin.py b/libraries/cheroot/ssl/builtin.py
deleted file mode 100644
index a19f7eef..00000000
--- a/libraries/cheroot/ssl/builtin.py
+++ /dev/null
@@ -1,162 +0,0 @@
-"""
-A library for integrating Python's builtin ``ssl`` library with Cheroot.
-
-The ssl module must be importable for SSL functionality.
-
-To use this module, set ``HTTPServer.ssl_adapter`` to an instance of
-``BuiltinSSLAdapter``.
-"""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-try:
-    import ssl
-except ImportError:
-    ssl = None
-
-try:
-    from _pyio import DEFAULT_BUFFER_SIZE
-except ImportError:
-    try:
-        from io import DEFAULT_BUFFER_SIZE
-    except ImportError:
-        DEFAULT_BUFFER_SIZE = -1
-
-import six
-
-from . import Adapter
-from .. import errors
-from ..makefile import MakeFile
-
-
-if six.PY3:
-    generic_socket_error = OSError
-else:
-    import socket
-    generic_socket_error = socket.error
-    del socket
-
-
-def _assert_ssl_exc_contains(exc, *msgs):
-    """Check whether SSL exception contains either of messages provided."""
-    if len(msgs) < 1:
-        raise TypeError(
-            '_assert_ssl_exc_contains() requires '
-            'at least one message to be passed.'
-        )
-    err_msg_lower = exc.args[1].lower()
-    return any(m.lower() in err_msg_lower for m in msgs)
-
-
-class BuiltinSSLAdapter(Adapter):
-    """A wrapper for integrating Python's builtin ssl module with Cheroot."""
-
-    certificate = None
-    """The filename of the server SSL certificate."""
-
-    private_key = None
-    """The filename of the server's private key file."""
-
-    certificate_chain = None
-    """The filename of the certificate chain file."""
-
-    context = None
-    """The ssl.SSLContext that will be used to wrap sockets."""
-
-    ciphers = None
-    """The ciphers list of SSL."""
-
-    def __init__(
-            self, certificate, private_key, certificate_chain=None,
-            ciphers=None):
-        """Set up context in addition to base class properties if available."""
-        if ssl is None:
-            raise ImportError('You must install the ssl module to use HTTPS.')
-
-        super(BuiltinSSLAdapter, self).__init__(
-            certificate, private_key, certificate_chain, ciphers)
-
-        self.context = ssl.create_default_context(
-            purpose=ssl.Purpose.CLIENT_AUTH,
-            cafile=certificate_chain
-        )
-        self.context.load_cert_chain(certificate, private_key)
-        if self.ciphers is not None:
-            self.context.set_ciphers(ciphers)
-
-    def bind(self, sock):
-        """Wrap and return the given socket."""
-        return super(BuiltinSSLAdapter, self).bind(sock)
-
-    def wrap(self, sock):
-        """Wrap and return the given socket, plus WSGI environ entries."""
-        EMPTY_RESULT = None, {}
-        try:
-            s = self.context.wrap_socket(
-                sock, do_handshake_on_connect=True, server_side=True,
-            )
-        except ssl.SSLError as ex:
-            if ex.errno == ssl.SSL_ERROR_EOF:
-                # This is almost certainly due to the cherrypy engine
-                # 'pinging' the socket to assert it's connectable;
-                # the 'ping' isn't SSL.
-                return EMPTY_RESULT
-            elif ex.errno == ssl.SSL_ERROR_SSL:
-                if _assert_ssl_exc_contains(ex, 'http request'):
-                    # The client is speaking HTTP to an HTTPS server.
-                    raise errors.NoSSLError
-
-                # Check if it's one of the known errors
-                # Errors that are caught by PyOpenSSL, but thrown by
-                # built-in ssl
-                _block_errors = (
-                    'unknown protocol', 'unknown ca', 'unknown_ca',
-                    'unknown error',
-                    'https proxy request', 'inappropriate fallback',
-                    'wrong version number',
-                    'no shared cipher', 'certificate unknown',
-                    'ccs received early',
-                )
-                if _assert_ssl_exc_contains(ex, *_block_errors):
-                    # Accepted error, let's pass
-                    return EMPTY_RESULT
-            elif _assert_ssl_exc_contains(ex, 'handshake operation timed out'):
-                # This error is thrown by builtin SSL after a timeout
-                # when client is speaking HTTP to an HTTPS server.
-                # The connection can safely be dropped.
-                return EMPTY_RESULT
-            raise
-        except generic_socket_error as exc:
-            """It is unclear why exactly this happens.
-
-            It's reproducible only under Python 2 with openssl>1.0 and stdlib
-            ``ssl`` wrapper, and only with CherryPy.
-            So it looks like some healthcheck tries to connect to this socket
-            during startup (from the same process).
-
-
-            Ref: https://github.com/cherrypy/cherrypy/issues/1618
-            """
-            if six.PY2 and exc.args == (0, 'Error'):
-                return EMPTY_RESULT
-            raise
-        return s, self.get_environ(s)
-
-    # TODO: fill this out more with mod ssl env
-    def get_environ(self, sock):
-        """Create WSGI environ entries to be merged into each request."""
-        cipher = sock.cipher()
-        ssl_environ = {
-            'wsgi.url_scheme': 'https',
-            'HTTPS': 'on',
-            'SSL_PROTOCOL': cipher[1],
-            'SSL_CIPHER': cipher[0]
-            # SSL_VERSION_INTERFACE     string  The mod_ssl program version
-            # SSL_VERSION_LIBRARY   string  The OpenSSL program version
-        }
-        return ssl_environ
-
-    def makefile(self, sock, mode='r', bufsize=DEFAULT_BUFFER_SIZE):
-        """Return socket file object."""
-        return MakeFile(sock, mode, bufsize)
diff --git a/libraries/cheroot/ssl/pyopenssl.py b/libraries/cheroot/ssl/pyopenssl.py
deleted file mode 100644
index 2185f851..00000000
--- a/libraries/cheroot/ssl/pyopenssl.py
+++ /dev/null
@@ -1,267 +0,0 @@
-"""
-A library for integrating pyOpenSSL with Cheroot.
-
-The OpenSSL module must be importable for SSL functionality.
-You can obtain it from `here <https://launchpad.net/pyopenssl>`_.
-
-To use this module, set HTTPServer.ssl_adapter to an instance of
-ssl.Adapter. There are two ways to use SSL:
-
-Method One
-----------
-
- * ``ssl_adapter.context``: an instance of SSL.Context.
-
-If this is not None, it is assumed to be an SSL.Context instance,
-and will be passed to SSL.Connection on bind(). The developer is
-responsible for forming a valid Context object. This approach is
-to be preferred for more flexibility, e.g. if the cert and key are
-streams instead of files, or need decryption, or SSL.SSLv3_METHOD
-is desired instead of the default SSL.SSLv23_METHOD, etc. Consult
-the pyOpenSSL documentation for complete options.
-
-Method Two (shortcut)
----------------------
-
- * ``ssl_adapter.certificate``: the filename of the server SSL certificate.
- * ``ssl_adapter.private_key``: the filename of the server's private key file.
-
-Both are None by default. If ssl_adapter.context is None, but .private_key
-and .certificate are both given and valid, they will be read, and the
-context will be automatically created from them.
-"""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import socket
-import threading
-import time
-
-try:
-    from OpenSSL import SSL
-    from OpenSSL import crypto
-except ImportError:
-    SSL = None
-
-from . import Adapter
-from .. import errors, server as cheroot_server
-from ..makefile import MakeFile
-
-
-class SSL_fileobject(MakeFile):
-    """SSL file object attached to a socket object."""
-
-    ssl_timeout = 3
-    ssl_retry = .01
-
-    def _safe_call(self, is_reader, call, *args, **kwargs):
-        """Wrap the given call with SSL error-trapping.
-
-        is_reader: if False EOF errors will be raised. If True, EOF errors
-        will return "" (to emulate normal sockets).
-        """
-        start = time.time()
-        while True:
-            try:
-                return call(*args, **kwargs)
-            except SSL.WantReadError:
-                # Sleep and try again. This is dangerous, because it means
-                # the rest of the stack has no way of differentiating
-                # between a "new handshake" error and "client dropped".
-                # Note this isn't an endless loop: there's a timeout below.
-                time.sleep(self.ssl_retry)
-            except SSL.WantWriteError:
-                time.sleep(self.ssl_retry)
-            except SSL.SysCallError as e:
-                if is_reader and e.args == (-1, 'Unexpected EOF'):
-                    return ''
-
-                errnum = e.args[0]
-                if is_reader and errnum in errors.socket_errors_to_ignore:
-                    return ''
-                raise socket.error(errnum)
-            except SSL.Error as e:
-                if is_reader and e.args == (-1, 'Unexpected EOF'):
-                    return ''
-
-                thirdarg = None
-                try:
-                    thirdarg = e.args[0][0][2]
-                except IndexError:
-                    pass
-
-                if thirdarg == 'http request':
-                    # The client is talking HTTP to an HTTPS server.
-                    raise errors.NoSSLError()
-
-                raise errors.FatalSSLAlert(*e.args)
-
-            if time.time() - start > self.ssl_timeout:
-                raise socket.timeout('timed out')
-
-    def recv(self, size):
-        """Receive message of a size from the socket."""
-        return self._safe_call(True, super(SSL_fileobject, self).recv, size)
-
-    def sendall(self, *args, **kwargs):
-        """Send whole message to the socket."""
-        return self._safe_call(False, super(SSL_fileobject, self).sendall,
-                               *args, **kwargs)
-
-    def send(self, *args, **kwargs):
-        """Send some part of message to the socket."""
-        return self._safe_call(False, super(SSL_fileobject, self).send,
-                               *args, **kwargs)
-
-
-class SSLConnection:
-    """A thread-safe wrapper for an SSL.Connection.
-
-    ``*args``: the arguments to create the wrapped ``SSL.Connection(*args)``.
-    """
-
-    def __init__(self, *args):
-        """Initialize SSLConnection instance."""
-        self._ssl_conn = SSL.Connection(*args)
-        self._lock = threading.RLock()
-
-    for f in ('get_context', 'pending', 'send', 'write', 'recv', 'read',
-              'renegotiate', 'bind', 'listen', 'connect', 'accept',
-              'setblocking', 'fileno', 'close', 'get_cipher_list',
-              'getpeername', 'getsockname', 'getsockopt', 'setsockopt',
-              'makefile', 'get_app_data', 'set_app_data', 'state_string',
-              'sock_shutdown', 'get_peer_certificate', 'want_read',
-              'want_write', 'set_connect_state', 'set_accept_state',
-              'connect_ex', 'sendall', 'settimeout', 'gettimeout'):
-        exec("""def %s(self, *args):
-        self._lock.acquire()
-        try:
-            return self._ssl_conn.%s(*args)
-        finally:
-            self._lock.release()
-""" % (f, f))
-
-    def shutdown(self, *args):
-        """Shutdown the SSL connection.
-
-        Ignore all incoming args since pyOpenSSL.socket.shutdown takes no args.
-        """
-        self._lock.acquire()
-        try:
-            return self._ssl_conn.shutdown()
-        finally:
-            self._lock.release()
-
-
-class pyOpenSSLAdapter(Adapter):
-    """A wrapper for integrating pyOpenSSL with Cheroot."""
-
-    certificate = None
-    """The filename of the server SSL certificate."""
-
-    private_key = None
-    """The filename of the server's private key file."""
-
-    certificate_chain = None
-    """Optional. The filename of CA's intermediate certificate bundle.
-
-    This is needed for cheaper "chained root" SSL certificates, and should be
-    left as None if not required."""
-
-    context = None
-    """An instance of SSL.Context."""
-
-    ciphers = None
-    """The ciphers list of SSL."""
-
-    def __init__(
-            self, certificate, private_key, certificate_chain=None,
-            ciphers=None):
-        """Initialize OpenSSL Adapter instance."""
-        if SSL is None:
-            raise ImportError('You must install pyOpenSSL to use HTTPS.')
-
-        super(pyOpenSSLAdapter, self).__init__(
-            certificate, private_key, certificate_chain, ciphers)
-
-        self._environ = None
-
-    def bind(self, sock):
-        """Wrap and return the given socket."""
-        if self.context is None:
-            self.context = self.get_context()
-        conn = SSLConnection(self.context, sock)
-        self._environ = self.get_environ()
-        return conn
-
-    def wrap(self, sock):
-        """Wrap and return the given socket, plus WSGI environ entries."""
-        return sock, self._environ.copy()
-
-    def get_context(self):
-        """Return an SSL.Context from self attributes."""
-        # See https://code.activestate.com/recipes/442473/
-        c = SSL.Context(SSL.SSLv23_METHOD)
-        c.use_privatekey_file(self.private_key)
-        if self.certificate_chain:
-            c.load_verify_locations(self.certificate_chain)
-        c.use_certificate_file(self.certificate)
-        return c
-
-    def get_environ(self):
-        """Return WSGI environ entries to be merged into each request."""
-        ssl_environ = {
-            'HTTPS': 'on',
-            # pyOpenSSL doesn't provide access to any of these AFAICT
-            # 'SSL_PROTOCOL': 'SSLv2',
-            # SSL_CIPHER    string  The cipher specification name
-            # SSL_VERSION_INTERFACE     string  The mod_ssl program version
-            # SSL_VERSION_LIBRARY   string  The OpenSSL program version
-        }
-
-        if self.certificate:
-            # Server certificate attributes
-            cert = open(self.certificate, 'rb').read()
-            cert = crypto.load_certificate(crypto.FILETYPE_PEM, cert)
-            ssl_environ.update({
-                'SSL_SERVER_M_VERSION': cert.get_version(),
-                'SSL_SERVER_M_SERIAL': cert.get_serial_number(),
-                # 'SSL_SERVER_V_START':
-                #   Validity of server's certificate (start time),
-                # 'SSL_SERVER_V_END':
-                #   Validity of server's certificate (end time),
-            })
-
-            for prefix, dn in [('I', cert.get_issuer()),
-                               ('S', cert.get_subject())]:
-                # X509Name objects don't seem to have a way to get the
-                # complete DN string. Use str() and slice it instead,
-                # because str(dn) == "<X509Name object '/C=US/ST=...'>"
-                dnstr = str(dn)[18:-2]
-
-                wsgikey = 'SSL_SERVER_%s_DN' % prefix
-                ssl_environ[wsgikey] = dnstr
-
-                # The DN should be of the form: /k1=v1/k2=v2, but we must allow
-                # for any value to contain slashes itself (in a URL).
-                while dnstr:
-                    pos = dnstr.rfind('=')
-                    dnstr, value = dnstr[:pos], dnstr[pos + 1:]
-                    pos = dnstr.rfind('/')
-                    dnstr, key = dnstr[:pos], dnstr[pos + 1:]
-                    if key and value:
-                        wsgikey = 'SSL_SERVER_%s_DN_%s' % (prefix, key)
-                        ssl_environ[wsgikey] = value
-
-        return ssl_environ
-
-    def makefile(self, sock, mode='r', bufsize=-1):
-        """Return socket file object."""
-        if SSL and isinstance(sock, SSL.ConnectionType):
-            timeout = sock.gettimeout()
-            f = SSL_fileobject(sock, mode, bufsize)
-            f.ssl_timeout = timeout
-            return f
-        else:
-            return cheroot_server.CP_fileobject(sock, mode, bufsize)
diff --git a/libraries/cheroot/test/__init__.py b/libraries/cheroot/test/__init__.py
deleted file mode 100644
index e2a7b348..00000000
--- a/libraries/cheroot/test/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""Cheroot test suite."""
diff --git a/libraries/cheroot/test/conftest.py b/libraries/cheroot/test/conftest.py
deleted file mode 100644
index 9f5f9284..00000000
--- a/libraries/cheroot/test/conftest.py
+++ /dev/null
@@ -1,27 +0,0 @@
-"""Pytest configuration module.
-
-Contains fixtures, which are tightly bound to the Cheroot framework
-itself, useless for end-users' app testing.
-"""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import pytest
-
-from ..testing import (  # noqa: F401
-    native_server, wsgi_server,
-)
-from ..testing import get_server_client
-
-
-@pytest.fixture  # noqa: F811
-def wsgi_server_client(wsgi_server):
-    """Create a test client out of given WSGI server."""
-    return get_server_client(wsgi_server)
-
-
-@pytest.fixture  # noqa: F811
-def native_server_client(native_server):
-    """Create a test client out of given HTTP server."""
-    return get_server_client(native_server)
diff --git a/libraries/cheroot/test/helper.py b/libraries/cheroot/test/helper.py
deleted file mode 100644
index 38f40b26..00000000
--- a/libraries/cheroot/test/helper.py
+++ /dev/null
@@ -1,169 +0,0 @@
-"""A library of helper functions for the Cheroot test suite."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import datetime
-import logging
-import os
-import sys
-import time
-import threading
-import types
-
-from six.moves import http_client
-
-import six
-
-import cheroot.server
-import cheroot.wsgi
-
-from cheroot.test import webtest
-
-log = logging.getLogger(__name__)
-thisdir = os.path.abspath(os.path.dirname(__file__))
-serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
-
-
-config = {
-    'bind_addr': ('127.0.0.1', 54583),
-    'server': 'wsgi',
-    'wsgi_app': None,
-}
-
-
-class CherootWebCase(webtest.WebCase):
-    """Helper class for a web app test suite."""
-
-    script_name = ''
-    scheme = 'http'
-
-    available_servers = {
-        'wsgi': cheroot.wsgi.Server,
-        'native': cheroot.server.HTTPServer,
-    }
-
-    @classmethod
-    def setup_class(cls):
-        """Create and run one HTTP server per class."""
-        conf = config.copy()
-        conf.update(getattr(cls, 'config', {}))
-
-        s_class = conf.pop('server', 'wsgi')
-        server_factory = cls.available_servers.get(s_class)
-        if server_factory is None:
-            raise RuntimeError('Unknown server in config: %s' % conf['server'])
-        cls.httpserver = server_factory(**conf)
-
-        cls.HOST, cls.PORT = cls.httpserver.bind_addr
-        if cls.httpserver.ssl_adapter is None:
-            ssl = ''
-            cls.scheme = 'http'
-        else:
-            ssl = ' (ssl)'
-            cls.HTTP_CONN = http_client.HTTPSConnection
-            cls.scheme = 'https'
-
-        v = sys.version.split()[0]
-        log.info('Python version used to run this test script: %s' % v)
-        log.info('Cheroot version: %s' % cheroot.__version__)
-        log.info('HTTP server version: %s%s' % (cls.httpserver.protocol, ssl))
-        log.info('PID: %s' % os.getpid())
-
-        if hasattr(cls, 'setup_server'):
-            # Clear the wsgi server so that
-            # it can be updated with the new root
-            cls.setup_server()
-            cls.start()
-
-    @classmethod
-    def teardown_class(cls):
-        """Cleanup HTTP server."""
-        if hasattr(cls, 'setup_server'):
-            cls.stop()
-
-    @classmethod
-    def start(cls):
-        """Load and start the HTTP server."""
-        threading.Thread(target=cls.httpserver.safe_start).start()
-        while not cls.httpserver.ready:
-            time.sleep(0.1)
-
-    @classmethod
-    def stop(cls):
-        """Terminate HTTP server."""
-        cls.httpserver.stop()
-        td = getattr(cls, 'teardown', None)
-        if td:
-            td()
-
-    date_tolerance = 2
-
-    def assertEqualDates(self, dt1, dt2, seconds=None):
-        """Assert abs(dt1 - dt2) is within Y seconds."""
-        if seconds is None:
-            seconds = self.date_tolerance
-
-        if dt1 > dt2:
-            diff = dt1 - dt2
-        else:
-            diff = dt2 - dt1
-        if not diff < datetime.timedelta(seconds=seconds):
-            raise AssertionError('%r and %r are not within %r seconds.' %
-                                 (dt1, dt2, seconds))
-
-
-class Request:
-    """HTTP request container."""
-
-    def __init__(self, environ):
-        """Initialize HTTP request."""
-        self.environ = environ
-
-
-class Response:
-    """HTTP response container."""
-
-    def __init__(self):
-        """Initialize HTTP response."""
-        self.status = '200 OK'
-        self.headers = {'Content-Type': 'text/html'}
-        self.body = None
-
-    def output(self):
-        """Generate iterable response body object."""
-        if self.body is None:
-            return []
-        elif isinstance(self.body, six.text_type):
-            return [self.body.encode('iso-8859-1')]
-        elif isinstance(self.body, six.binary_type):
-            return [self.body]
-        else:
-            return [x.encode('iso-8859-1') for x in self.body]
-
-
-class Controller:
-    """WSGI app for tests."""
-
-    def __call__(self, environ, start_response):
-        """WSGI request handler."""
-        req, resp = Request(environ), Response()
-        try:
-            # Python 3 supports unicode attribute names
-            # Python 2 encodes them
-            handler = self.handlers[environ['PATH_INFO']]
-        except KeyError:
-            resp.status = '404 Not Found'
-        else:
-            output = handler(req, resp)
-            if (output is not None and
-                    not any(resp.status.startswith(status_code)
-                            for status_code in ('204', '304'))):
-                resp.body = output
-                try:
-                    resp.headers.setdefault('Content-Length', str(len(output)))
-                except TypeError:
-                    if not isinstance(output, types.GeneratorType):
-                        raise
-        start_response(resp.status, resp.headers.items())
-        return resp.output()
diff --git a/libraries/cheroot/test/test.pem b/libraries/cheroot/test/test.pem
deleted file mode 100644
index 47a47042..00000000
--- a/libraries/cheroot/test/test.pem
+++ /dev/null
@@ -1,38 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
-R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
-da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
-AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
-9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
-enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
-8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
-tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
-0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
-MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
-yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
-8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
-yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
------END RSA PRIVATE KEY-----
------BEGIN CERTIFICATE-----
-MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
-VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
-MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
-MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
-cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
-A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
-bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
-FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
-cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
-ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
-C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
-KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
-2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
-/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
-YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
-MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
-CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
-BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
-8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
-D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
-NluCaWQys3MS
------END CERTIFICATE-----
diff --git a/libraries/cheroot/test/test__compat.py b/libraries/cheroot/test/test__compat.py
deleted file mode 100644
index d34e5eb8..00000000
--- a/libraries/cheroot/test/test__compat.py
+++ /dev/null
@@ -1,49 +0,0 @@
-# -*- coding: utf-8 -*-
-"""Test suite for cross-python compatibility helpers."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import pytest
-import six
-
-from cheroot._compat import ntob, ntou, bton
-
-
-@pytest.mark.parametrize(
-    'func,inp,out',
-    [
-        (ntob, 'bar', b'bar'),
-        (ntou, 'bar', u'bar'),
-        (bton, b'bar', 'bar'),
-    ],
-)
-def test_compat_functions_positive(func, inp, out):
-    """Check that compat functions work with correct input."""
-    assert func(inp, encoding='utf-8') == out
-
-
-@pytest.mark.parametrize(
-    'func',
-    [
-        ntob,
-        ntou,
-    ],
-)
-def test_compat_functions_negative_nonnative(func):
-    """Check that compat functions fail loudly for incorrect input."""
-    non_native_test_str = b'bar' if six.PY3 else u'bar'
-    with pytest.raises(TypeError):
-        func(non_native_test_str, encoding='utf-8')
-
-
-@pytest.mark.skip(reason='This test does not work now')
-@pytest.mark.skipif(
-    six.PY3,
-    reason='This code path only appears in Python 2 version.',
-)
-def test_ntou_escape():
-    """Check that ntou supports escape-encoding under Python 2."""
-    expected = u''
-    actual = ntou('hi'.encode('ISO-8859-1'), encoding='escape')
-    assert actual == expected
diff --git a/libraries/cheroot/test/test_conn.py b/libraries/cheroot/test/test_conn.py
deleted file mode 100644
index f543dd9b..00000000
--- a/libraries/cheroot/test/test_conn.py
+++ /dev/null
@@ -1,897 +0,0 @@
-"""Tests for TCP connection handling, including proper and timely close."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import socket
-import time
-
-from six.moves import range, http_client, urllib
-
-import six
-import pytest
-
-from cheroot.test import helper, webtest
-
-
-timeout = 1
-pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
-
-
-class Controller(helper.Controller):
-    """Controller for serving WSGI apps."""
-
-    def hello(req, resp):
-        """Render Hello world."""
-        return 'Hello, world!'
-
-    def pov(req, resp):
-        """Render pov value."""
-        return pov
-
-    def stream(req, resp):
-        """Render streaming response."""
-        if 'set_cl' in req.environ['QUERY_STRING']:
-            resp.headers['Content-Length'] = str(10)
-
-        def content():
-            for x in range(10):
-                yield str(x)
-
-        return content()
-
-    def upload(req, resp):
-        """Process file upload and render thank."""
-        if not req.environ['REQUEST_METHOD'] == 'POST':
-            raise AssertionError("'POST' != request.method %r" %
-                                 req.environ['REQUEST_METHOD'])
-        return "thanks for '%s'" % req.environ['wsgi.input'].read()
-
-    def custom_204(req, resp):
-        """Render response with status 204."""
-        resp.status = '204'
-        return 'Code = 204'
-
-    def custom_304(req, resp):
-        """Render response with status 304."""
-        resp.status = '304'
-        return 'Code = 304'
-
-    def err_before_read(req, resp):
-        """Render response with status 500."""
-        resp.status = '500 Internal Server Error'
-        return 'ok'
-
-    def one_megabyte_of_a(req, resp):
-        """Render 1MB response."""
-        return ['a' * 1024] * 1024
-
-    def wrong_cl_buffered(req, resp):
-        """Render buffered response with invalid length value."""
-        resp.headers['Content-Length'] = '5'
-        return 'I have too many bytes'
-
-    def wrong_cl_unbuffered(req, resp):
-        """Render unbuffered response with invalid length value."""
-        resp.headers['Content-Length'] = '5'
-        return ['I too', ' have too many bytes']
-
-    def _munge(string):
-        """Encode PATH_INFO correctly depending on Python version.
-
-        WSGI 1.0 is a mess around unicode. Create endpoints
-        that match the PATH_INFO that it produces.
-        """
-        if six.PY3:
-            return string.encode('utf-8').decode('latin-1')
-        return string
-
-    handlers = {
-        '/hello': hello,
-        '/pov': pov,
-        '/page1': pov,
-        '/page2': pov,
-        '/page3': pov,
-        '/stream': stream,
-        '/upload': upload,
-        '/custom/204': custom_204,
-        '/custom/304': custom_304,
-        '/err_before_read': err_before_read,
-        '/one_megabyte_of_a': one_megabyte_of_a,
-        '/wrong_cl_buffered': wrong_cl_buffered,
-        '/wrong_cl_unbuffered': wrong_cl_unbuffered,
-    }
-
-
-@pytest.fixture
-def testing_server(wsgi_server_client):
-    """Attach a WSGI app to the given server and pre-configure it."""
-    app = Controller()
-
-    def _timeout(req, resp):
-        return str(wsgi_server.timeout)
-    app.handlers['/timeout'] = _timeout
-    wsgi_server = wsgi_server_client.server_instance
-    wsgi_server.wsgi_app = app
-    wsgi_server.max_request_body_size = 1001
-    wsgi_server.timeout = timeout
-    wsgi_server.server_client = wsgi_server_client
-    return wsgi_server
-
-
-@pytest.fixture
-def test_client(testing_server):
-    """Get and return a test client out of the given server."""
-    return testing_server.server_client
-
-
-def header_exists(header_name, headers):
-    """Check that a header is present."""
-    return header_name.lower() in (k.lower() for (k, _) in headers)
-
-
-def header_has_value(header_name, header_value, headers):
-    """Check that a header with a given value is present."""
-    return header_name.lower() in (k.lower() for (k, v) in headers
-                                   if v == header_value)
-
-
-def test_HTTP11_persistent_connections(test_client):
-    """Test persistent HTTP/1.1 connections."""
-    # Initialize a persistent HTTP connection
-    http_connection = test_client.get_connection()
-    http_connection.auto_open = False
-    http_connection.connect()
-
-    # Make the first request and assert there's no "Connection: close".
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/pov', http_conn=http_connection
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert not header_exists('Connection', actual_headers)
-
-    # Make another request on the same connection.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/page1', http_conn=http_connection
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert not header_exists('Connection', actual_headers)
-
-    # Test client-side close.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/page2', http_conn=http_connection,
-        headers=[('Connection', 'close')]
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert header_has_value('Connection', 'close', actual_headers)
-
-    # Make another request on the same connection, which should error.
-    with pytest.raises(http_client.NotConnected):
-        test_client.get('/pov', http_conn=http_connection)
-
-
-@pytest.mark.parametrize(
-    'set_cl',
-    (
-        False,  # Without Content-Length
-        True,  # With Content-Length
-    )
-)
-def test_streaming_11(test_client, set_cl):
-    """Test serving of streaming responses with HTTP/1.1 protocol."""
-    # Initialize a persistent HTTP connection
-    http_connection = test_client.get_connection()
-    http_connection.auto_open = False
-    http_connection.connect()
-
-    # Make the first request and assert there's no "Connection: close".
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/pov', http_conn=http_connection
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert not header_exists('Connection', actual_headers)
-
-    # Make another, streamed request on the same connection.
-    if set_cl:
-        # When a Content-Length is provided, the content should stream
-        # without closing the connection.
-        status_line, actual_headers, actual_resp_body = test_client.get(
-            '/stream?set_cl=Yes', http_conn=http_connection
-        )
-        assert header_exists('Content-Length', actual_headers)
-        assert not header_has_value('Connection', 'close', actual_headers)
-        assert not header_exists('Transfer-Encoding', actual_headers)
-
-        assert actual_status == 200
-        assert status_line[4:] == 'OK'
-        assert actual_resp_body == b'0123456789'
-    else:
-        # When no Content-Length response header is provided,
-        # streamed output will either close the connection, or use
-        # chunked encoding, to determine transfer-length.
-        status_line, actual_headers, actual_resp_body = test_client.get(
-            '/stream', http_conn=http_connection
-        )
-        assert not header_exists('Content-Length', actual_headers)
-        assert actual_status == 200
-        assert status_line[4:] == 'OK'
-        assert actual_resp_body == b'0123456789'
-
-        chunked_response = False
-        for k, v in actual_headers:
-            if k.lower() == 'transfer-encoding':
-                if str(v) == 'chunked':
-                    chunked_response = True
-
-        if chunked_response:
-            assert not header_has_value('Connection', 'close', actual_headers)
-        else:
-            assert header_has_value('Connection', 'close', actual_headers)
-
-            # Make another request on the same connection, which should
-            # error.
-            with pytest.raises(http_client.NotConnected):
-                test_client.get('/pov', http_conn=http_connection)
-
-        # Try HEAD.
-        # See https://www.bitbucket.org/cherrypy/cherrypy/issue/864.
-        # TODO: figure out how can this be possible on an closed connection
-        # (chunked_response case)
-        status_line, actual_headers, actual_resp_body = test_client.head(
-            '/stream', http_conn=http_connection
-        )
-        assert actual_status == 200
-        assert status_line[4:] == 'OK'
-        assert actual_resp_body == b''
-        assert not header_exists('Transfer-Encoding', actual_headers)
-
-
-@pytest.mark.parametrize(
-    'set_cl',
-    (
-        False,  # Without Content-Length
-        True,  # With Content-Length
-    )
-)
-def test_streaming_10(test_client, set_cl):
-    """Test serving of streaming responses with HTTP/1.0 protocol."""
-    original_server_protocol = test_client.server_instance.protocol
-    test_client.server_instance.protocol = 'HTTP/1.0'
-
-    # Initialize a persistent HTTP connection
-    http_connection = test_client.get_connection()
-    http_connection.auto_open = False
-    http_connection.connect()
-
-    # Make the first request and assert Keep-Alive.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/pov', http_conn=http_connection,
-        headers=[('Connection', 'Keep-Alive')],
-        protocol='HTTP/1.0',
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert header_has_value('Connection', 'Keep-Alive', actual_headers)
-
-    # Make another, streamed request on the same connection.
-    if set_cl:
-        # When a Content-Length is provided, the content should
-        # stream without closing the connection.
-        status_line, actual_headers, actual_resp_body = test_client.get(
-            '/stream?set_cl=Yes', http_conn=http_connection,
-            headers=[('Connection', 'Keep-Alive')],
-            protocol='HTTP/1.0',
-        )
-        actual_status = int(status_line[:3])
-        assert actual_status == 200
-        assert status_line[4:] == 'OK'
-        assert actual_resp_body == b'0123456789'
-
-        assert header_exists('Content-Length', actual_headers)
-        assert header_has_value('Connection', 'Keep-Alive', actual_headers)
-        assert not header_exists('Transfer-Encoding', actual_headers)
-    else:
-        # When a Content-Length is not provided,
-        # the server should close the connection.
-        status_line, actual_headers, actual_resp_body = test_client.get(
-            '/stream', http_conn=http_connection,
-            headers=[('Connection', 'Keep-Alive')],
-            protocol='HTTP/1.0',
-        )
-        actual_status = int(status_line[:3])
-        assert actual_status == 200
-        assert status_line[4:] == 'OK'
-        assert actual_resp_body == b'0123456789'
-
-        assert not header_exists('Content-Length', actual_headers)
-        assert not header_has_value('Connection', 'Keep-Alive', actual_headers)
-        assert not header_exists('Transfer-Encoding', actual_headers)
-
-        # Make another request on the same connection, which should error.
-        with pytest.raises(http_client.NotConnected):
-            test_client.get(
-                '/pov', http_conn=http_connection,
-                protocol='HTTP/1.0',
-            )
-
-    test_client.server_instance.protocol = original_server_protocol
-
-
-@pytest.mark.parametrize(
-    'http_server_protocol',
-    (
-        'HTTP/1.0',
-        'HTTP/1.1',
-    )
-)
-def test_keepalive(test_client, http_server_protocol):
-    """Test Keep-Alive enabled connections."""
-    original_server_protocol = test_client.server_instance.protocol
-    test_client.server_instance.protocol = http_server_protocol
-
-    http_client_protocol = 'HTTP/1.0'
-
-    # Initialize a persistent HTTP connection
-    http_connection = test_client.get_connection()
-    http_connection.auto_open = False
-    http_connection.connect()
-
-    # Test a normal HTTP/1.0 request.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/page2',
-        protocol=http_client_protocol,
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert not header_exists('Connection', actual_headers)
-
-    # Test a keep-alive HTTP/1.0 request.
-
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/page3', headers=[('Connection', 'Keep-Alive')],
-        http_conn=http_connection, protocol=http_client_protocol,
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert header_has_value('Connection', 'Keep-Alive', actual_headers)
-
-    # Remove the keep-alive header again.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/page3', http_conn=http_connection,
-        protocol=http_client_protocol,
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert not header_exists('Connection', actual_headers)
-
-    test_client.server_instance.protocol = original_server_protocol
-
-
-@pytest.mark.parametrize(
-    'timeout_before_headers',
-    (
-        True,
-        False,
-    )
-)
-def test_HTTP11_Timeout(test_client, timeout_before_headers):
-    """Check timeout without sending any data.
-
-    The server will close the conn with a 408.
-    """
-    conn = test_client.get_connection()
-    conn.auto_open = False
-    conn.connect()
-
-    if not timeout_before_headers:
-        # Connect but send half the headers only.
-        conn.send(b'GET /hello HTTP/1.1')
-        conn.send(('Host: %s' % conn.host).encode('ascii'))
-    # else: Connect but send nothing.
-
-    # Wait for our socket timeout
-    time.sleep(timeout * 2)
-
-    # The request should have returned 408 already.
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    assert response.status == 408
-    conn.close()
-
-
-def test_HTTP11_Timeout_after_request(test_client):
-    """Check timeout after at least one request has succeeded.
-
-    The server should close the connection without 408.
-    """
-    fail_msg = "Writing to timed out socket didn't fail as it should have: %s"
-
-    # Make an initial request
-    conn = test_client.get_connection()
-    conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.endheaders()
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    assert response.status == 200
-    actual_body = response.read()
-    expected_body = str(timeout).encode()
-    assert actual_body == expected_body
-
-    # Make a second request on the same socket
-    conn._output(b'GET /hello HTTP/1.1')
-    conn._output(('Host: %s' % conn.host).encode('ascii'))
-    conn._send_output()
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    assert response.status == 200
-    actual_body = response.read()
-    expected_body = b'Hello, world!'
-    assert actual_body == expected_body
-
-    # Wait for our socket timeout
-    time.sleep(timeout * 2)
-
-    # Make another request on the same socket, which should error
-    conn._output(b'GET /hello HTTP/1.1')
-    conn._output(('Host: %s' % conn.host).encode('ascii'))
-    conn._send_output()
-    response = conn.response_class(conn.sock, method='GET')
-    try:
-        response.begin()
-    except (socket.error, http_client.BadStatusLine):
-        pass
-    except Exception as ex:
-        pytest.fail(fail_msg % ex)
-    else:
-        if response.status != 408:
-            pytest.fail(fail_msg % response.read())
-
-    conn.close()
-
-    # Make another request on a new socket, which should work
-    conn = test_client.get_connection()
-    conn.putrequest('GET', '/pov', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.endheaders()
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    assert response.status == 200
-    actual_body = response.read()
-    expected_body = pov.encode()
-    assert actual_body == expected_body
-
-    # Make another request on the same socket,
-    # but timeout on the headers
-    conn.send(b'GET /hello HTTP/1.1')
-    # Wait for our socket timeout
-    time.sleep(timeout * 2)
-    response = conn.response_class(conn.sock, method='GET')
-    try:
-        response.begin()
-    except (socket.error, http_client.BadStatusLine):
-        pass
-    except Exception as ex:
-        pytest.fail(fail_msg % ex)
-    else:
-        if response.status != 408:
-            pytest.fail(fail_msg % response.read())
-
-    conn.close()
-
-    # Retry the request on a new connection, which should work
-    conn = test_client.get_connection()
-    conn.putrequest('GET', '/pov', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.endheaders()
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    assert response.status == 200
-    actual_body = response.read()
-    expected_body = pov.encode()
-    assert actual_body == expected_body
-    conn.close()
-
-
-def test_HTTP11_pipelining(test_client):
-    """Test HTTP/1.1 pipelining.
-
-    httplib doesn't support this directly.
-    """
-    conn = test_client.get_connection()
-
-    # Put request 1
-    conn.putrequest('GET', '/hello', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.endheaders()
-
-    for trial in range(5):
-        # Put next request
-        conn._output(
-            ('GET /hello?%s HTTP/1.1' % trial).encode('iso-8859-1')
-        )
-        conn._output(('Host: %s' % conn.host).encode('ascii'))
-        conn._send_output()
-
-        # Retrieve previous response
-        response = conn.response_class(conn.sock, method='GET')
-        # there is a bug in python3 regarding the buffering of
-        # ``conn.sock``. Until that bug get's fixed we will
-        # monkey patch the ``reponse`` instance.
-        # https://bugs.python.org/issue23377
-        if six.PY3:
-            response.fp = conn.sock.makefile('rb', 0)
-        response.begin()
-        body = response.read(13)
-        assert response.status == 200
-        assert body == b'Hello, world!'
-
-    # Retrieve final response
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    body = response.read()
-    assert response.status == 200
-    assert body == b'Hello, world!'
-
-    conn.close()
-
-
-def test_100_Continue(test_client):
-    """Test 100-continue header processing."""
-    conn = test_client.get_connection()
-
-    # Try a page without an Expect request header first.
-    # Note that httplib's response.begin automatically ignores
-    # 100 Continue responses, so we must manually check for it.
-    conn.putrequest('POST', '/upload', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.putheader('Content-Type', 'text/plain')
-    conn.putheader('Content-Length', '4')
-    conn.endheaders()
-    conn.send(b"d'oh")
-    response = conn.response_class(conn.sock, method='POST')
-    version, status, reason = response._read_status()
-    assert status != 100
-    conn.close()
-
-    # Now try a page with an Expect header...
-    conn.connect()
-    conn.putrequest('POST', '/upload', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.putheader('Content-Type', 'text/plain')
-    conn.putheader('Content-Length', '17')
-    conn.putheader('Expect', '100-continue')
-    conn.endheaders()
-    response = conn.response_class(conn.sock, method='POST')
-
-    # ...assert and then skip the 100 response
-    version, status, reason = response._read_status()
-    assert status == 100
-    while True:
-        line = response.fp.readline().strip()
-        if line:
-            pytest.fail(
-                '100 Continue should not output any headers. Got %r' %
-                line)
-        else:
-            break
-
-    # ...send the body
-    body = b'I am a small file'
-    conn.send(body)
-
-    # ...get the final response
-    response.begin()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    expected_resp_body = ("thanks for '%s'" % body).encode()
-    assert actual_resp_body == expected_resp_body
-    conn.close()
-
-
-@pytest.mark.parametrize(
-    'max_request_body_size',
-    (
-        0,
-        1001,
-    )
-)
-def test_readall_or_close(test_client, max_request_body_size):
-    """Test a max_request_body_size of 0 (the default) and 1001."""
-    old_max = test_client.server_instance.max_request_body_size
-
-    test_client.server_instance.max_request_body_size = max_request_body_size
-
-    conn = test_client.get_connection()
-
-    # Get a POST page with an error
-    conn.putrequest('POST', '/err_before_read', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.putheader('Content-Type', 'text/plain')
-    conn.putheader('Content-Length', '1000')
-    conn.putheader('Expect', '100-continue')
-    conn.endheaders()
-    response = conn.response_class(conn.sock, method='POST')
-
-    # ...assert and then skip the 100 response
-    version, status, reason = response._read_status()
-    assert status == 100
-    skip = True
-    while skip:
-        skip = response.fp.readline().strip()
-
-    # ...send the body
-    conn.send(b'x' * 1000)
-
-    # ...get the final response
-    response.begin()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-    assert actual_status == 500
-
-    # Now try a working page with an Expect header...
-    conn._output(b'POST /upload HTTP/1.1')
-    conn._output(('Host: %s' % conn.host).encode('ascii'))
-    conn._output(b'Content-Type: text/plain')
-    conn._output(b'Content-Length: 17')
-    conn._output(b'Expect: 100-continue')
-    conn._send_output()
-    response = conn.response_class(conn.sock, method='POST')
-
-    # ...assert and then skip the 100 response
-    version, status, reason = response._read_status()
-    assert status == 100
-    skip = True
-    while skip:
-        skip = response.fp.readline().strip()
-
-    # ...send the body
-    body = b'I am a small file'
-    conn.send(body)
-
-    # ...get the final response
-    response.begin()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    expected_resp_body = ("thanks for '%s'" % body).encode()
-    assert actual_resp_body == expected_resp_body
-    conn.close()
-
-    test_client.server_instance.max_request_body_size = old_max
-
-
-def test_No_Message_Body(test_client):
-    """Test HTTP queries with an empty response body."""
-    # Initialize a persistent HTTP connection
-    http_connection = test_client.get_connection()
-    http_connection.auto_open = False
-    http_connection.connect()
-
-    # Make the first request and assert there's no "Connection: close".
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/pov', http_conn=http_connection
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    assert actual_resp_body == pov.encode()
-    assert not header_exists('Connection', actual_headers)
-
-    # Make a 204 request on the same connection.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/custom/204', http_conn=http_connection
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 204
-    assert not header_exists('Content-Length', actual_headers)
-    assert actual_resp_body == b''
-    assert not header_exists('Connection', actual_headers)
-
-    # Make a 304 request on the same connection.
-    status_line, actual_headers, actual_resp_body = test_client.get(
-        '/custom/304', http_conn=http_connection
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == 304
-    assert not header_exists('Content-Length', actual_headers)
-    assert actual_resp_body == b''
-    assert not header_exists('Connection', actual_headers)
-
-
-@pytest.mark.xfail(
-    reason='Server does not correctly read trailers/ending of the previous '
-           'HTTP request, thus the second request fails as the server tries '
-           r"to parse b'Content-Type: application/json\r\n' as a "
-           'Request-Line. This results in HTTP status code 400, instead of 413'
-           'Ref: https://github.com/cherrypy/cheroot/issues/69'
-)
-def test_Chunked_Encoding(test_client):
-    """Test HTTP uploads with chunked transfer-encoding."""
-    # Initialize a persistent HTTP connection
-    conn = test_client.get_connection()
-
-    # Try a normal chunked request (with extensions)
-    body = (
-        b'8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
-        b'Content-Type: application/json\r\n'
-        b'\r\n'
-    )
-    conn.putrequest('POST', '/upload', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.putheader('Transfer-Encoding', 'chunked')
-    conn.putheader('Trailer', 'Content-Type')
-    # Note that this is somewhat malformed:
-    # we shouldn't be sending Content-Length.
-    # RFC 2616 says the server should ignore it.
-    conn.putheader('Content-Length', '3')
-    conn.endheaders()
-    conn.send(body)
-    response = conn.getresponse()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-    assert actual_status == 200
-    assert status_line[4:] == 'OK'
-    expected_resp_body = ("thanks for '%s'" % b'xx\r\nxxxxyyyyy').encode()
-    assert actual_resp_body == expected_resp_body
-
-    # Try a chunked request that exceeds server.max_request_body_size.
-    # Note that the delimiters and trailer are included.
-    body = b'3e3\r\n' + (b'x' * 995) + b'\r\n0\r\n\r\n'
-    conn.putrequest('POST', '/upload', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.putheader('Transfer-Encoding', 'chunked')
-    conn.putheader('Content-Type', 'text/plain')
-    # Chunked requests don't need a content-length
-    # conn.putheader("Content-Length", len(body))
-    conn.endheaders()
-    conn.send(body)
-    response = conn.getresponse()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-    assert actual_status == 413
-    conn.close()
-
-
-def test_Content_Length_in(test_client):
-    """Try a non-chunked request where Content-Length exceeds limit.
-
-    (server.max_request_body_size).
-    Assert error before body send.
-    """
-    # Initialize a persistent HTTP connection
-    conn = test_client.get_connection()
-
-    conn.putrequest('POST', '/upload', skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.putheader('Content-Type', 'text/plain')
-    conn.putheader('Content-Length', '9999')
-    conn.endheaders()
-    response = conn.getresponse()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-    assert actual_status == 413
-    expected_resp_body = (
-        b'The entity sent with the request exceeds '
-        b'the maximum allowed bytes.'
-    )
-    assert actual_resp_body == expected_resp_body
-    conn.close()
-
-
-def test_Content_Length_not_int(test_client):
-    """Test that malicious Content-Length header returns 400."""
-    status_line, actual_headers, actual_resp_body = test_client.post(
-        '/upload',
-        headers=[
-            ('Content-Type', 'text/plain'),
-            ('Content-Length', 'not-an-integer'),
-        ],
-    )
-    actual_status = int(status_line[:3])
-
-    assert actual_status == 400
-    assert actual_resp_body == b'Malformed Content-Length Header.'
-
-
-@pytest.mark.parametrize(
-    'uri,expected_resp_status,expected_resp_body',
-    (
-        ('/wrong_cl_buffered', 500,
-         (b'The requested resource returned more bytes than the '
-          b'declared Content-Length.')),
-        ('/wrong_cl_unbuffered', 200, b'I too'),
-    )
-)
-def test_Content_Length_out(
-    test_client,
-    uri, expected_resp_status, expected_resp_body
-):
-    """Test response with Content-Length less than the response body.
-
-    (non-chunked response)
-    """
-    conn = test_client.get_connection()
-    conn.putrequest('GET', uri, skip_host=True)
-    conn.putheader('Host', conn.host)
-    conn.endheaders()
-
-    response = conn.getresponse()
-    status_line, actual_headers, actual_resp_body = webtest.shb(response)
-    actual_status = int(status_line[:3])
-
-    assert actual_status == expected_resp_status
-    assert actual_resp_body == expected_resp_body
-
-    conn.close()
-
-
-@pytest.mark.xfail(
-    reason='Sometimes this test fails due to low timeout. '
-           'Ref: https://github.com/cherrypy/cherrypy/issues/598'
-)
-def test_598(test_client):
-    """Test serving large file with a read timeout in place."""
-    # Initialize a persistent HTTP connection
-    conn = test_client.get_connection()
-    remote_data_conn = urllib.request.urlopen(
-        '%s://%s:%s/one_megabyte_of_a'
-        % ('http', conn.host, conn.port)
-    )
-    buf = remote_data_conn.read(512)
-    time.sleep(timeout * 0.6)
-    remaining = (1024 * 1024) - 512
-    while remaining:
-        data = remote_data_conn.read(remaining)
-        if not data:
-            break
-        buf += data
-        remaining -= len(data)
-
-    assert len(buf) == 1024 * 1024
-    assert buf == b'a' * 1024 * 1024
-    assert remaining == 0
-    remote_data_conn.close()
-
-
-@pytest.mark.parametrize(
-    'invalid_terminator',
-    (
-        b'\n\n',
-        b'\r\n\n',
-    )
-)
-def test_No_CRLF(test_client, invalid_terminator):
-    """Test HTTP queries with no valid CRLF terminators."""
-    # Initialize a persistent HTTP connection
-    conn = test_client.get_connection()
-
-    # (b'%s' % b'') is not supported in Python 3.4, so just use +
-    conn.send(b'GET /hello HTTP/1.1' + invalid_terminator)
-    response = conn.response_class(conn.sock, method='GET')
-    response.begin()
-    actual_resp_body = response.read()
-    expected_resp_body = b'HTTP requires CRLF terminators'
-    assert actual_resp_body == expected_resp_body
-    conn.close()
diff --git a/libraries/cheroot/test/test_core.py b/libraries/cheroot/test/test_core.py
deleted file mode 100644
index 7c91b13e..00000000
--- a/libraries/cheroot/test/test_core.py
+++ /dev/null
@@ -1,405 +0,0 @@
-"""Tests for managing HTTP issues (malformed requests, etc)."""
-# -*- coding: utf-8 -*-
-# vim: set fileencoding=utf-8 :
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import errno
-import socket
-
-import pytest
-import six
-from six.moves import urllib
-
-from cheroot.test import helper
-
-
-HTTP_BAD_REQUEST = 400
-HTTP_LENGTH_REQUIRED = 411
-HTTP_NOT_FOUND = 404
-HTTP_OK = 200
-HTTP_VERSION_NOT_SUPPORTED = 505
-
-
-class HelloController(helper.Controller):
-    """Controller for serving WSGI apps."""
-
-    def hello(req, resp):
-        """Render Hello world."""
-        return 'Hello world!'
-
-    def body_required(req, resp):
-        """Render Hello world or set 411."""
-        if req.environ.get('Content-Length', None) is None:
-            resp.status = '411 Length Required'
-            return
-        return 'Hello world!'
-
-    def query_string(req, resp):
-        """Render QUERY_STRING value."""
-        return req.environ.get('QUERY_STRING', '')
-
-    def asterisk(req, resp):
-        """Render request method value."""
-        method = req.environ.get('REQUEST_METHOD', 'NO METHOD FOUND')
-        tmpl = 'Got asterisk URI path with {method} method'
-        return tmpl.format(**locals())
-
-    def _munge(string):
-        """Encode PATH_INFO correctly depending on Python version.
-
-        WSGI 1.0 is a mess around unicode. Create endpoints
-        that match the PATH_INFO that it produces.
-        """
-        if six.PY3:
-            return string.encode('utf-8').decode('latin-1')
-        return string
-
-    handlers = {
-        '/hello': hello,
-        '/no_body': hello,
-        '/body_required': body_required,
-        '/query_string': query_string,
-        _munge('/привіт'): hello,
-        _munge('/Юххууу'): hello,
-        '/\xa0Ðblah key 0 900 4 data': hello,
-        '/*': asterisk,
-    }
-
-
-def _get_http_response(connection, method='GET'):
-    c = connection
-    kwargs = {'strict': c.strict} if hasattr(c, 'strict') else {}
-    # Python 3.2 removed the 'strict' feature, saying:
-    # "http.client now always assumes HTTP/1.x compliant servers."
-    return c.response_class(c.sock, method=method, **kwargs)
-
-
-@pytest.fixture
-def testing_server(wsgi_server_client):
-    """Attach a WSGI app to the given server and pre-configure it."""
-    wsgi_server = wsgi_server_client.server_instance
-    wsgi_server.wsgi_app = HelloController()
-    wsgi_server.max_request_body_size = 30000000
-    wsgi_server.server_client = wsgi_server_client
-    return wsgi_server
-
-
-@pytest.fixture
-def test_client(testing_server):
-    """Get and return a test client out of the given server."""
-    return testing_server.server_client
-
-
-def test_http_connect_request(test_client):
-    """Check that CONNECT query results in Method Not Allowed status."""
-    status_line = test_client.connect('/anything')[0]
-    actual_status = int(status_line[:3])
-    assert actual_status == 405
-
-
-def test_normal_request(test_client):
-    """Check that normal GET query succeeds."""
-    status_line, _, actual_resp_body = test_client.get('/hello')
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_OK
-    assert actual_resp_body == b'Hello world!'
-
-
-def test_query_string_request(test_client):
-    """Check that GET param is parsed well."""
-    status_line, _, actual_resp_body = test_client.get(
-        '/query_string?test=True'
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_OK
-    assert actual_resp_body == b'test=True'
-
-
-@pytest.mark.parametrize(
-    'uri',
-    (
-        '/hello',  # plain
-        '/query_string?test=True',  # query
-        '/{0}?{1}={2}'.format(  # quoted unicode
-            *map(urllib.parse.quote, ('Юххууу', 'ї', 'йо'))
-        ),
-    )
-)
-def test_parse_acceptable_uri(test_client, uri):
-    """Check that server responds with OK to valid GET queries."""
-    status_line = test_client.get(uri)[0]
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_OK
-
-
-@pytest.mark.xfail(six.PY2, reason='Fails on Python 2')
-def test_parse_uri_unsafe_uri(test_client):
-    """Test that malicious URI does not allow HTTP injection.
-
-    This effectively checks that sending GET request with URL
-
-    /%A0%D0blah%20key%200%20900%204%20data
-
-    is not converted into
-
-    GET /
-    blah key 0 900 4 data
-    HTTP/1.1
-
-    which would be a security issue otherwise.
-    """
-    c = test_client.get_connection()
-    resource = '/\xa0Ðblah key 0 900 4 data'.encode('latin-1')
-    quoted = urllib.parse.quote(resource)
-    assert quoted == '/%A0%D0blah%20key%200%20900%204%20data'
-    request = 'GET {quoted} HTTP/1.1'.format(**locals())
-    c._output(request.encode('utf-8'))
-    c._send_output()
-    response = _get_http_response(c, method='GET')
-    response.begin()
-    assert response.status == HTTP_OK
-    assert response.fp.read(12) == b'Hello world!'
-    c.close()
-
-
-def test_parse_uri_invalid_uri(test_client):
-    """Check that server responds with Bad Request to invalid GET queries.
-
-    Invalid request line test case: it should only contain US-ASCII.
-    """
-    c = test_client.get_connection()
-    c._output(u'GET /йопта! HTTP/1.1'.encode('utf-8'))
-    c._send_output()
-    response = _get_http_response(c, method='GET')
-    response.begin()
-    assert response.status == HTTP_BAD_REQUEST
-    assert response.fp.read(21) == b'Malformed Request-URI'
-    c.close()
-
-
-@pytest.mark.parametrize(
-    'uri',
-    (
-        'hello',  # ascii
-        'привіт',  # non-ascii
-    )
-)
-def test_parse_no_leading_slash_invalid(test_client, uri):
-    """Check that server responds with Bad Request to invalid GET queries.
-
-    Invalid request line test case: it should have leading slash (be absolute).
-    """
-    status_line, _, actual_resp_body = test_client.get(
-        urllib.parse.quote(uri)
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_BAD_REQUEST
-    assert b'starting with a slash' in actual_resp_body
-
-
-def test_parse_uri_absolute_uri(test_client):
-    """Check that server responds with Bad Request to Absolute URI.
-
-    Only proxy servers should allow this.
-    """
-    status_line, _, actual_resp_body = test_client.get('http://google.com/')
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_BAD_REQUEST
-    expected_body = b'Absolute URI not allowed if server is not a proxy.'
-    assert actual_resp_body == expected_body
-
-
-def test_parse_uri_asterisk_uri(test_client):
-    """Check that server responds with OK to OPTIONS with "*" Absolute URI."""
-    status_line, _, actual_resp_body = test_client.options('*')
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_OK
-    expected_body = b'Got asterisk URI path with OPTIONS method'
-    assert actual_resp_body == expected_body
-
-
-def test_parse_uri_fragment_uri(test_client):
-    """Check that server responds with Bad Request to URI with fragment."""
-    status_line, _, actual_resp_body = test_client.get(
-        '/hello?test=something#fake',
-    )
-    actual_status = int(status_line[:3])
-    assert actual_status == HTTP_BAD_REQUEST
-    expected_body = b'Illegal #fragment in Request-URI.'
-    assert actual_resp_body == expected_body
-
-
-def test_no_content_length(test_client):
-    """Test POST query with an empty body being successful."""
-    # "The presence of a message-body in a request is signaled by the
-    # inclusion of a Content-Length or Transfer-Encoding header field in
-    # the request's message-headers."
-    #
-    # Send a message with neither header and no body.
-    c = test_client.get_connection()
-    c.request('POST', '/no_body')
-    response = c.getresponse()
-    actual_resp_body = response.fp.read()
-    actual_status = response.status
-    assert actual_status == HTTP_OK
-    assert actual_resp_body == b'Hello world!'
-
-
-def test_content_length_required(test_client):
-    """Test POST query with body failing because of missing Content-Length."""
-    # Now send a message that has no Content-Length, but does send a body.
-    # Verify that CP times out the socket and responds
-    # with 411 Length Required.
-
-    c = test_client.get_connection()
-    c.request('POST', '/body_required')
-    response = c.getresponse()
-    response.fp.read()
-
-    actual_status = response.status
-    assert actual_status == HTTP_LENGTH_REQUIRED
-
-
-@pytest.mark.parametrize(
-    'request_line,status_code,expected_body',
-    (
-        (b'GET /',  # missing proto
-         HTTP_BAD_REQUEST, b'Malformed Request-Line'),
-        (b'GET / HTTPS/1.1',  # invalid proto
-         HTTP_BAD_REQUEST, b'Malformed Request-Line: bad protocol'),
-        (b'GET / HTTP/2.15',  # invalid ver
-         HTTP_VERSION_NOT_SUPPORTED, b'Cannot fulfill request'),
-    )
-)
-def test_malformed_request_line(
-    test_client, request_line,
-    status_code, expected_body
-):
-    """Test missing or invalid HTTP version in Request-Line."""
-    c = test_client.get_connection()
-    c._output(request_line)
-    c._send_output()
-    response = _get_http_response(c, method='GET')
-    response.begin()
-    assert response.status == status_code
-    assert response.fp.read(len(expected_body)) == expected_body
-    c.close()
-
-
-def test_malformed_http_method(test_client):
-    """Test non-uppercase HTTP method."""
-    c = test_client.get_connection()
-    c.putrequest('GeT', '/malformed_method_case')
-    c.putheader('Content-Type', 'text/plain')
-    c.endheaders()
-
-    response = c.getresponse()
-    actual_status = response.status
-    assert actual_status == HTTP_BAD_REQUEST
-    actual_resp_body = response.fp.read(21)
-    assert actual_resp_body == b'Malformed method name'
-
-
-def test_malformed_header(test_client):
-    """Check that broken HTTP header results in Bad Request."""
-    c = test_client.get_connection()
-    c.putrequest('GET', '/')
-    c.putheader('Content-Type', 'text/plain')
-    # See https://www.bitbucket.org/cherrypy/cherrypy/issue/941
-    c._output(b'Re, 1.2.3.4#015#012')
-    c.endheaders()
-
-    response = c.getresponse()
-    actual_status = response.status
-    assert actual_status == HTTP_BAD_REQUEST
-    actual_resp_body = response.fp.read(20)
-    assert actual_resp_body == b'Illegal header line.'
-
-
-def test_request_line_split_issue_1220(test_client):
-    """Check that HTTP request line of exactly 256 chars length is OK."""
-    Request_URI = (
-        '/hello?'
-        'intervenant-entreprise-evenement_classaction='
-        'evenement-mailremerciements'
-        '&_path=intervenant-entreprise-evenement'
-        '&intervenant-entreprise-evenement_action-id=19404'
-        '&intervenant-entreprise-evenement_id=19404'
-        '&intervenant-entreprise_id=28092'
-    )
-    assert len('GET %s HTTP/1.1\r\n' % Request_URI) == 256
-
-    actual_resp_body = test_client.get(Request_URI)[2]
-    assert actual_resp_body == b'Hello world!'
-
-
-def test_garbage_in(test_client):
-    """Test that server sends an error for garbage received over TCP."""
-    # Connect without SSL regardless of server.scheme
-
-    c = test_client.get_connection()
-    c._output(b'gjkgjklsgjklsgjkljklsg')
-    c._send_output()
-    response = c.response_class(c.sock, method='GET')
-    try:
-        response.begin()
-        actual_status = response.status
-        assert actual_status == HTTP_BAD_REQUEST
-        actual_resp_body = response.fp.read(22)
-        assert actual_resp_body == b'Malformed Request-Line'
-        c.close()
-    except socket.error as ex:
-        # "Connection reset by peer" is also acceptable.
-        if ex.errno != errno.ECONNRESET:
-            raise
-
-
-class CloseController:
-    """Controller for testing the close callback."""
-
-    def __call__(self, environ, start_response):
-        """Get the req to know header sent status."""
-        self.req = start_response.__self__.req
-        resp = CloseResponse(self.close)
-        start_response(resp.status, resp.headers.items())
-        return resp
-
-    def close(self):
-        """Close, writing hello."""
-        self.req.write(b'hello')
-
-
-class CloseResponse:
-    """Dummy empty response to trigger the no body status."""
-
-    def __init__(self, close):
-        """Use some defaults to ensure we have a header."""
-        self.status = '200 OK'
-        self.headers = {'Content-Type': 'text/html'}
-        self.close = close
-
-    def __getitem__(self, index):
-        """Ensure we don't have a body."""
-        raise IndexError()
-
-    def output(self):
-        """Return self to hook the close method."""
-        return self
-
-
-@pytest.fixture
-def testing_server_close(wsgi_server_client):
-    """Attach a WSGI app to the given server and pre-configure it."""
-    wsgi_server = wsgi_server_client.server_instance
-    wsgi_server.wsgi_app = CloseController()
-    wsgi_server.max_request_body_size = 30000000
-    wsgi_server.server_client = wsgi_server_client
-    return wsgi_server
-
-
-def test_send_header_before_closing(testing_server_close):
-    """Test we are actually sending the headers before calling 'close'."""
-    _, _, resp_body = testing_server_close.server_client.get('/')
-    assert resp_body == b'hello'
diff --git a/libraries/cheroot/test/test_server.py b/libraries/cheroot/test/test_server.py
deleted file mode 100644
index c53f7a81..00000000
--- a/libraries/cheroot/test/test_server.py
+++ /dev/null
@@ -1,193 +0,0 @@
-"""Tests for the HTTP server."""
-# -*- coding: utf-8 -*-
-# vim: set fileencoding=utf-8 :
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import os
-import socket
-import tempfile
-import threading
-import time
-
-import pytest
-
-from .._compat import bton
-from ..server import Gateway, HTTPServer
-from ..testing import (
-    ANY_INTERFACE_IPV4,
-    ANY_INTERFACE_IPV6,
-    EPHEMERAL_PORT,
-    get_server_client,
-)
-
-
-def make_http_server(bind_addr):
-    """Create and start an HTTP server bound to bind_addr."""
-    httpserver = HTTPServer(
-        bind_addr=bind_addr,
-        gateway=Gateway,
-    )
-
-    threading.Thread(target=httpserver.safe_start).start()
-
-    while not httpserver.ready:
-        time.sleep(0.1)
-
-    return httpserver
-
-
-non_windows_sock_test = pytest.mark.skipif(
-    not hasattr(socket, 'AF_UNIX'),
-    reason='UNIX domain sockets are only available under UNIX-based OS',
-)
-
-
-@pytest.fixture
-def http_server():
-    """Provision a server creator as a fixture."""
-    def start_srv():
-        bind_addr = yield
-        httpserver = make_http_server(bind_addr)
-        yield httpserver
-        yield httpserver
-
-    srv_creator = iter(start_srv())
-    next(srv_creator)
-    yield srv_creator
-    try:
-        while True:
-            httpserver = next(srv_creator)
-            if httpserver is not None:
-                httpserver.stop()
-    except StopIteration:
-        pass
-
-
-@pytest.fixture
-def unix_sock_file():
-    """Check that bound UNIX socket address is stored in server."""
-    tmp_sock_fh, tmp_sock_fname = tempfile.mkstemp()
-
-    yield tmp_sock_fname
-
-    os.close(tmp_sock_fh)
-    os.unlink(tmp_sock_fname)
-
-
-def test_prepare_makes_server_ready():
-    """Check that prepare() makes the server ready, and stop() clears it."""
-    httpserver = HTTPServer(
-        bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
-        gateway=Gateway,
-    )
-
-    assert not httpserver.ready
-    assert not httpserver.requests._threads
-
-    httpserver.prepare()
-
-    assert httpserver.ready
-    assert httpserver.requests._threads
-    for thr in httpserver.requests._threads:
-        assert thr.ready
-
-    httpserver.stop()
-
-    assert not httpserver.requests._threads
-    assert not httpserver.ready
-
-
-def test_stop_interrupts_serve():
-    """Check that stop() interrupts running of serve()."""
-    httpserver = HTTPServer(
-        bind_addr=(ANY_INTERFACE_IPV4, EPHEMERAL_PORT),
-        gateway=Gateway,
-    )
-
-    httpserver.prepare()
-    serve_thread = threading.Thread(target=httpserver.serve)
-    serve_thread.start()
-
-    serve_thread.join(0.5)
-    assert serve_thread.is_alive()
-
-    httpserver.stop()
-
-    serve_thread.join(0.5)
-    assert not serve_thread.is_alive()
-
-
-@pytest.mark.parametrize(
-    'ip_addr',
-    (
-        ANY_INTERFACE_IPV4,
-        ANY_INTERFACE_IPV6,
-    )
-)
-def test_bind_addr_inet(http_server, ip_addr):
-    """Check that bound IP address is stored in server."""
-    httpserver = http_server.send((ip_addr, EPHEMERAL_PORT))
-
-    assert httpserver.bind_addr[0] == ip_addr
-    assert httpserver.bind_addr[1] != EPHEMERAL_PORT
-
-
-@non_windows_sock_test
-def test_bind_addr_unix(http_server, unix_sock_file):
-    """Check that bound UNIX socket address is stored in server."""
-    httpserver = http_server.send(unix_sock_file)
-
-    assert httpserver.bind_addr == unix_sock_file
-
-
-@pytest.mark.skip(reason="Abstract sockets don't work currently")
-@non_windows_sock_test
-def test_bind_addr_unix_abstract(http_server):
-    """Check that bound UNIX socket address is stored in server."""
-    unix_abstract_sock = b'\x00cheroot/test/socket/here.sock'
-    httpserver = http_server.send(unix_abstract_sock)
-
-    assert httpserver.bind_addr == unix_abstract_sock
-
-
-PEERCRED_IDS_URI = '/peer_creds/ids'
-PEERCRED_TEXTS_URI = '/peer_creds/texts'
-
-
-class _TestGateway(Gateway):
-    def respond(self):
-        req = self.req
-        conn = req.conn
-        req_uri = bton(req.uri)
-        if req_uri == PEERCRED_IDS_URI:
-            peer_creds = conn.peer_pid, conn.peer_uid, conn.peer_gid
-            return ['|'.join(map(str, peer_creds))]
-        elif req_uri == PEERCRED_TEXTS_URI:
-            return ['!'.join((conn.peer_user, conn.peer_group))]
-        return super(_TestGateway, self).respond()
-
-
-@pytest.mark.skip(
-    reason='Test HTTP client is not able to work through UNIX socket currently'
-)
-@non_windows_sock_test
-def test_peercreds_unix_sock(http_server, unix_sock_file):
-    """Check that peercred lookup and resolution work when enabled."""
-    httpserver = http_server.send(unix_sock_file)
-    httpserver.gateway = _TestGateway
-    httpserver.peercreds_enabled = True
-
-    testclient = get_server_client(httpserver)
-
-    expected_peercreds = os.getpid(), os.getuid(), os.getgid()
-    expected_peercreds = '|'.join(map(str, expected_peercreds))
-    assert testclient.get(PEERCRED_IDS_URI) == expected_peercreds
-    assert 'RuntimeError' in testclient.get(PEERCRED_TEXTS_URI)
-
-    httpserver.peercreds_resolve_enabled = True
-    import grp
-    expected_textcreds = os.getlogin(), grp.getgrgid(os.getgid()).gr_name
-    expected_textcreds = '!'.join(map(str, expected_textcreds))
-    assert testclient.get(PEERCRED_TEXTS_URI) == expected_textcreds
diff --git a/libraries/cheroot/test/webtest.py b/libraries/cheroot/test/webtest.py
deleted file mode 100644
index 43448f5b..00000000
--- a/libraries/cheroot/test/webtest.py
+++ /dev/null
@@ -1,581 +0,0 @@
-"""Extensions to unittest for web frameworks.
-
-Use the WebCase.getPage method to request a page from your HTTP server.
-Framework Integration
-=====================
-If you have control over your server process, you can handle errors
-in the server-side of the HTTP conversation a bit better. You must run
-both the client (your WebCase tests) and the server in the same process
-(but in separate threads, obviously).
-When an error occurs in the framework, call server_error. It will print
-the traceback to stdout, and keep any assertions you have from running
-(the assumption is that, if the server errors, the page output will not
-be of further significance to your tests).
-"""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import pprint
-import re
-import socket
-import sys
-import time
-import traceback
-import os
-import json
-import unittest
-import warnings
-
-from six.moves import range, http_client, map, urllib_parse
-import six
-
-from more_itertools.more import always_iterable
-
-
-def interface(host):
-    """Return an IP address for a client connection given the server host.
-
-    If the server is listening on '0.0.0.0' (INADDR_ANY)
-    or '::' (IN6ADDR_ANY), this will return the proper localhost.
-    """
-    if host == '0.0.0.0':
-        # INADDR_ANY, which should respond on localhost.
-        return '127.0.0.1'
-    if host == '::':
-        # IN6ADDR_ANY, which should respond on localhost.
-        return '::1'
-    return host
-
-
-try:
-    # Jython support
-    if sys.platform[:4] == 'java':
-        def getchar():
-            """Get a key press."""
-            # Hopefully this is enough
-            return sys.stdin.read(1)
-    else:
-        # On Windows, msvcrt.getch reads a single char without output.
-        import msvcrt
-
-        def getchar():
-            """Get a key press."""
-            return msvcrt.getch()
-except ImportError:
-    # Unix getchr
-    import tty
-    import termios
-
-    def getchar():
-        """Get a key press."""
-        fd = sys.stdin.fileno()
-        old_settings = termios.tcgetattr(fd)
-        try:
-            tty.setraw(sys.stdin.fileno())
-            ch = sys.stdin.read(1)
-        finally:
-            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
-        return ch
-
-
-# from jaraco.properties
-class NonDataProperty:
-    """Non-data property decorator."""
-
-    def __init__(self, fget):
-        """Initialize a non-data property."""
-        assert fget is not None, 'fget cannot be none'
-        assert callable(fget), 'fget must be callable'
-        self.fget = fget
-
-    def __get__(self, obj, objtype=None):
-        """Return a class property."""
-        if obj is None:
-            return self
-        return self.fget(obj)
-
-
-class WebCase(unittest.TestCase):
-    """Helper web test suite base."""
-
-    HOST = '127.0.0.1'
-    PORT = 8000
-    HTTP_CONN = http_client.HTTPConnection
-    PROTOCOL = 'HTTP/1.1'
-
-    scheme = 'http'
-    url = None
-
-    status = None
-    headers = None
-    body = None
-
-    encoding = 'utf-8'
-
-    time = None
-
-    @property
-    def _Conn(self):
-        """Return HTTPConnection or HTTPSConnection based on self.scheme.
-
-        * from http.client.
-        """
-        cls_name = '{scheme}Connection'.format(scheme=self.scheme.upper())
-        return getattr(http_client, cls_name)
-
-    def get_conn(self, auto_open=False):
-        """Return a connection to our HTTP server."""
-        conn = self._Conn(self.interface(), self.PORT)
-        # Automatically re-connect?
-        conn.auto_open = auto_open
-        conn.connect()
-        return conn
-
-    def set_persistent(self, on=True, auto_open=False):
-        """Make our HTTP_CONN persistent (or not).
-
-        If the 'on' argument is True (the default), then self.HTTP_CONN
-        will be set to an instance of HTTP(S)?Connection
-        to persist across requests.
-        As this class only allows for a single open connection, if
-        self already has an open connection, it will be closed.
-        """
-        try:
-            self.HTTP_CONN.close()
-        except (TypeError, AttributeError):
-            pass
-
-        self.HTTP_CONN = (
-            self.get_conn(auto_open=auto_open)
-            if on
-            else self._Conn
-        )
-
-    @property
-    def persistent(self):  # noqa: D401; irrelevant for properties
-        """Presense of the persistent HTTP connection."""
-        return hasattr(self.HTTP_CONN, '__class__')
-
-    @persistent.setter
-    def persistent(self, on):
-        self.set_persistent(on)
-
-    def interface(self):
-        """Return an IP address for a client connection.
-
-        If the server is listening on '0.0.0.0' (INADDR_ANY)
-        or '::' (IN6ADDR_ANY), this will return the proper localhost.
-        """
-        return interface(self.HOST)
-
-    def getPage(self, url, headers=None, method='GET', body=None,
-                protocol=None, raise_subcls=None):
-        """Open the url with debugging support. Return status, headers, body.
-
-        url should be the identifier passed to the server, typically a
-        server-absolute path and query string (sent between method and
-        protocol), and should only be an absolute URI if proxy support is
-        enabled in the server.
-
-        If the application under test generates absolute URIs, be sure
-        to wrap them first with strip_netloc::
-
-            class MyAppWebCase(WebCase):
-                def getPage(url, *args, **kwargs):
-                    super(MyAppWebCase, self).getPage(
-                        cheroot.test.webtest.strip_netloc(url),
-                        *args, **kwargs
-                    )
-
-        `raise_subcls` must be a tuple with the exceptions classes
-        or a single exception class that are not going to be considered
-        a socket.error regardless that they were are subclass of a
-        socket.error and therefore not considered for a connection retry.
-        """
-        ServerError.on = False
-
-        if isinstance(url, six.text_type):
-            url = url.encode('utf-8')
-        if isinstance(body, six.text_type):
-            body = body.encode('utf-8')
-
-        self.url = url
-        self.time = None
-        start = time.time()
-        result = openURL(url, headers, method, body, self.HOST, self.PORT,
-                         self.HTTP_CONN, protocol or self.PROTOCOL,
-                         raise_subcls)
-        self.time = time.time() - start
-        self.status, self.headers, self.body = result
-
-        # Build a list of request cookies from the previous response cookies.
-        self.cookies = [('Cookie', v) for k, v in self.headers
-                        if k.lower() == 'set-cookie']
-
-        if ServerError.on:
-            raise ServerError()
-        return result
-
-    @NonDataProperty
-    def interactive(self):
-        """Determine whether tests are run in interactive mode.
-
-        Load interactivity setting from environment, where
-        the value can be numeric or a string like true or
-        False or 1 or 0.
-        """
-        env_str = os.environ.get('WEBTEST_INTERACTIVE', 'True')
-        is_interactive = bool(json.loads(env_str.lower()))
-        if is_interactive:
-            warnings.warn(
-                'Interactive test failure interceptor support via '
-                'WEBTEST_INTERACTIVE environment variable is deprecated.',
-                DeprecationWarning
-            )
-        return is_interactive
-
-    console_height = 30
-
-    def _handlewebError(self, msg):
-        print('')
-        print('    ERROR: %s' % msg)
-
-        if not self.interactive:
-            raise self.failureException(msg)
-
-        p = ('    Show: '
-             '[B]ody [H]eaders [S]tatus [U]RL; '
-             '[I]gnore, [R]aise, or sys.e[X]it >> ')
-        sys.stdout.write(p)
-        sys.stdout.flush()
-        while True:
-            i = getchar().upper()
-            if not isinstance(i, type('')):
-                i = i.decode('ascii')
-            if i not in 'BHSUIRX':
-                continue
-            print(i.upper())  # Also prints new line
-            if i == 'B':
-                for x, line in enumerate(self.body.splitlines()):
-                    if (x + 1) % self.console_height == 0:
-                        # The \r and comma should make the next line overwrite
-                        sys.stdout.write('<-- More -->\r')
-                        m = getchar().lower()
-                        # Erase our "More" prompt
-                        sys.stdout.write('            \r')
-                        if m == 'q':
-                            break
-                    print(line)
-            elif i == 'H':
-                pprint.pprint(self.headers)
-            elif i == 'S':
-                print(self.status)
-            elif i == 'U':
-                print(self.url)
-            elif i == 'I':
-                # return without raising the normal exception
-                return
-            elif i == 'R':
-                raise self.failureException(msg)
-            elif i == 'X':
-                sys.exit()
-            sys.stdout.write(p)
-            sys.stdout.flush()
-
-    @property
-    def status_code(self):  # noqa: D401; irrelevant for properties
-        """Integer HTTP status code."""
-        return int(self.status[:3])
-
-    def status_matches(self, expected):
-        """Check whether actual status matches expected."""
-        actual = (
-            self.status_code
-            if isinstance(expected, int) else
-            self.status
-        )
-        return expected == actual
-
-    def assertStatus(self, status, msg=None):
-        """Fail if self.status != status.
-
-        status may be integer code, exact string status, or
-        iterable of allowed possibilities.
-        """
-        if any(map(self.status_matches, always_iterable(status))):
-            return
-
-        tmpl = 'Status {self.status} does not match {status}'
-        msg = msg or tmpl.format(**locals())
-        self._handlewebError(msg)
-
-    def assertHeader(self, key, value=None, msg=None):
-        """Fail if (key, [value]) not in self.headers."""
-        lowkey = key.lower()
-        for k, v in self.headers:
-            if k.lower() == lowkey:
-                if value is None or str(value) == v:
-                    return v
-
-        if msg is None:
-            if value is None:
-                msg = '%r not in headers' % key
-            else:
-                msg = '%r:%r not in headers' % (key, value)
-        self._handlewebError(msg)
-
-    def assertHeaderIn(self, key, values, msg=None):
-        """Fail if header indicated by key doesn't have one of the values."""
-        lowkey = key.lower()
-        for k, v in self.headers:
-            if k.lower() == lowkey:
-                matches = [value for value in values if str(value) == v]
-                if matches:
-                    return matches
-
-        if msg is None:
-            msg = '%(key)r not in %(values)r' % vars()
-        self._handlewebError(msg)
-
-    def assertHeaderItemValue(self, key, value, msg=None):
-        """Fail if the header does not contain the specified value."""
-        actual_value = self.assertHeader(key, msg=msg)
-        header_values = map(str.strip, actual_value.split(','))
-        if value in header_values:
-            return value
-
-        if msg is None:
-            msg = '%r not in %r' % (value, header_values)
-        self._handlewebError(msg)
-
-    def assertNoHeader(self, key, msg=None):
-        """Fail if key in self.headers."""
-        lowkey = key.lower()
-        matches = [k for k, v in self.headers if k.lower() == lowkey]
-        if matches:
-            if msg is None:
-                msg = '%r in headers' % key
-            self._handlewebError(msg)
-
-    def assertNoHeaderItemValue(self, key, value, msg=None):
-        """Fail if the header contains the specified value."""
-        lowkey = key.lower()
-        hdrs = self.headers
-        matches = [k for k, v in hdrs if k.lower() == lowkey and v == value]
-        if matches:
-            if msg is None:
-                msg = '%r:%r in %r' % (key, value, hdrs)
-            self._handlewebError(msg)
-
-    def assertBody(self, value, msg=None):
-        """Fail if value != self.body."""
-        if isinstance(value, six.text_type):
-            value = value.encode(self.encoding)
-        if value != self.body:
-            if msg is None:
-                msg = 'expected body:\n%r\n\nactual body:\n%r' % (
-                    value, self.body)
-            self._handlewebError(msg)
-
-    def assertInBody(self, value, msg=None):
-        """Fail if value not in self.body."""
-        if isinstance(value, six.text_type):
-            value = value.encode(self.encoding)
-        if value not in self.body:
-            if msg is None:
-                msg = '%r not in body: %s' % (value, self.body)
-            self._handlewebError(msg)
-
-    def assertNotInBody(self, value, msg=None):
-        """Fail if value in self.body."""
-        if isinstance(value, six.text_type):
-            value = value.encode(self.encoding)
-        if value in self.body:
-            if msg is None:
-                msg = '%r found in body' % value
-            self._handlewebError(msg)
-
-    def assertMatchesBody(self, pattern, msg=None, flags=0):
-        """Fail if value (a regex pattern) is not in self.body."""
-        if isinstance(pattern, six.text_type):
-            pattern = pattern.encode(self.encoding)
-        if re.search(pattern, self.body, flags) is None:
-            if msg is None:
-                msg = 'No match for %r in body' % pattern
-            self._handlewebError(msg)
-
-
-methods_with_bodies = ('POST', 'PUT', 'PATCH')
-
-
-def cleanHeaders(headers, method, body, host, port):
-    """Return request headers, with required headers added (if missing)."""
-    if headers is None:
-        headers = []
-
-    # Add the required Host request header if not present.
-    # [This specifies the host:port of the server, not the client.]
-    found = False
-    for k, v in headers:
-        if k.lower() == 'host':
-            found = True
-            break
-    if not found:
-        if port == 80:
-            headers.append(('Host', host))
-        else:
-            headers.append(('Host', '%s:%s' % (host, port)))
-
-    if method in methods_with_bodies:
-        # Stick in default type and length headers if not present
-        found = False
-        for k, v in headers:
-            if k.lower() == 'content-type':
-                found = True
-                break
-        if not found:
-            headers.append(
-                ('Content-Type', 'application/x-www-form-urlencoded'))
-            headers.append(('Content-Length', str(len(body or ''))))
-
-    return headers
-
-
-def shb(response):
-    """Return status, headers, body the way we like from a response."""
-    if six.PY3:
-        h = response.getheaders()
-    else:
-        h = []
-        key, value = None, None
-        for line in response.msg.headers:
-            if line:
-                if line[0] in ' \t':
-                    value += line.strip()
-                else:
-                    if key and value:
-                        h.append((key, value))
-                    key, value = line.split(':', 1)
-                    key = key.strip()
-                    value = value.strip()
-        if key and value:
-            h.append((key, value))
-
-    return '%s %s' % (response.status, response.reason), h, response.read()
-
-
-def openURL(url, headers=None, method='GET', body=None,
-            host='127.0.0.1', port=8000, http_conn=http_client.HTTPConnection,
-            protocol='HTTP/1.1', raise_subcls=None):
-    """
-    Open the given HTTP resource and return status, headers, and body.
-
-    `raise_subcls` must be a tuple with the exceptions classes
-    or a single exception class that are not going to be considered
-    a socket.error regardless that they were are subclass of a
-    socket.error and therefore not considered for a connection retry.
-    """
-    headers = cleanHeaders(headers, method, body, host, port)
-
-    # Trying 10 times is simply in case of socket errors.
-    # Normal case--it should run once.
-    for trial in range(10):
-        try:
-            # Allow http_conn to be a class or an instance
-            if hasattr(http_conn, 'host'):
-                conn = http_conn
-            else:
-                conn = http_conn(interface(host), port)
-
-            conn._http_vsn_str = protocol
-            conn._http_vsn = int(''.join([x for x in protocol if x.isdigit()]))
-
-            if six.PY3 and isinstance(url, bytes):
-                url = url.decode()
-            conn.putrequest(method.upper(), url, skip_host=True,
-                            skip_accept_encoding=True)
-
-            for key, value in headers:
-                conn.putheader(key, value.encode('Latin-1'))
-            conn.endheaders()
-
-            if body is not None:
-                conn.send(body)
-
-            # Handle response
-            response = conn.getresponse()
-
-            s, h, b = shb(response)
-
-            if not hasattr(http_conn, 'host'):
-                # We made our own conn instance. Close it.
-                conn.close()
-
-            return s, h, b
-        except socket.error as e:
-            if raise_subcls is not None and isinstance(e, raise_subcls):
-                raise
-            else:
-                time.sleep(0.5)
-                if trial == 9:
-                    raise
-
-
-def strip_netloc(url):
-    """Return absolute-URI path from URL.
-
-    Strip the scheme and host from the URL, returning the
-    server-absolute portion.
-
-    Useful for wrapping an absolute-URI for which only the
-    path is expected (such as in calls to getPage).
-
-    >>> strip_netloc('https://google.com/foo/bar?bing#baz')
-    '/foo/bar?bing'
-
-    >>> strip_netloc('//google.com/foo/bar?bing#baz')
-    '/foo/bar?bing'
-
-    >>> strip_netloc('/foo/bar?bing#baz')
-    '/foo/bar?bing'
-    """
-    parsed = urllib_parse.urlparse(url)
-    scheme, netloc, path, params, query, fragment = parsed
-    stripped = '', '', path, params, query, ''
-    return urllib_parse.urlunparse(stripped)
-
-
-# Add any exceptions which your web framework handles
-# normally (that you don't want server_error to trap).
-ignored_exceptions = []
-
-# You'll want set this to True when you can't guarantee
-# that each response will immediately follow each request;
-# for example, when handling requests via multiple threads.
-ignore_all = False
-
-
-class ServerError(Exception):
-    """Exception for signalling server error."""
-
-    on = False
-
-
-def server_error(exc=None):
-    """Server debug hook.
-
-    Return True if exception handled, False if ignored.
-    You probably want to wrap this, so you can still handle an error using
-    your framework when it's ignored.
-    """
-    if exc is None:
-        exc = sys.exc_info()
-
-    if ignore_all or exc[0] in ignored_exceptions:
-        return False
-    else:
-        ServerError.on = True
-        print('')
-        print(''.join(traceback.format_exception(*exc)))
-        return True
diff --git a/libraries/cheroot/testing.py b/libraries/cheroot/testing.py
deleted file mode 100644
index f01d0aa1..00000000
--- a/libraries/cheroot/testing.py
+++ /dev/null
@@ -1,144 +0,0 @@
-"""Pytest fixtures and other helpers for doing testing by end-users."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-from contextlib import closing
-import errno
-import socket
-import threading
-import time
-
-import pytest
-from six.moves import http_client
-
-import cheroot.server
-from cheroot.test import webtest
-import cheroot.wsgi
-
-EPHEMERAL_PORT = 0
-NO_INTERFACE = None  # Using this or '' will cause an exception
-ANY_INTERFACE_IPV4 = '0.0.0.0'
-ANY_INTERFACE_IPV6 = '::'
-
-config = {
-    cheroot.wsgi.Server: {
-        'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT),
-        'wsgi_app': None,
-    },
-    cheroot.server.HTTPServer: {
-        'bind_addr': (NO_INTERFACE, EPHEMERAL_PORT),
-        'gateway': cheroot.server.Gateway,
-    },
-}
-
-
-def cheroot_server(server_factory):
-    """Set up and tear down a Cheroot server instance."""
-    conf = config[server_factory].copy()
-    bind_port = conf.pop('bind_addr')[-1]
-
-    for interface in ANY_INTERFACE_IPV6, ANY_INTERFACE_IPV4:
-        try:
-            actual_bind_addr = (interface, bind_port)
-            httpserver = server_factory(  # create it
-                bind_addr=actual_bind_addr,
-                **conf
-            )
-        except OSError:
-            pass
-        else:
-            break
-
-    threading.Thread(target=httpserver.safe_start).start()  # spawn it
-    while not httpserver.ready:  # wait until fully initialized and bound
-        time.sleep(0.1)
-
-    yield httpserver
-
-    httpserver.stop()  # destroy it
-
-
-@pytest.fixture(scope='module')
-def wsgi_server():
-    """Set up and tear down a Cheroot WSGI server instance."""
-    for srv in cheroot_server(cheroot.wsgi.Server):
-        yield srv
-
-
-@pytest.fixture(scope='module')
-def native_server():
-    """Set up and tear down a Cheroot HTTP server instance."""
-    for srv in cheroot_server(cheroot.server.HTTPServer):
-        yield srv
-
-
-class _TestClient:
-    def __init__(self, server):
-        self._interface, self._host, self._port = _get_conn_data(server)
-        self._http_connection = self.get_connection()
-        self.server_instance = server
-
-    def get_connection(self):
-        name = '{interface}:{port}'.format(
-            interface=self._interface,
-            port=self._port,
-        )
-        return http_client.HTTPConnection(name)
-
-    def request(
-        self, uri, method='GET', headers=None, http_conn=None,
-        protocol='HTTP/1.1',
-    ):
-        return webtest.openURL(
-            uri, method=method,
-            headers=headers,
-            host=self._host, port=self._port,
-            http_conn=http_conn or self._http_connection,
-            protocol=protocol,
-        )
-
-    def __getattr__(self, attr_name):
-        def _wrapper(uri, **kwargs):
-            http_method = attr_name.upper()
-            return self.request(uri, method=http_method, **kwargs)
-
-        return _wrapper
-
-
-def _probe_ipv6_sock(interface):
-    # Alternate way is to check IPs on interfaces using glibc, like:
-    # github.com/Gautier/minifail/blob/master/minifail/getifaddrs.py
-    try:
-        with closing(socket.socket(family=socket.AF_INET6)) as sock:
-            sock.bind((interface, 0))
-    except (OSError, socket.error) as sock_err:
-        # In Python 3 socket.error is an alias for OSError
-        # In Python 2 socket.error is a subclass of IOError
-        if sock_err.errno != errno.EADDRNOTAVAIL:
-            raise
-    else:
-        return True
-
-    return False
-
-
-def _get_conn_data(server):
-    if isinstance(server.bind_addr, tuple):
-        host, port = server.bind_addr
-    else:
-        host, port = server.bind_addr, 0
-
-    interface = webtest.interface(host)
-
-    if ':' in interface and not _probe_ipv6_sock(interface):
-        interface = '127.0.0.1'
-        if ':' in host:
-            host = interface
-
-    return interface, host, port
-
-
-def get_server_client(server):
-    """Create and return a test client for the given server."""
-    return _TestClient(server)
diff --git a/libraries/cheroot/workers/__init__.py b/libraries/cheroot/workers/__init__.py
deleted file mode 100644
index 098b8f25..00000000
--- a/libraries/cheroot/workers/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-"""HTTP workers pool."""
diff --git a/libraries/cheroot/workers/threadpool.py b/libraries/cheroot/workers/threadpool.py
deleted file mode 100644
index ff8fbcee..00000000
--- a/libraries/cheroot/workers/threadpool.py
+++ /dev/null
@@ -1,271 +0,0 @@
-"""A thread-based worker pool."""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-
-import threading
-import time
-import socket
-
-from six.moves import queue
-
-
-__all__ = ('WorkerThread', 'ThreadPool')
-
-
-class TrueyZero:
-    """Object which equals and does math like the integer 0 but evals True."""
-
-    def __add__(self, other):
-        return other
-
-    def __radd__(self, other):
-        return other
-
-
-trueyzero = TrueyZero()
-
-_SHUTDOWNREQUEST = None
-
-
-class WorkerThread(threading.Thread):
-    """Thread which continuously polls a Queue for Connection objects.
-
-    Due to the timing issues of polling a Queue, a WorkerThread does not
-    check its own 'ready' flag after it has started. To stop the thread,
-    it is necessary to stick a _SHUTDOWNREQUEST object onto the Queue
-    (one for each running WorkerThread).
-    """
-
-    conn = None
-    """The current connection pulled off the Queue, or None."""
-
-    server = None
-    """The HTTP Server which spawned this thread, and which owns the
-    Queue and is placing active connections into it."""
-
-    ready = False
-    """A simple flag for the calling server to know when this thread
-    has begun polling the Queue."""
-
-    def __init__(self, server):
-        """Initialize WorkerThread instance.
-
-        Args:
-            server (cheroot.server.HTTPServer): web server object
-                receiving this request
-        """
-        self.ready = False
-        self.server = server
-
-        self.requests_seen = 0
-        self.bytes_read = 0
-        self.bytes_written = 0
-        self.start_time = None
-        self.work_time = 0
-        self.stats = {
-            'Requests': lambda s: self.requests_seen + (
-                (self.start_time is None) and
-                trueyzero or
-                self.conn.requests_seen
-            ),
-            'Bytes Read': lambda s: self.bytes_read + (
-                (self.start_time is None) and
-                trueyzero or
-                self.conn.rfile.bytes_read
-            ),
-            'Bytes Written': lambda s: self.bytes_written + (
-                (self.start_time is None) and
-                trueyzero or
-                self.conn.wfile.bytes_written
-            ),
-            'Work Time': lambda s: self.work_time + (
-                (self.start_time is None) and
-                trueyzero or
-                time.time() - self.start_time
-            ),
-            'Read Throughput': lambda s: s['Bytes Read'](s) / (
-                s['Work Time'](s) or 1e-6),
-            'Write Throughput': lambda s: s['Bytes Written'](s) / (
-                s['Work Time'](s) or 1e-6),
-        }
-        threading.Thread.__init__(self)
-
-    def run(self):
-        """Process incoming HTTP connections.
-
-        Retrieves incoming connections from thread pool.
-        """
-        self.server.stats['Worker Threads'][self.getName()] = self.stats
-        try:
-            self.ready = True
-            while True:
-                conn = self.server.requests.get()
-                if conn is _SHUTDOWNREQUEST:
-                    return
-
-                self.conn = conn
-                if self.server.stats['Enabled']:
-                    self.start_time = time.time()
-                try:
-                    conn.communicate()
-                finally:
-                    conn.close()
-                    if self.server.stats['Enabled']:
-                        self.requests_seen += self.conn.requests_seen
-                        self.bytes_read += self.conn.rfile.bytes_read
-                        self.bytes_written += self.conn.wfile.bytes_written
-                        self.work_time += time.time() - self.start_time
-                        self.start_time = None
-                    self.conn = None
-        except (KeyboardInterrupt, SystemExit) as ex:
-            self.server.interrupt = ex
-
-
-class ThreadPool:
-    """A Request Queue for an HTTPServer which pools threads.
-
-    ThreadPool objects must provide min, get(), put(obj), start()
-    and stop(timeout) attributes.
-    """
-
-    def __init__(
-            self, server, min=10, max=-1, accepted_queue_size=-1,
-            accepted_queue_timeout=10):
-        """Initialize HTTP requests queue instance.
-
-        Args:
-            server (cheroot.server.HTTPServer): web server object
-                receiving this request
-            min (int): minimum number of worker threads
-            max (int): maximum number of worker threads
-            accepted_queue_size (int): maximum number of active
-                requests in queue
-            accepted_queue_timeout (int): timeout for putting request
-                into queue
-        """
-        self.server = server
-        self.min = min
-        self.max = max
-        self._threads = []
-        self._queue = queue.Queue(maxsize=accepted_queue_size)
-        self._queue_put_timeout = accepted_queue_timeout
-        self.get = self._queue.get
-
-    def start(self):
-        """Start the pool of threads."""
-        for i in range(self.min):
-            self._threads.append(WorkerThread(self.server))
-        for worker in self._threads:
-            worker.setName('CP Server ' + worker.getName())
-            worker.start()
-        for worker in self._threads:
-            while not worker.ready:
-                time.sleep(.1)
-
-    @property
-    def idle(self):  # noqa: D401; irrelevant for properties
-        """Number of worker threads which are idle. Read-only."""
-        return len([t for t in self._threads if t.conn is None])
-
-    def put(self, obj):
-        """Put request into queue.
-
-        Args:
-            obj (cheroot.server.HTTPConnection): HTTP connection
-                waiting to be processed
-        """
-        self._queue.put(obj, block=True, timeout=self._queue_put_timeout)
-        if obj is _SHUTDOWNREQUEST:
-            return
-
-    def grow(self, amount):
-        """Spawn new worker threads (not above self.max)."""
-        if self.max > 0:
-            budget = max(self.max - len(self._threads), 0)
-        else:
-            # self.max <= 0 indicates no maximum
-            budget = float('inf')
-
-        n_new = min(amount, budget)
-
-        workers = [self._spawn_worker() for i in range(n_new)]
-        while not all(worker.ready for worker in workers):
-            time.sleep(.1)
-        self._threads.extend(workers)
-
-    def _spawn_worker(self):
-        worker = WorkerThread(self.server)
-        worker.setName('CP Server ' + worker.getName())
-        worker.start()
-        return worker
-
-    def shrink(self, amount):
-        """Kill off worker threads (not below self.min)."""
-        # Grow/shrink the pool if necessary.
-        # Remove any dead threads from our list
-        for t in self._threads:
-            if not t.isAlive():
-                self._threads.remove(t)
-                amount -= 1
-
-        # calculate the number of threads above the minimum
-        n_extra = max(len(self._threads) - self.min, 0)
-
-        # don't remove more than amount
-        n_to_remove = min(amount, n_extra)
-
-        # put shutdown requests on the queue equal to the number of threads
-        # to remove. As each request is processed by a worker, that worker
-        # will terminate and be culled from the list.
-        for n in range(n_to_remove):
-            self._queue.put(_SHUTDOWNREQUEST)
-
-    def stop(self, timeout=5):
-        """Terminate all worker threads.
-
-        Args:
-            timeout (int): time to wait for threads to stop gracefully
-        """
-        # Must shut down threads here so the code that calls
-        # this method can know when all threads are stopped.
-        for worker in self._threads:
-            self._queue.put(_SHUTDOWNREQUEST)
-
-        # Don't join currentThread (when stop is called inside a request).
-        current = threading.currentThread()
-        if timeout is not None and timeout >= 0:
-            endtime = time.time() + timeout
-        while self._threads:
-            worker = self._threads.pop()
-            if worker is not current and worker.isAlive():
-                try:
-                    if timeout is None or timeout < 0:
-                        worker.join()
-                    else:
-                        remaining_time = endtime - time.time()
-                        if remaining_time > 0:
-                            worker.join(remaining_time)
-                        if worker.isAlive():
-                            # We exhausted the timeout.
-                            # Forcibly shut down the socket.
-                            c = worker.conn
-                            if c and not c.rfile.closed:
-                                try:
-                                    c.socket.shutdown(socket.SHUT_RD)
-                                except TypeError:
-                                    # pyOpenSSL sockets don't take an arg
-                                    c.socket.shutdown()
-                            worker.join()
-                except (AssertionError,
-                        # Ignore repeated Ctrl-C.
-                        # See
-                        # https://github.com/cherrypy/cherrypy/issues/691.
-                        KeyboardInterrupt):
-                    pass
-
-    @property
-    def qsize(self):
-        """Return the queue size."""
-        return self._queue.qsize()
diff --git a/libraries/cheroot/wsgi.py b/libraries/cheroot/wsgi.py
deleted file mode 100644
index a04c9438..00000000
--- a/libraries/cheroot/wsgi.py
+++ /dev/null
@@ -1,423 +0,0 @@
-"""This class holds Cheroot WSGI server implementation.
-
-Simplest example on how to use this server::
-
-    from cheroot import wsgi
-
-    def my_crazy_app(environ, start_response):
-        status = '200 OK'
-        response_headers = [('Content-type','text/plain')]
-        start_response(status, response_headers)
-        return [b'Hello world!']
-
-    addr = '0.0.0.0', 8070
-    server = wsgi.Server(addr, my_crazy_app)
-    server.start()
-
-The Cheroot WSGI server can serve as many WSGI applications
-as you want in one instance by using a PathInfoDispatcher::
-
-    path_map = {
-        '/': my_crazy_app,
-        '/blog': my_blog_app,
-    }
-    d = wsgi.PathInfoDispatcher(path_map)
-    server = wsgi.Server(addr, d)
-"""
-
-from __future__ import absolute_import, division, print_function
-__metaclass__ = type
-
-import sys
-
-import six
-from six.moves import filter
-
-from . import server
-from .workers import threadpool
-from ._compat import ntob, bton
-
-
-class Server(server.HTTPServer):
-    """A subclass of HTTPServer which calls a WSGI application."""
-
-    wsgi_version = (1, 0)
-    """The version of WSGI to produce."""
-
-    def __init__(
-        self, bind_addr, wsgi_app, numthreads=10, server_name=None,
-        max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5,
-        accepted_queue_size=-1, accepted_queue_timeout=10,
-        peercreds_enabled=False, peercreds_resolve_enabled=False,
-    ):
-        """Initialize WSGI Server instance.
-
-        Args:
-            bind_addr (tuple): network interface to listen to
-            wsgi_app (callable): WSGI application callable
-            numthreads (int): number of threads for WSGI thread pool
-            server_name (str): web server name to be advertised via
-                Server HTTP header
-            max (int): maximum number of worker threads
-            request_queue_size (int): the 'backlog' arg to
-                socket.listen(); max queued connections
-            timeout (int): the timeout in seconds for accepted connections
-            shutdown_timeout (int): the total time, in seconds, to
-                wait for worker threads to cleanly exit
-            accepted_queue_size (int): maximum number of active
-                requests in queue
-            accepted_queue_timeout (int): timeout for putting request
-                into queue
-        """
-        super(Server, self).__init__(
-            bind_addr,
-            gateway=wsgi_gateways[self.wsgi_version],
-            server_name=server_name,
-            peercreds_enabled=peercreds_enabled,
-            peercreds_resolve_enabled=peercreds_resolve_enabled,
-        )
-        self.wsgi_app = wsgi_app
-        self.request_queue_size = request_queue_size
-        self.timeout = timeout
-        self.shutdown_timeout = shutdown_timeout
-        self.requests = threadpool.ThreadPool(
-            self, min=numthreads or 1, max=max,
-            accepted_queue_size=accepted_queue_size,
-            accepted_queue_timeout=accepted_queue_timeout)
-
-    @property
-    def numthreads(self):
-        """Set minimum number of threads."""
-        return self.requests.min
-
-    @numthreads.setter
-    def numthreads(self, value):
-        self.requests.min = value
-
-
-class Gateway(server.Gateway):
-    """A base class to interface HTTPServer with WSGI."""
-
-    def __init__(self, req):
-        """Initialize WSGI Gateway instance with request.
-
-        Args:
-            req (HTTPRequest): current HTTP request
-        """
-        super(Gateway, self).__init__(req)
-        self.started_response = False
-        self.env = self.get_environ()
-        self.remaining_bytes_out = None
-
-    @classmethod
-    def gateway_map(cls):
-        """Create a mapping of gateways and their versions.
-
-        Returns:
-            dict[tuple[int,int],class]: map of gateway version and
-                corresponding class
-
-        """
-        return dict(
-            (gw.version, gw)
-            for gw in cls.__subclasses__()
-        )
-
-    def get_environ(self):
-        """Return a new environ dict targeting the given wsgi.version."""
-        raise NotImplementedError
-
-    def respond(self):
-        """Process the current request.
-
-        From :pep:`333`:
-
-            The start_response callable must not actually transmit
-            the response headers. Instead, it must store them for the
-            server or gateway to transmit only after the first
-            iteration of the application return value that yields
-            a NON-EMPTY string, or upon the application's first
-            invocation of the write() callable.
-        """
-        response = self.req.server.wsgi_app(self.env, self.start_response)
-        try:
-            for chunk in filter(None, response):
-                if not isinstance(chunk, six.binary_type):
-                    raise ValueError('WSGI Applications must yield bytes')
-                self.write(chunk)
-        finally:
-            # Send headers if not already sent
-            self.req.ensure_headers_sent()
-            if hasattr(response, 'close'):
-                response.close()
-
-    def start_response(self, status, headers, exc_info=None):
-        """WSGI callable to begin the HTTP response."""
-        # "The application may call start_response more than once,
-        # if and only if the exc_info argument is provided."
-        if self.started_response and not exc_info:
-            raise AssertionError('WSGI start_response called a second '
-                                 'time with no exc_info.')
-        self.started_response = True
-
-        # "if exc_info is provided, and the HTTP headers have already been
-        # sent, start_response must raise an error, and should raise the
-        # exc_info tuple."
-        if self.req.sent_headers:
-            try:
-                six.reraise(*exc_info)
-            finally:
-                exc_info = None
-
-        self.req.status = self._encode_status(status)
-
-        for k, v in headers:
-            if not isinstance(k, str):
-                raise TypeError(
-                    'WSGI response header key %r is not of type str.' % k)
-            if not isinstance(v, str):
-                raise TypeError(
-                    'WSGI response header value %r is not of type str.' % v)
-            if k.lower() == 'content-length':
-                self.remaining_bytes_out = int(v)
-            out_header = ntob(k), ntob(v)
-            self.req.outheaders.append(out_header)
-
-        return self.write
-
-    @staticmethod
-    def _encode_status(status):
-        """Cast status to bytes representation of current Python version.
-
-        According to :pep:`3333`, when using Python 3, the response status
-        and headers must be bytes masquerading as unicode; that is, they
-        must be of type "str" but are restricted to code points in the
-        "latin-1" set.
-        """
-        if six.PY2:
-            return status
-        if not isinstance(status, str):
-            raise TypeError('WSGI response status is not of type str.')
-        return status.encode('ISO-8859-1')
-
-    def write(self, chunk):
-        """WSGI callable to write unbuffered data to the client.
-
-        This method is also used internally by start_response (to write
-        data from the iterable returned by the WSGI application).
-        """
-        if not self.started_response:
-            raise AssertionError('WSGI write called before start_response.')
-
-        chunklen = len(chunk)
-        rbo = self.remaining_bytes_out
-        if rbo is not None and chunklen > rbo:
-            if not self.req.sent_headers:
-                # Whew. We can send a 500 to the client.
-                self.req.simple_response(
-                    '500 Internal Server Error',
-                    'The requested resource returned more bytes than the '
-                    'declared Content-Length.')
-            else:
-                # Dang. We have probably already sent data. Truncate the chunk
-                # to fit (so the client doesn't hang) and raise an error later.
-                chunk = chunk[:rbo]
-
-        self.req.ensure_headers_sent()
-
-        self.req.write(chunk)
-
-        if rbo is not None:
-            rbo -= chunklen
-            if rbo < 0:
-                raise ValueError(
-                    'Response body exceeds the declared Content-Length.')
-
-
-class Gateway_10(Gateway):
-    """A Gateway class to interface HTTPServer with WSGI 1.0.x."""
-
-    version = 1, 0
-
-    def get_environ(self):
-        """Return a new environ dict targeting the given wsgi.version."""
-        req = self.req
-        req_conn = req.conn
-        env = {
-            # set a non-standard environ entry so the WSGI app can know what
-            # the *real* server protocol is (and what features to support).
-            # See http://www.faqs.org/rfcs/rfc2145.html.
-            'ACTUAL_SERVER_PROTOCOL': req.server.protocol,
-            'PATH_INFO': bton(req.path),
-            'QUERY_STRING': bton(req.qs),
-            'REMOTE_ADDR': req_conn.remote_addr or '',
-            'REMOTE_PORT': str(req_conn.remote_port or ''),
-            'REQUEST_METHOD': bton(req.method),
-            'REQUEST_URI': bton(req.uri),
-            'SCRIPT_NAME': '',
-            'SERVER_NAME': req.server.server_name,
-            # Bah. "SERVER_PROTOCOL" is actually the REQUEST protocol.
-            'SERVER_PROTOCOL': bton(req.request_protocol),
-            'SERVER_SOFTWARE': req.server.software,
-            'wsgi.errors': sys.stderr,
-            'wsgi.input': req.rfile,
-            'wsgi.input_terminated': bool(req.chunked_read),
-            'wsgi.multiprocess': False,
-            'wsgi.multithread': True,
-            'wsgi.run_once': False,
-            'wsgi.url_scheme': bton(req.scheme),
-            'wsgi.version': self.version,
-        }
-
-        if isinstance(req.server.bind_addr, six.string_types):
-            # AF_UNIX. This isn't really allowed by WSGI, which doesn't
-            # address unix domain sockets. But it's better than nothing.
-            env['SERVER_PORT'] = ''
-            try:
-                env['X_REMOTE_PID'] = str(req_conn.peer_pid)
-                env['X_REMOTE_UID'] = str(req_conn.peer_uid)
-                env['X_REMOTE_GID'] = str(req_conn.peer_gid)
-
-                env['X_REMOTE_USER'] = str(req_conn.peer_user)
-                env['X_REMOTE_GROUP'] = str(req_conn.peer_group)
-
-                env['REMOTE_USER'] = env['X_REMOTE_USER']
-            except RuntimeError:
-                """Unable to retrieve peer creds data.
-
-                Unsupported by current kernel or socket error happened, or
-                unsupported socket type, or disabled.
-                """
-        else:
-            env['SERVER_PORT'] = str(req.server.bind_addr[1])
-
-        # Request headers
-        env.update(
-            ('HTTP_' + bton(k).upper().replace('-', '_'), bton(v))
-            for k, v in req.inheaders.items()
-        )
-
-        # CONTENT_TYPE/CONTENT_LENGTH
-        ct = env.pop('HTTP_CONTENT_TYPE', None)
-        if ct is not None:
-            env['CONTENT_TYPE'] = ct
-        cl = env.pop('HTTP_CONTENT_LENGTH', None)
-        if cl is not None:
-            env['CONTENT_LENGTH'] = cl
-
-        if req.conn.ssl_env:
-            env.update(req.conn.ssl_env)
-
-        return env
-
-
-class Gateway_u0(Gateway_10):
-    """A Gateway class to interface HTTPServer with WSGI u.0.
-
-    WSGI u.0 is an experimental protocol, which uses unicode for keys
-    and values in both Python 2 and Python 3.
-    """
-
-    version = 'u', 0
-
-    def get_environ(self):
-        """Return a new environ dict targeting the given wsgi.version."""
-        req = self.req
-        env_10 = super(Gateway_u0, self).get_environ()
-        env = dict(map(self._decode_key, env_10.items()))
-
-        # Request-URI
-        enc = env.setdefault(six.u('wsgi.url_encoding'), six.u('utf-8'))
-        try:
-            env['PATH_INFO'] = req.path.decode(enc)
-            env['QUERY_STRING'] = req.qs.decode(enc)
-        except UnicodeDecodeError:
-            # Fall back to latin 1 so apps can transcode if needed.
-            env['wsgi.url_encoding'] = 'ISO-8859-1'
-            env['PATH_INFO'] = env_10['PATH_INFO']
-            env['QUERY_STRING'] = env_10['QUERY_STRING']
-
-        env.update(map(self._decode_value, env.items()))
-
-        return env
-
-    @staticmethod
-    def _decode_key(item):
-        k, v = item
-        if six.PY2:
-            k = k.decode('ISO-8859-1')
-        return k, v
-
-    @staticmethod
-    def _decode_value(item):
-        k, v = item
-        skip_keys = 'REQUEST_URI', 'wsgi.input'
-        if six.PY3 or not isinstance(v, bytes) or k in skip_keys:
-            return k, v
-        return k, v.decode('ISO-8859-1')
-
-
-wsgi_gateways = Gateway.gateway_map()
-
-
-class PathInfoDispatcher:
-    """A WSGI dispatcher for dispatch based on the PATH_INFO."""
-
-    def __init__(self, apps):
-        """Initialize path info WSGI app dispatcher.
-
-        Args:
-            apps (dict[str,object]|list[tuple[str,object]]): URI prefix
-                and WSGI app pairs
-        """
-        try:
-            apps = list(apps.items())
-        except AttributeError:
-            pass
-
-        # Sort the apps by len(path), descending
-        def by_path_len(app):
-            return len(app[0])
-        apps.sort(key=by_path_len, reverse=True)
-
-        # The path_prefix strings must start, but not end, with a slash.
-        # Use "" instead of "/".
-        self.apps = [(p.rstrip('/'), a) for p, a in apps]
-
-    def __call__(self, environ, start_response):
-        """Process incoming WSGI request.
-
-        Ref: :pep:`3333`
-
-        Args:
-            environ (Mapping): a dict containing WSGI environment variables
-            start_response (callable): function, which sets response
-                status and headers
-
-        Returns:
-            list[bytes]: iterable containing bytes to be returned in
-                HTTP response body
-
-        """
-        path = environ['PATH_INFO'] or '/'
-        for p, app in self.apps:
-            # The apps list should be sorted by length, descending.
-            if path.startswith(p + '/') or path == p:
-                environ = environ.copy()
-                environ['SCRIPT_NAME'] = environ['SCRIPT_NAME'] + p
-                environ['PATH_INFO'] = path[len(p):]
-                return app(environ, start_response)
-
-        start_response('404 Not Found', [('Content-Type', 'text/plain'),
-                                         ('Content-Length', '0')])
-        return ['']
-
-
-# compatibility aliases
-globals().update(
-    WSGIServer=Server,
-    WSGIGateway=Gateway,
-    WSGIGateway_u0=Gateway_u0,
-    WSGIGateway_10=Gateway_10,
-    WSGIPathInfoDispatcher=PathInfoDispatcher,
-)
diff --git a/libraries/cherrypy/__init__.py b/libraries/cherrypy/__init__.py
deleted file mode 100644
index c5925980..00000000
--- a/libraries/cherrypy/__init__.py
+++ /dev/null
@@ -1,362 +0,0 @@
-"""CherryPy is a pythonic, object-oriented HTTP framework.
-
-CherryPy consists of not one, but four separate API layers.
-
-The APPLICATION LAYER is the simplest. CherryPy applications are written as
-a tree of classes and methods, where each branch in the tree corresponds to
-a branch in the URL path. Each method is a 'page handler', which receives
-GET and POST params as keyword arguments, and returns or yields the (HTML)
-body of the response. The special method name 'index' is used for paths
-that end in a slash, and the special method name 'default' is used to
-handle multiple paths via a single handler. This layer also includes:
-
- * the 'exposed' attribute (and cherrypy.expose)
- * cherrypy.quickstart()
- * _cp_config attributes
- * cherrypy.tools (including cherrypy.session)
- * cherrypy.url()
-
-The ENVIRONMENT LAYER is used by developers at all levels. It provides
-information about the current request and response, plus the application
-and server environment, via a (default) set of top-level objects:
-
- * cherrypy.request
- * cherrypy.response
- * cherrypy.engine
- * cherrypy.server
- * cherrypy.tree
- * cherrypy.config
- * cherrypy.thread_data
- * cherrypy.log
- * cherrypy.HTTPError, NotFound, and HTTPRedirect
- * cherrypy.lib
-
-The EXTENSION LAYER allows advanced users to construct and share their own
-plugins. It consists of:
-
- * Hook API
- * Tool API
- * Toolbox API
- * Dispatch API
- * Config Namespace API
-
-Finally, there is the CORE LAYER, which uses the core API's to construct
-the default components which are available at higher layers. You can think
-of the default components as the 'reference implementation' for CherryPy.
-Megaframeworks (and advanced users) may replace the default components
-with customized or extended components. The core API's are:
-
- * Application API
- * Engine API
- * Request API
- * Server API
- * WSGI API
-
-These API's are described in the `CherryPy specification
-<https://github.com/cherrypy/cherrypy/wiki/CherryPySpec>`_.
-"""
-
-from threading import local as _local
-
-from ._cperror import (
-    HTTPError, HTTPRedirect, InternalRedirect,
-    NotFound, CherryPyException,
-)
-
-from . import _cpdispatch as dispatch
-
-from ._cptools import default_toolbox as tools, Tool
-from ._helper import expose, popargs, url
-
-from . import _cprequest, _cpserver, _cptree, _cplogging, _cpconfig
-
-import cherrypy.lib.httputil as _httputil
-
-from ._cptree import Application
-from . import _cpwsgi as wsgi
-
-from . import process
-try:
-    from .process import win32
-    engine = win32.Win32Bus()
-    engine.console_control_handler = win32.ConsoleCtrlHandler(engine)
-    del win32
-except ImportError:
-    engine = process.bus
-
-from . import _cpchecker
-
-__all__ = (
-    'HTTPError', 'HTTPRedirect', 'InternalRedirect',
-    'NotFound', 'CherryPyException',
-    'dispatch', 'tools', 'Tool', 'Application',
-    'wsgi', 'process', 'tree', 'engine',
-    'quickstart', 'serving', 'request', 'response', 'thread_data',
-    'log', 'expose', 'popargs', 'url', 'config',
-)
-
-
-__import__('cherrypy._cptools')
-__import__('cherrypy._cprequest')
-
-
-tree = _cptree.Tree()
-
-
-__version__ = '17.4.0'
-
-
-engine.listeners['before_request'] = set()
-engine.listeners['after_request'] = set()
-
-
-engine.autoreload = process.plugins.Autoreloader(engine)
-engine.autoreload.subscribe()
-
-engine.thread_manager = process.plugins.ThreadManager(engine)
-engine.thread_manager.subscribe()
-
-engine.signal_handler = process.plugins.SignalHandler(engine)
-
-
-class _HandleSignalsPlugin(object):
-    """Handle signals from other processes.
-
-    Based on the configured platform handlers above.
-    """
-
-    def __init__(self, bus):
-        self.bus = bus
-
-    def subscribe(self):
-        """Add the handlers based on the platform."""
-        if hasattr(self.bus, 'signal_handler'):
-            self.bus.signal_handler.subscribe()
-        if hasattr(self.bus, 'console_control_handler'):
-            self.bus.console_control_handler.subscribe()
-
-
-engine.signals = _HandleSignalsPlugin(engine)
-
-
-server = _cpserver.Server()
-server.subscribe()
-
-
-def quickstart(root=None, script_name='', config=None):
-    """Mount the given root, start the builtin server (and engine), then block.
-
-    root: an instance of a "controller class" (a collection of page handler
-        methods) which represents the root of the application.
-    script_name: a string containing the "mount point" of the application.
-        This should start with a slash, and be the path portion of the URL
-        at which to mount the given root. For example, if root.index() will
-        handle requests to "http://www.example.com:8080/dept/app1/", then
-        the script_name argument would be "/dept/app1".
-
-        It MUST NOT end in a slash. If the script_name refers to the root
-        of the URI, it MUST be an empty string (not "/").
-    config: a file or dict containing application config. If this contains
-        a [global] section, those entries will be used in the global
-        (site-wide) config.
-    """
-    if config:
-        _global_conf_alias.update(config)
-
-    tree.mount(root, script_name, config)
-
-    engine.signals.subscribe()
-    engine.start()
-    engine.block()
-
-
-class _Serving(_local):
-    """An interface for registering request and response objects.
-
-    Rather than have a separate "thread local" object for the request and
-    the response, this class works as a single threadlocal container for
-    both objects (and any others which developers wish to define). In this
-    way, we can easily dump those objects when we stop/start a new HTTP
-    conversation, yet still refer to them as module-level globals in a
-    thread-safe way.
-    """
-
-    request = _cprequest.Request(_httputil.Host('127.0.0.1', 80),
-                                 _httputil.Host('127.0.0.1', 1111))
-    """
-    The request object for the current thread. In the main thread,
-    and any threads which are not receiving HTTP requests, this is None."""
-
-    response = _cprequest.Response()
-    """
-    The response object for the current thread. In the main thread,
-    and any threads which are not receiving HTTP requests, this is None."""
-
-    def load(self, request, response):
-        self.request = request
-        self.response = response
-
-    def clear(self):
-        """Remove all attributes of self."""
-        self.__dict__.clear()
-
-
-serving = _Serving()
-
-
-class _ThreadLocalProxy(object):
-
-    __slots__ = ['__attrname__', '__dict__']
-
-    def __init__(self, attrname):
-        self.__attrname__ = attrname
-
-    def __getattr__(self, name):
-        child = getattr(serving, self.__attrname__)
-        return getattr(child, name)
-
-    def __setattr__(self, name, value):
-        if name in ('__attrname__', ):
-            object.__setattr__(self, name, value)
-        else:
-            child = getattr(serving, self.__attrname__)
-            setattr(child, name, value)
-
-    def __delattr__(self, name):
-        child = getattr(serving, self.__attrname__)
-        delattr(child, name)
-
-    @property
-    def __dict__(self):
-        child = getattr(serving, self.__attrname__)
-        d = child.__class__.__dict__.copy()
-        d.update(child.__dict__)
-        return d
-
-    def __getitem__(self, key):
-        child = getattr(serving, self.__attrname__)
-        return child[key]
-
-    def __setitem__(self, key, value):
-        child = getattr(serving, self.__attrname__)
-        child[key] = value
-
-    def __delitem__(self, key):
-        child = getattr(serving, self.__attrname__)
-        del child[key]
-
-    def __contains__(self, key):
-        child = getattr(serving, self.__attrname__)
-        return key in child
-
-    def __len__(self):
-        child = getattr(serving, self.__attrname__)
-        return len(child)
-
-    def __nonzero__(self):
-        child = getattr(serving, self.__attrname__)
-        return bool(child)
-    # Python 3
-    __bool__ = __nonzero__
-
-
-# Create request and response object (the same objects will be used
-#   throughout the entire life of the webserver, but will redirect
-#   to the "serving" object)
-request = _ThreadLocalProxy('request')
-response = _ThreadLocalProxy('response')
-
-# Create thread_data object as a thread-specific all-purpose storage
-
-
-class _ThreadData(_local):
-    """A container for thread-specific data."""
-
-
-thread_data = _ThreadData()
-
-
-# Monkeypatch pydoc to allow help() to go through the threadlocal proxy.
-# Jan 2007: no Googleable examples of anyone else replacing pydoc.resolve.
-# The only other way would be to change what is returned from type(request)
-# and that's not possible in pure Python (you'd have to fake ob_type).
-def _cherrypy_pydoc_resolve(thing, forceload=0):
-    """Given an object or a path to an object, get the object and its name."""
-    if isinstance(thing, _ThreadLocalProxy):
-        thing = getattr(serving, thing.__attrname__)
-    return _pydoc._builtin_resolve(thing, forceload)
-
-
-try:
-    import pydoc as _pydoc
-    _pydoc._builtin_resolve = _pydoc.resolve
-    _pydoc.resolve = _cherrypy_pydoc_resolve
-except ImportError:
-    pass
-
-
-class _GlobalLogManager(_cplogging.LogManager):
-    """A site-wide LogManager; routes to app.log or global log as appropriate.
-
-    This :class:`LogManager<cherrypy._cplogging.LogManager>` implements
-    cherrypy.log() and cherrypy.log.access(). If either
-    function is called during a request, the message will be sent to the
-    logger for the current Application. If they are called outside of a
-    request, the message will be sent to the site-wide logger.
-    """
-
-    def __call__(self, *args, **kwargs):
-        """Log the given message to the app.log or global log.
-
-        Log the given message to the app.log or global
-        log as appropriate.
-        """
-        # Do NOT use try/except here. See
-        # https://github.com/cherrypy/cherrypy/issues/945
-        if hasattr(request, 'app') and hasattr(request.app, 'log'):
-            log = request.app.log
-        else:
-            log = self
-        return log.error(*args, **kwargs)
-
-    def access(self):
-        """Log an access message to the app.log or global log.
-
-        Log the given message to the app.log or global
-        log as appropriate.
-        """
-        try:
-            return request.app.log.access()
-        except AttributeError:
-            return _cplogging.LogManager.access(self)
-
-
-log = _GlobalLogManager()
-# Set a default screen handler on the global log.
-log.screen = True
-log.error_file = ''
-# Using an access file makes CP about 10% slower. Leave off by default.
-log.access_file = ''
-
-
-@engine.subscribe('log')
-def _buslog(msg, level):
-    log.error(msg, 'ENGINE', severity=level)
-
-
-# Use _global_conf_alias so quickstart can use 'config' as an arg
-# without shadowing cherrypy.config.
-config = _global_conf_alias = _cpconfig.Config()
-config.defaults = {
-    'tools.log_tracebacks.on': True,
-    'tools.log_headers.on': True,
-    'tools.trailing_slash.on': True,
-    'tools.encode.on': True
-}
-config.namespaces['log'] = lambda k, v: setattr(log, k, v)
-config.namespaces['checker'] = lambda k, v: setattr(checker, k, v)
-# Must reset to get our defaults applied.
-config.reset()
-
-checker = _cpchecker.Checker()
-engine.subscribe('start', checker)
diff --git a/libraries/cherrypy/__main__.py b/libraries/cherrypy/__main__.py
deleted file mode 100644
index 6674f7cb..00000000
--- a/libraries/cherrypy/__main__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-"""CherryPy'd cherryd daemon runner."""
-from cherrypy.daemon import run
-
-
-__name__ == '__main__' and run()
diff --git a/libraries/cherrypy/_cpchecker.py b/libraries/cherrypy/_cpchecker.py
deleted file mode 100644
index 39b7c972..00000000
--- a/libraries/cherrypy/_cpchecker.py
+++ /dev/null
@@ -1,325 +0,0 @@
-"""Checker for CherryPy sites and mounted apps."""
-import os
-import warnings
-
-import six
-from six.moves import builtins
-
-import cherrypy
-
-
-class Checker(object):
-    """A checker for CherryPy sites and their mounted applications.
-
-    When this object is called at engine startup, it executes each
-    of its own methods whose names start with ``check_``. If you wish
-    to disable selected checks, simply add a line in your global
-    config which sets the appropriate method to False::
-
-        [global]
-        checker.check_skipped_app_config = False
-
-    You may also dynamically add or replace ``check_*`` methods in this way.
-    """
-
-    on = True
-    """If True (the default), run all checks; if False, turn off all checks."""
-
-    def __init__(self):
-        """Initialize Checker instance."""
-        self._populate_known_types()
-
-    def __call__(self):
-        """Run all check_* methods."""
-        if self.on:
-            oldformatwarning = warnings.formatwarning
-            warnings.formatwarning = self.formatwarning
-            try:
-                for name in dir(self):
-                    if name.startswith('check_'):
-                        method = getattr(self, name)
-                        if method and hasattr(method, '__call__'):
-                            method()
-            finally:
-                warnings.formatwarning = oldformatwarning
-
-    def formatwarning(self, message, category, filename, lineno, line=None):
-        """Format a warning."""
-        return 'CherryPy Checker:\n%s\n\n' % message
-
-    # This value should be set inside _cpconfig.
-    global_config_contained_paths = False
-
-    def check_app_config_entries_dont_start_with_script_name(self):
-        """Check for App config with sections that repeat script_name."""
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            if not app.config:
-                continue
-            if sn == '':
-                continue
-            sn_atoms = sn.strip('/').split('/')
-            for key in app.config.keys():
-                key_atoms = key.strip('/').split('/')
-                if key_atoms[:len(sn_atoms)] == sn_atoms:
-                    warnings.warn(
-                        'The application mounted at %r has config '
-                        'entries that start with its script name: %r' % (sn,
-                                                                         key))
-
-    def check_site_config_entries_in_app_config(self):
-        """Check for mounted Applications that have site-scoped config."""
-        for sn, app in six.iteritems(cherrypy.tree.apps):
-            if not isinstance(app, cherrypy.Application):
-                continue
-
-            msg = []
-            for section, entries in six.iteritems(app.config):
-                if section.startswith('/'):
-                    for key, value in six.iteritems(entries):
-                        for n in ('engine.', 'server.', 'tree.', 'checker.'):
-                            if key.startswith(n):
-                                msg.append('[%s] %s = %s' %
-                                           (section, key, value))
-            if msg:
-                msg.insert(0,
-                           'The application mounted at %r contains the '
-                           'following config entries, which are only allowed '
-                           'in site-wide config. Move them to a [global] '
-                           'section and pass them to cherrypy.config.update() '
-                           'instead of tree.mount().' % sn)
-                warnings.warn(os.linesep.join(msg))
-
-    def check_skipped_app_config(self):
-        """Check for mounted Applications that have no config."""
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            if not app.config:
-                msg = 'The Application mounted at %r has an empty config.' % sn
-                if self.global_config_contained_paths:
-                    msg += (' It looks like the config you passed to '
-                            'cherrypy.config.update() contains application-'
-                            'specific sections. You must explicitly pass '
-                            'application config via '
-                            'cherrypy.tree.mount(..., config=app_config)')
-                warnings.warn(msg)
-                return
-
-    def check_app_config_brackets(self):
-        """Check for App config with extraneous brackets in section names."""
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            if not app.config:
-                continue
-            for key in app.config.keys():
-                if key.startswith('[') or key.endswith(']'):
-                    warnings.warn(
-                        'The application mounted at %r has config '
-                        'section names with extraneous brackets: %r. '
-                        'Config *files* need brackets; config *dicts* '
-                        '(e.g. passed to tree.mount) do not.' % (sn, key))
-
-    def check_static_paths(self):
-        """Check Application config for incorrect static paths."""
-        # Use the dummy Request object in the main thread.
-        request = cherrypy.request
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            request.app = app
-            for section in app.config:
-                # get_resource will populate request.config
-                request.get_resource(section + '/dummy.html')
-                conf = request.config.get
-
-                if conf('tools.staticdir.on', False):
-                    msg = ''
-                    root = conf('tools.staticdir.root')
-                    dir = conf('tools.staticdir.dir')
-                    if dir is None:
-                        msg = 'tools.staticdir.dir is not set.'
-                    else:
-                        fulldir = ''
-                        if os.path.isabs(dir):
-                            fulldir = dir
-                            if root:
-                                msg = ('dir is an absolute path, even '
-                                       'though a root is provided.')
-                                testdir = os.path.join(root, dir[1:])
-                                if os.path.exists(testdir):
-                                    msg += (
-                                        '\nIf you meant to serve the '
-                                        'filesystem folder at %r, remove the '
-                                        'leading slash from dir.' % (testdir,))
-                        else:
-                            if not root:
-                                msg = (
-                                    'dir is a relative path and '
-                                    'no root provided.')
-                            else:
-                                fulldir = os.path.join(root, dir)
-                                if not os.path.isabs(fulldir):
-                                    msg = ('%r is not an absolute path.' % (
-                                        fulldir,))
-
-                        if fulldir and not os.path.exists(fulldir):
-                            if msg:
-                                msg += '\n'
-                            msg += ('%r (root + dir) is not an existing '
-                                    'filesystem path.' % fulldir)
-
-                    if msg:
-                        warnings.warn('%s\nsection: [%s]\nroot: %r\ndir: %r'
-                                      % (msg, section, root, dir))
-
-    # -------------------------- Compatibility -------------------------- #
-    obsolete = {
-        'server.default_content_type': 'tools.response_headers.headers',
-        'log_access_file': 'log.access_file',
-        'log_config_options': None,
-        'log_file': 'log.error_file',
-        'log_file_not_found': None,
-        'log_request_headers': 'tools.log_headers.on',
-        'log_to_screen': 'log.screen',
-        'show_tracebacks': 'request.show_tracebacks',
-        'throw_errors': 'request.throw_errors',
-        'profiler.on': ('cherrypy.tree.mount(profiler.make_app('
-                        'cherrypy.Application(Root())))'),
-    }
-
-    deprecated = {}
-
-    def _compat(self, config):
-        """Process config and warn on each obsolete or deprecated entry."""
-        for section, conf in config.items():
-            if isinstance(conf, dict):
-                for k in conf:
-                    if k in self.obsolete:
-                        warnings.warn('%r is obsolete. Use %r instead.\n'
-                                      'section: [%s]' %
-                                      (k, self.obsolete[k], section))
-                    elif k in self.deprecated:
-                        warnings.warn('%r is deprecated. Use %r instead.\n'
-                                      'section: [%s]' %
-                                      (k, self.deprecated[k], section))
-            else:
-                if section in self.obsolete:
-                    warnings.warn('%r is obsolete. Use %r instead.'
-                                  % (section, self.obsolete[section]))
-                elif section in self.deprecated:
-                    warnings.warn('%r is deprecated. Use %r instead.'
-                                  % (section, self.deprecated[section]))
-
-    def check_compatibility(self):
-        """Process config and warn on each obsolete or deprecated entry."""
-        self._compat(cherrypy.config)
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            self._compat(app.config)
-
-    # ------------------------ Known Namespaces ------------------------ #
-    extra_config_namespaces = []
-
-    def _known_ns(self, app):
-        ns = ['wsgi']
-        ns.extend(app.toolboxes)
-        ns.extend(app.namespaces)
-        ns.extend(app.request_class.namespaces)
-        ns.extend(cherrypy.config.namespaces)
-        ns += self.extra_config_namespaces
-
-        for section, conf in app.config.items():
-            is_path_section = section.startswith('/')
-            if is_path_section and isinstance(conf, dict):
-                for k in conf:
-                    atoms = k.split('.')
-                    if len(atoms) > 1:
-                        if atoms[0] not in ns:
-                            # Spit out a special warning if a known
-                            # namespace is preceded by "cherrypy."
-                            if atoms[0] == 'cherrypy' and atoms[1] in ns:
-                                msg = (
-                                    'The config entry %r is invalid; '
-                                    'try %r instead.\nsection: [%s]'
-                                    % (k, '.'.join(atoms[1:]), section))
-                            else:
-                                msg = (
-                                    'The config entry %r is invalid, '
-                                    'because the %r config namespace '
-                                    'is unknown.\n'
-                                    'section: [%s]' % (k, atoms[0], section))
-                            warnings.warn(msg)
-                        elif atoms[0] == 'tools':
-                            if atoms[1] not in dir(cherrypy.tools):
-                                msg = (
-                                    'The config entry %r may be invalid, '
-                                    'because the %r tool was not found.\n'
-                                    'section: [%s]' % (k, atoms[1], section))
-                                warnings.warn(msg)
-
-    def check_config_namespaces(self):
-        """Process config and warn on each unknown config namespace."""
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            self._known_ns(app)
-
-    # -------------------------- Config Types -------------------------- #
-    known_config_types = {}
-
-    def _populate_known_types(self):
-        b = [x for x in vars(builtins).values()
-             if type(x) is type(str)]
-
-        def traverse(obj, namespace):
-            for name in dir(obj):
-                # Hack for 3.2's warning about body_params
-                if name == 'body_params':
-                    continue
-                vtype = type(getattr(obj, name, None))
-                if vtype in b:
-                    self.known_config_types[namespace + '.' + name] = vtype
-
-        traverse(cherrypy.request, 'request')
-        traverse(cherrypy.response, 'response')
-        traverse(cherrypy.server, 'server')
-        traverse(cherrypy.engine, 'engine')
-        traverse(cherrypy.log, 'log')
-
-    def _known_types(self, config):
-        msg = ('The config entry %r in section %r is of type %r, '
-               'which does not match the expected type %r.')
-
-        for section, conf in config.items():
-            if not isinstance(conf, dict):
-                conf = {section: conf}
-            for k, v in conf.items():
-                if v is not None:
-                    expected_type = self.known_config_types.get(k, None)
-                    vtype = type(v)
-                    if expected_type and vtype != expected_type:
-                        warnings.warn(msg % (k, section, vtype.__name__,
-                                             expected_type.__name__))
-
-    def check_config_types(self):
-        """Assert that config values are of the same type as default values."""
-        self._known_types(cherrypy.config)
-        for sn, app in cherrypy.tree.apps.items():
-            if not isinstance(app, cherrypy.Application):
-                continue
-            self._known_types(app.config)
-
-    # -------------------- Specific config warnings -------------------- #
-    def check_localhost(self):
-        """Warn if any socket_host is 'localhost'. See #711."""
-        for k, v in cherrypy.config.items():
-            if k == 'server.socket_host' and v == 'localhost':
-                warnings.warn("The use of 'localhost' as a socket host can "
-                              'cause problems on newer systems, since '
-                              "'localhost' can map to either an IPv4 or an "
-                              "IPv6 address. You should use '127.0.0.1' "
-                              "or '[::1]' instead.")
diff --git a/libraries/cherrypy/_cpcompat.py b/libraries/cherrypy/_cpcompat.py
deleted file mode 100644
index f454505c..00000000
--- a/libraries/cherrypy/_cpcompat.py
+++ /dev/null
@@ -1,162 +0,0 @@
-"""Compatibility code for using CherryPy with various versions of Python.
-
-To retain compatibility with older Python versions, this module provides a
-useful abstraction over the differences between Python versions, sometimes by
-preferring a newer idiom, sometimes an older one, and sometimes a custom one.
-
-In particular, Python 2 uses str and '' for byte strings, while Python 3
-uses str and '' for unicode strings. We will call each of these the 'native
-string' type for each version. Because of this major difference, this module
-provides
-two functions: 'ntob', which translates native strings (of type 'str') into
-byte strings regardless of Python version, and 'ntou', which translates native
-strings to unicode strings.
-
-Try not to use the compatibility functions 'ntob', 'ntou', 'tonative'.
-They were created with Python 2.3-2.5 compatibility in mind.
-Instead, use unicode literals (from __future__) and bytes literals
-and their .encode/.decode methods as needed.
-"""
-
-import re
-import sys
-import threading
-
-import six
-from six.moves import urllib
-
-
-if six.PY3:
-    def ntob(n, encoding='ISO-8859-1'):
-        """Return the given native string as a byte string in the given
-        encoding.
-        """
-        assert_native(n)
-        # In Python 3, the native string type is unicode
-        return n.encode(encoding)
-
-    def ntou(n, encoding='ISO-8859-1'):
-        """Return the given native string as a unicode string with the given
-        encoding.
-        """
-        assert_native(n)
-        # In Python 3, the native string type is unicode
-        return n
-
-    def tonative(n, encoding='ISO-8859-1'):
-        """Return the given string as a native string in the given encoding."""
-        # In Python 3, the native string type is unicode
-        if isinstance(n, bytes):
-            return n.decode(encoding)
-        return n
-else:
-    # Python 2
-    def ntob(n, encoding='ISO-8859-1'):
-        """Return the given native string as a byte string in the given
-        encoding.
-        """
-        assert_native(n)
-        # In Python 2, the native string type is bytes. Assume it's already
-        # in the given encoding, which for ISO-8859-1 is almost always what
-        # was intended.
-        return n
-
-    def ntou(n, encoding='ISO-8859-1'):
-        """Return the given native string as a unicode string with the given
-        encoding.
-        """
-        assert_native(n)
-        # In Python 2, the native string type is bytes.
-        # First, check for the special encoding 'escape'. The test suite uses
-        # this to signal that it wants to pass a string with embedded \uXXXX
-        # escapes, but without having to prefix it with u'' for Python 2,
-        # but no prefix for Python 3.
-        if encoding == 'escape':
-            return six.text_type(  # unicode for Python 2
-                re.sub(r'\\u([0-9a-zA-Z]{4})',
-                       lambda m: six.unichr(int(m.group(1), 16)),
-                       n.decode('ISO-8859-1')))
-        # Assume it's already in the given encoding, which for ISO-8859-1
-        # is almost always what was intended.
-        return n.decode(encoding)
-
-    def tonative(n, encoding='ISO-8859-1'):
-        """Return the given string as a native string in the given encoding."""
-        # In Python 2, the native string type is bytes.
-        if isinstance(n, six.text_type):  # unicode for Python 2
-            return n.encode(encoding)
-        return n
-
-
-def assert_native(n):
-    if not isinstance(n, str):
-        raise TypeError('n must be a native str (got %s)' % type(n).__name__)
-
-
-# Some platforms don't expose HTTPSConnection, so handle it separately
-HTTPSConnection = getattr(six.moves.http_client, 'HTTPSConnection', None)
-
-
-def _unquote_plus_compat(string, encoding='utf-8', errors='replace'):
-    return urllib.parse.unquote_plus(string).decode(encoding, errors)
-
-
-def _unquote_compat(string, encoding='utf-8', errors='replace'):
-    return urllib.parse.unquote(string).decode(encoding, errors)
-
-
-def _quote_compat(string, encoding='utf-8', errors='replace'):
-    return urllib.parse.quote(string.encode(encoding, errors))
-
-
-unquote_plus = urllib.parse.unquote_plus if six.PY3 else _unquote_plus_compat
-unquote = urllib.parse.unquote if six.PY3 else _unquote_compat
-quote = urllib.parse.quote if six.PY3 else _quote_compat
-
-try:
-    # Prefer simplejson
-    import simplejson as json
-except ImportError:
-    import json
-
-
-json_decode = json.JSONDecoder().decode
-_json_encode = json.JSONEncoder().iterencode
-
-
-if six.PY3:
-    # Encode to bytes on Python 3
-    def json_encode(value):
-        for chunk in _json_encode(value):
-            yield chunk.encode('utf-8')
-else:
-    json_encode = _json_encode
-
-
-text_or_bytes = six.text_type, bytes
-
-
-if sys.version_info >= (3, 3):
-    Timer = threading.Timer
-    Event = threading.Event
-else:
-    # Python 3.2 and earlier
-    Timer = threading._Timer
-    Event = threading._Event
-
-# html module come in 3.2 version
-try:
-    from html import escape
-except ImportError:
-    from cgi import escape
-
-
-# html module needed the argument quote=False because in cgi the default
-# is False. With quote=True the results differ.
-
-def escape_html(s, escape_quote=False):
-    """Replace special characters "&", "<" and ">" to HTML-safe sequences.
-
-    When escape_quote=True, escape (') and (") chars.
-    """
-    return escape(s, quote=escape_quote)
diff --git a/libraries/cherrypy/_cpconfig.py b/libraries/cherrypy/_cpconfig.py
deleted file mode 100644
index 79d9d911..00000000
--- a/libraries/cherrypy/_cpconfig.py
+++ /dev/null
@@ -1,300 +0,0 @@
-"""
-Configuration system for CherryPy.
-
-Configuration in CherryPy is implemented via dictionaries. Keys are strings
-which name the mapped value, which may be of any type.
-
-
-Architecture
-------------
-
-CherryPy Requests are part of an Application, which runs in a global context,
-and configuration data may apply to any of those three scopes:
-
-Global
-    Configuration entries which apply everywhere are stored in
-    cherrypy.config.
-
-Application
-    Entries which apply to each mounted application are stored
-    on the Application object itself, as 'app.config'. This is a two-level
-    dict where each key is a path, or "relative URL" (for example, "/" or
-    "/path/to/my/page"), and each value is a config dict. Usually, this
-    data is provided in the call to tree.mount(root(), config=conf),
-    although you may also use app.merge(conf).
-
-Request
-    Each Request object possesses a single 'Request.config' dict.
-    Early in the request process, this dict is populated by merging global
-    config entries, Application entries (whose path equals or is a parent
-    of Request.path_info), and any config acquired while looking up the
-    page handler (see next).
-
-
-Declaration
------------
-
-Configuration data may be supplied as a Python dictionary, as a filename,
-or as an open file object. When you supply a filename or file, CherryPy
-uses Python's builtin ConfigParser; you declare Application config by
-writing each path as a section header::
-
-    [/path/to/my/page]
-    request.stream = True
-
-To declare global configuration entries, place them in a [global] section.
-
-You may also declare config entries directly on the classes and methods
-(page handlers) that make up your CherryPy application via the ``_cp_config``
-attribute, set with the ``cherrypy.config`` decorator. For example::
-
-    @cherrypy.config(**{'tools.gzip.on': True})
-    class Demo:
-
-        @cherrypy.expose
-        @cherrypy.config(**{'request.show_tracebacks': False})
-        def index(self):
-            return "Hello world"
-
-.. note::
-
-    This behavior is only guaranteed for the default dispatcher.
-    Other dispatchers may have different restrictions on where
-    you can attach config attributes.
-
-
-Namespaces
-----------
-
-Configuration keys are separated into namespaces by the first "." in the key.
-Current namespaces:
-
-engine
-    Controls the 'application engine', including autoreload.
-    These can only be declared in the global config.
-
-tree
-    Grafts cherrypy.Application objects onto cherrypy.tree.
-    These can only be declared in the global config.
-
-hooks
-    Declares additional request-processing functions.
-
-log
-    Configures the logging for each application.
-    These can only be declared in the global or / config.
-
-request
-    Adds attributes to each Request.
-
-response
-    Adds attributes to each Response.
-
-server
-    Controls the default HTTP server via cherrypy.server.
-    These can only be declared in the global config.
-
-tools
-    Runs and configures additional request-processing packages.
-
-wsgi
-    Adds WSGI middleware to an Application's "pipeline".
-    These can only be declared in the app's root config ("/").
-
-checker
-    Controls the 'checker', which looks for common errors in
-    app state (including config) when the engine starts.
-    Global config only.
-
-The only key that does not exist in a namespace is the "environment" entry.
-This special entry 'imports' other config entries from a template stored in
-cherrypy._cpconfig.environments[environment]. It only applies to the global
-config, and only when you use cherrypy.config.update.
-
-You can define your own namespaces to be called at the Global, Application,
-or Request level, by adding a named handler to cherrypy.config.namespaces,
-app.namespaces, or app.request_class.namespaces. The name can
-be any string, and the handler must be either a callable or a (Python 2.5
-style) context manager.
-"""
-
-import cherrypy
-from cherrypy._cpcompat import text_or_bytes
-from cherrypy.lib import reprconf
-
-
-def _if_filename_register_autoreload(ob):
-    """Register for autoreload if ob is a string (presumed filename)."""
-    is_filename = isinstance(ob, text_or_bytes)
-    is_filename and cherrypy.engine.autoreload.files.add(ob)
-
-
-def merge(base, other):
-    """Merge one app config (from a dict, file, or filename) into another.
-
-    If the given config is a filename, it will be appended to
-    the list of files to monitor for "autoreload" changes.
-    """
-    _if_filename_register_autoreload(other)
-
-    # Load other into base
-    for section, value_map in reprconf.Parser.load(other).items():
-        if not isinstance(value_map, dict):
-            raise ValueError(
-                'Application config must include section headers, but the '
-                "config you tried to merge doesn't have any sections. "
-                'Wrap your config in another dict with paths as section '
-                "headers, for example: {'/': config}.")
-        base.setdefault(section, {}).update(value_map)
-
-
-class Config(reprconf.Config):
-    """The 'global' configuration data for the entire CherryPy process."""
-
-    def update(self, config):
-        """Update self from a dict, file or filename."""
-        _if_filename_register_autoreload(config)
-        super(Config, self).update(config)
-
-    def _apply(self, config):
-        """Update self from a dict."""
-        if isinstance(config.get('global'), dict):
-            if len(config) > 1:
-                cherrypy.checker.global_config_contained_paths = True
-            config = config['global']
-        if 'tools.staticdir.dir' in config:
-            config['tools.staticdir.section'] = 'global'
-        super(Config, self)._apply(config)
-
-    @staticmethod
-    def __call__(**kwargs):
-        """Decorate for page handlers to set _cp_config."""
-        def tool_decorator(f):
-            _Vars(f).setdefault('_cp_config', {}).update(kwargs)
-            return f
-        return tool_decorator
-
-
-class _Vars(object):
-    """Adapter allowing setting a default attribute on a function or class."""
-
-    def __init__(self, target):
-        self.target = target
-
-    def setdefault(self, key, default):
-        if not hasattr(self.target, key):
-            setattr(self.target, key, default)
-        return getattr(self.target, key)
-
-
-# Sphinx begin config.environments
-Config.environments = environments = {
-    'staging': {
-        'engine.autoreload.on': False,
-        'checker.on': False,
-        'tools.log_headers.on': False,
-        'request.show_tracebacks': False,
-        'request.show_mismatched_params': False,
-    },
-    'production': {
-        'engine.autoreload.on': False,
-        'checker.on': False,
-        'tools.log_headers.on': False,
-        'request.show_tracebacks': False,
-        'request.show_mismatched_params': False,
-        'log.screen': False,
-    },
-    'embedded': {
-        # For use with CherryPy embedded in another deployment stack.
-        'engine.autoreload.on': False,
-        'checker.on': False,
-        'tools.log_headers.on': False,
-        'request.show_tracebacks': False,
-        'request.show_mismatched_params': False,
-        'log.screen': False,
-        'engine.SIGHUP': None,
-        'engine.SIGTERM': None,
-    },
-    'test_suite': {
-        'engine.autoreload.on': False,
-        'checker.on': False,
-        'tools.log_headers.on': False,
-        'request.show_tracebacks': True,
-        'request.show_mismatched_params': True,
-        'log.screen': False,
-    },
-}
-# Sphinx end config.environments
-
-
-def _server_namespace_handler(k, v):
-    """Config handler for the "server" namespace."""
-    atoms = k.split('.', 1)
-    if len(atoms) > 1:
-        # Special-case config keys of the form 'server.servername.socket_port'
-        # to configure additional HTTP servers.
-        if not hasattr(cherrypy, 'servers'):
-            cherrypy.servers = {}
-
-        servername, k = atoms
-        if servername not in cherrypy.servers:
-            from cherrypy import _cpserver
-            cherrypy.servers[servername] = _cpserver.Server()
-            # On by default, but 'on = False' can unsubscribe it (see below).
-            cherrypy.servers[servername].subscribe()
-
-        if k == 'on':
-            if v:
-                cherrypy.servers[servername].subscribe()
-            else:
-                cherrypy.servers[servername].unsubscribe()
-        else:
-            setattr(cherrypy.servers[servername], k, v)
-    else:
-        setattr(cherrypy.server, k, v)
-
-
-Config.namespaces['server'] = _server_namespace_handler
-
-
-def _engine_namespace_handler(k, v):
-    """Config handler for the "engine" namespace."""
-    engine = cherrypy.engine
-
-    if k in {'SIGHUP', 'SIGTERM'}:
-        engine.subscribe(k, v)
-        return
-
-    if '.' in k:
-        plugin, attrname = k.split('.', 1)
-        try:
-            plugin = getattr(engine, plugin)
-        except Exception as error:
-            setattr(engine, k, v)
-        else:
-            op = 'subscribe' if v else 'unsubscribe'
-            sub_unsub = getattr(plugin, op, None)
-            if attrname == 'on' and callable(sub_unsub):
-                sub_unsub()
-                return
-            setattr(plugin, attrname, v)
-    else:
-        setattr(engine, k, v)
-
-
-Config.namespaces['engine'] = _engine_namespace_handler
-
-
-def _tree_namespace_handler(k, v):
-    """Namespace handler for the 'tree' config namespace."""
-    if isinstance(v, dict):
-        for script_name, app in v.items():
-            cherrypy.tree.graft(app, script_name)
-            msg = 'Mounted: %s on %s' % (app, script_name or '/')
-            cherrypy.engine.log(msg)
-    else:
-        cherrypy.tree.graft(v, v.script_name)
-        cherrypy.engine.log('Mounted: %s on %s' % (v, v.script_name or '/'))
-
-
-Config.namespaces['tree'] = _tree_namespace_handler
diff --git a/libraries/cherrypy/_cpdispatch.py b/libraries/cherrypy/_cpdispatch.py
deleted file mode 100644
index 83eb79cb..00000000
--- a/libraries/cherrypy/_cpdispatch.py
+++ /dev/null
@@ -1,686 +0,0 @@
-"""CherryPy dispatchers.
-
-A 'dispatcher' is the object which looks up the 'page handler' callable
-and collects config for the current request based on the path_info, other
-request attributes, and the application architecture. The core calls the
-dispatcher as early as possible, passing it a 'path_info' argument.
-
-The default dispatcher discovers the page handler by matching path_info
-to a hierarchical arrangement of objects, starting at request.app.root.
-"""
-
-import string
-import sys
-import types
-try:
-    classtype = (type, types.ClassType)
-except AttributeError:
-    classtype = type
-
-import cherrypy
-
-
-class PageHandler(object):
-
-    """Callable which sets response.body."""
-
-    def __init__(self, callable, *args, **kwargs):
-        self.callable = callable
-        self.args = args
-        self.kwargs = kwargs
-
-    @property
-    def args(self):
-        """The ordered args should be accessible from post dispatch hooks."""
-        return cherrypy.serving.request.args
-
-    @args.setter
-    def args(self, args):
-        cherrypy.serving.request.args = args
-        return cherrypy.serving.request.args
-
-    @property
-    def kwargs(self):
-        """The named kwargs should be accessible from post dispatch hooks."""
-        return cherrypy.serving.request.kwargs
-
-    @kwargs.setter
-    def kwargs(self, kwargs):
-        cherrypy.serving.request.kwargs = kwargs
-        return cherrypy.serving.request.kwargs
-
-    def __call__(self):
-        try:
-            return self.callable(*self.args, **self.kwargs)
-        except TypeError:
-            x = sys.exc_info()[1]
-            try:
-                test_callable_spec(self.callable, self.args, self.kwargs)
-            except cherrypy.HTTPError:
-                raise sys.exc_info()[1]
-            except Exception:
-                raise x
-            raise
-
-
-def test_callable_spec(callable, callable_args, callable_kwargs):
-    """
-    Inspect callable and test to see if the given args are suitable for it.
-
-    When an error occurs during the handler's invoking stage there are 2
-    erroneous cases:
-    1.  Too many parameters passed to a function which doesn't define
-        one of *args or **kwargs.
-    2.  Too little parameters are passed to the function.
-
-    There are 3 sources of parameters to a cherrypy handler.
-    1.  query string parameters are passed as keyword parameters to the
-        handler.
-    2.  body parameters are also passed as keyword parameters.
-    3.  when partial matching occurs, the final path atoms are passed as
-        positional args.
-    Both the query string and path atoms are part of the URI.  If they are
-    incorrect, then a 404 Not Found should be raised. Conversely the body
-    parameters are part of the request; if they are invalid a 400 Bad Request.
-    """
-    show_mismatched_params = getattr(
-        cherrypy.serving.request, 'show_mismatched_params', False)
-    try:
-        (args, varargs, varkw, defaults) = getargspec(callable)
-    except TypeError:
-        if isinstance(callable, object) and hasattr(callable, '__call__'):
-            (args, varargs, varkw,
-             defaults) = getargspec(callable.__call__)
-        else:
-            # If it wasn't one of our own types, re-raise
-            # the original error
-            raise
-
-    if args and (
-            # For callable objects, which have a __call__(self) method
-            hasattr(callable, '__call__') or
-            # For normal methods
-            inspect.ismethod(callable)
-    ):
-        # Strip 'self'
-        args = args[1:]
-
-    arg_usage = dict([(arg, 0,) for arg in args])
-    vararg_usage = 0
-    varkw_usage = 0
-    extra_kwargs = set()
-
-    for i, value in enumerate(callable_args):
-        try:
-            arg_usage[args[i]] += 1
-        except IndexError:
-            vararg_usage += 1
-
-    for key in callable_kwargs.keys():
-        try:
-            arg_usage[key] += 1
-        except KeyError:
-            varkw_usage += 1
-            extra_kwargs.add(key)
-
-    # figure out which args have defaults.
-    args_with_defaults = args[-len(defaults or []):]
-    for i, val in enumerate(defaults or []):
-        # Defaults take effect only when the arg hasn't been used yet.
-        if arg_usage[args_with_defaults[i]] == 0:
-            arg_usage[args_with_defaults[i]] += 1
-
-    missing_args = []
-    multiple_args = []
-    for key, usage in arg_usage.items():
-        if usage == 0:
-            missing_args.append(key)
-        elif usage > 1:
-            multiple_args.append(key)
-
-    if missing_args:
-        # In the case where the method allows body arguments
-        # there are 3 potential errors:
-        # 1. not enough query string parameters -> 404
-        # 2. not enough body parameters -> 400
-        # 3. not enough path parts (partial matches) -> 404
-        #
-        # We can't actually tell which case it is,
-        # so I'm raising a 404 because that covers 2/3 of the
-        # possibilities
-        #
-        # In the case where the method does not allow body
-        # arguments it's definitely a 404.
-        message = None
-        if show_mismatched_params:
-            message = 'Missing parameters: %s' % ','.join(missing_args)
-        raise cherrypy.HTTPError(404, message=message)
-
-    # the extra positional arguments come from the path - 404 Not Found
-    if not varargs and vararg_usage > 0:
-        raise cherrypy.HTTPError(404)
-
-    body_params = cherrypy.serving.request.body.params or {}
-    body_params = set(body_params.keys())
-    qs_params = set(callable_kwargs.keys()) - body_params
-
-    if multiple_args:
-        if qs_params.intersection(set(multiple_args)):
-            # If any of the multiple parameters came from the query string then
-            # it's a 404 Not Found
-            error = 404
-        else:
-            # Otherwise it's a 400 Bad Request
-            error = 400
-
-        message = None
-        if show_mismatched_params:
-            message = 'Multiple values for parameters: '\
-                '%s' % ','.join(multiple_args)
-        raise cherrypy.HTTPError(error, message=message)
-
-    if not varkw and varkw_usage > 0:
-
-        # If there were extra query string parameters, it's a 404 Not Found
-        extra_qs_params = set(qs_params).intersection(extra_kwargs)
-        if extra_qs_params:
-            message = None
-            if show_mismatched_params:
-                message = 'Unexpected query string '\
-                    'parameters: %s' % ', '.join(extra_qs_params)
-            raise cherrypy.HTTPError(404, message=message)
-
-        # If there were any extra body parameters, it's a 400 Not Found
-        extra_body_params = set(body_params).intersection(extra_kwargs)
-        if extra_body_params:
-            message = None
-            if show_mismatched_params:
-                message = 'Unexpected body parameters: '\
-                    '%s' % ', '.join(extra_body_params)
-            raise cherrypy.HTTPError(400, message=message)
-
-
-try:
-    import inspect
-except ImportError:
-    def test_callable_spec(callable, args, kwargs):  # noqa: F811
-        return None
-else:
-    getargspec = inspect.getargspec
-    # Python 3 requires using getfullargspec if
-    # keyword-only arguments are present
-    if hasattr(inspect, 'getfullargspec'):
-        def getargspec(callable):
-            return inspect.getfullargspec(callable)[:4]
-
-
-class LateParamPageHandler(PageHandler):
-
-    """When passing cherrypy.request.params to the page handler, we do not
-    want to capture that dict too early; we want to give tools like the
-    decoding tool a chance to modify the params dict in-between the lookup
-    of the handler and the actual calling of the handler. This subclass
-    takes that into account, and allows request.params to be 'bound late'
-    (it's more complicated than that, but that's the effect).
-    """
-
-    @property
-    def kwargs(self):
-        """Page handler kwargs (with cherrypy.request.params copied in)."""
-        kwargs = cherrypy.serving.request.params.copy()
-        if self._kwargs:
-            kwargs.update(self._kwargs)
-        return kwargs
-
-    @kwargs.setter
-    def kwargs(self, kwargs):
-        cherrypy.serving.request.kwargs = kwargs
-        self._kwargs = kwargs
-
-
-if sys.version_info < (3, 0):
-    punctuation_to_underscores = string.maketrans(
-        string.punctuation, '_' * len(string.punctuation))
-
-    def validate_translator(t):
-        if not isinstance(t, str) or len(t) != 256:
-            raise ValueError(
-                'The translate argument must be a str of len 256.')
-else:
-    punctuation_to_underscores = str.maketrans(
-        string.punctuation, '_' * len(string.punctuation))
-
-    def validate_translator(t):
-        if not isinstance(t, dict):
-            raise ValueError('The translate argument must be a dict.')
-
-
-class Dispatcher(object):
-
-    """CherryPy Dispatcher which walks a tree of objects to find a handler.
-
-    The tree is rooted at cherrypy.request.app.root, and each hierarchical
-    component in the path_info argument is matched to a corresponding nested
-    attribute of the root object. Matching handlers must have an 'exposed'
-    attribute which evaluates to True. The special method name "index"
-    matches a URI which ends in a slash ("/"). The special method name
-    "default" may match a portion of the path_info (but only when no longer
-    substring of the path_info matches some other object).
-
-    This is the default, built-in dispatcher for CherryPy.
-    """
-
-    dispatch_method_name = '_cp_dispatch'
-    """
-    The name of the dispatch method that nodes may optionally implement
-    to provide their own dynamic dispatch algorithm.
-    """
-
-    def __init__(self, dispatch_method_name=None,
-                 translate=punctuation_to_underscores):
-        validate_translator(translate)
-        self.translate = translate
-        if dispatch_method_name:
-            self.dispatch_method_name = dispatch_method_name
-
-    def __call__(self, path_info):
-        """Set handler and config for the current request."""
-        request = cherrypy.serving.request
-        func, vpath = self.find_handler(path_info)
-
-        if func:
-            # Decode any leftover %2F in the virtual_path atoms.
-            vpath = [x.replace('%2F', '/') for x in vpath]
-            request.handler = LateParamPageHandler(func, *vpath)
-        else:
-            request.handler = cherrypy.NotFound()
-
-    def find_handler(self, path):
-        """Return the appropriate page handler, plus any virtual path.
-
-        This will return two objects. The first will be a callable,
-        which can be used to generate page output. Any parameters from
-        the query string or request body will be sent to that callable
-        as keyword arguments.
-
-        The callable is found by traversing the application's tree,
-        starting from cherrypy.request.app.root, and matching path
-        components to successive objects in the tree. For example, the
-        URL "/path/to/handler" might return root.path.to.handler.
-
-        The second object returned will be a list of names which are
-        'virtual path' components: parts of the URL which are dynamic,
-        and were not used when looking up the handler.
-        These virtual path components are passed to the handler as
-        positional arguments.
-        """
-        request = cherrypy.serving.request
-        app = request.app
-        root = app.root
-        dispatch_name = self.dispatch_method_name
-
-        # Get config for the root object/path.
-        fullpath = [x for x in path.strip('/').split('/') if x] + ['index']
-        fullpath_len = len(fullpath)
-        segleft = fullpath_len
-        nodeconf = {}
-        if hasattr(root, '_cp_config'):
-            nodeconf.update(root._cp_config)
-        if '/' in app.config:
-            nodeconf.update(app.config['/'])
-        object_trail = [['root', root, nodeconf, segleft]]
-
-        node = root
-        iternames = fullpath[:]
-        while iternames:
-            name = iternames[0]
-            # map to legal Python identifiers (e.g. replace '.' with '_')
-            objname = name.translate(self.translate)
-
-            nodeconf = {}
-            subnode = getattr(node, objname, None)
-            pre_len = len(iternames)
-            if subnode is None:
-                dispatch = getattr(node, dispatch_name, None)
-                if dispatch and hasattr(dispatch, '__call__') and not \
-                        getattr(dispatch, 'exposed', False) and \
-                        pre_len > 1:
-                    # Don't expose the hidden 'index' token to _cp_dispatch
-                    # We skip this if pre_len == 1 since it makes no sense
-                    # to call a dispatcher when we have no tokens left.
-                    index_name = iternames.pop()
-                    subnode = dispatch(vpath=iternames)
-                    iternames.append(index_name)
-                else:
-                    # We didn't find a path, but keep processing in case there
-                    # is a default() handler.
-                    iternames.pop(0)
-            else:
-                # We found the path, remove the vpath entry
-                iternames.pop(0)
-            segleft = len(iternames)
-            if segleft > pre_len:
-                # No path segment was removed.  Raise an error.
-                raise cherrypy.CherryPyException(
-                    'A vpath segment was added.  Custom dispatchers may only '
-                    'remove elements.  While trying to process '
-                    '{0} in {1}'.format(name, fullpath)
-                )
-            elif segleft == pre_len:
-                # Assume that the handler used the current path segment, but
-                # did not pop it.  This allows things like
-                # return getattr(self, vpath[0], None)
-                iternames.pop(0)
-                segleft -= 1
-            node = subnode
-
-            if node is not None:
-                # Get _cp_config attached to this node.
-                if hasattr(node, '_cp_config'):
-                    nodeconf.update(node._cp_config)
-
-            # Mix in values from app.config for this path.
-            existing_len = fullpath_len - pre_len
-            if existing_len != 0:
-                curpath = '/' + '/'.join(fullpath[0:existing_len])
-            else:
-                curpath = ''
-            new_segs = fullpath[fullpath_len - pre_len:fullpath_len - segleft]
-            for seg in new_segs:
-                curpath += '/' + seg
-                if curpath in app.config:
-                    nodeconf.update(app.config[curpath])
-
-            object_trail.append([name, node, nodeconf, segleft])
-
-        def set_conf():
-            """Collapse all object_trail config into cherrypy.request.config.
-            """
-            base = cherrypy.config.copy()
-            # Note that we merge the config from each node
-            # even if that node was None.
-            for name, obj, conf, segleft in object_trail:
-                base.update(conf)
-                if 'tools.staticdir.dir' in conf:
-                    base['tools.staticdir.section'] = '/' + \
-                        '/'.join(fullpath[0:fullpath_len - segleft])
-            return base
-
-        # Try successive objects (reverse order)
-        num_candidates = len(object_trail) - 1
-        for i in range(num_candidates, -1, -1):
-
-            name, candidate, nodeconf, segleft = object_trail[i]
-            if candidate is None:
-                continue
-
-            # Try a "default" method on the current leaf.
-            if hasattr(candidate, 'default'):
-                defhandler = candidate.default
-                if getattr(defhandler, 'exposed', False):
-                    # Insert any extra _cp_config from the default handler.
-                    conf = getattr(defhandler, '_cp_config', {})
-                    object_trail.insert(
-                        i + 1, ['default', defhandler, conf, segleft])
-                    request.config = set_conf()
-                    # See https://github.com/cherrypy/cherrypy/issues/613
-                    request.is_index = path.endswith('/')
-                    return defhandler, fullpath[fullpath_len - segleft:-1]
-
-            # Uncomment the next line to restrict positional params to
-            # "default".
-            # if i < num_candidates - 2: continue
-
-            # Try the current leaf.
-            if getattr(candidate, 'exposed', False):
-                request.config = set_conf()
-                if i == num_candidates:
-                    # We found the extra ".index". Mark request so tools
-                    # can redirect if path_info has no trailing slash.
-                    request.is_index = True
-                else:
-                    # We're not at an 'index' handler. Mark request so tools
-                    # can redirect if path_info has NO trailing slash.
-                    # Note that this also includes handlers which take
-                    # positional parameters (virtual paths).
-                    request.is_index = False
-                return candidate, fullpath[fullpath_len - segleft:-1]
-
-        # We didn't find anything
-        request.config = set_conf()
-        return None, []
-
-
-class MethodDispatcher(Dispatcher):
-
-    """Additional dispatch based on cherrypy.request.method.upper().
-
-    Methods named GET, POST, etc will be called on an exposed class.
-    The method names must be all caps; the appropriate Allow header
-    will be output showing all capitalized method names as allowable
-    HTTP verbs.
-
-    Note that the containing class must be exposed, not the methods.
-    """
-
-    def __call__(self, path_info):
-        """Set handler and config for the current request."""
-        request = cherrypy.serving.request
-        resource, vpath = self.find_handler(path_info)
-
-        if resource:
-            # Set Allow header
-            avail = [m for m in dir(resource) if m.isupper()]
-            if 'GET' in avail and 'HEAD' not in avail:
-                avail.append('HEAD')
-            avail.sort()
-            cherrypy.serving.response.headers['Allow'] = ', '.join(avail)
-
-            # Find the subhandler
-            meth = request.method.upper()
-            func = getattr(resource, meth, None)
-            if func is None and meth == 'HEAD':
-                func = getattr(resource, 'GET', None)
-            if func:
-                # Grab any _cp_config on the subhandler.
-                if hasattr(func, '_cp_config'):
-                    request.config.update(func._cp_config)
-
-                # Decode any leftover %2F in the virtual_path atoms.
-                vpath = [x.replace('%2F', '/') for x in vpath]
-                request.handler = LateParamPageHandler(func, *vpath)
-            else:
-                request.handler = cherrypy.HTTPError(405)
-        else:
-            request.handler = cherrypy.NotFound()
-
-
-class RoutesDispatcher(object):
-
-    """A Routes based dispatcher for CherryPy."""
-
-    def __init__(self, full_result=False, **mapper_options):
-        """
-        Routes dispatcher
-
-        Set full_result to True if you wish the controller
-        and the action to be passed on to the page handler
-        parameters. By default they won't be.
-        """
-        import routes
-        self.full_result = full_result
-        self.controllers = {}
-        self.mapper = routes.Mapper(**mapper_options)
-        self.mapper.controller_scan = self.controllers.keys
-
-    def connect(self, name, route, controller, **kwargs):
-        self.controllers[name] = controller
-        self.mapper.connect(name, route, controller=name, **kwargs)
-
-    def redirect(self, url):
-        raise cherrypy.HTTPRedirect(url)
-
-    def __call__(self, path_info):
-        """Set handler and config for the current request."""
-        func = self.find_handler(path_info)
-        if func:
-            cherrypy.serving.request.handler = LateParamPageHandler(func)
-        else:
-            cherrypy.serving.request.handler = cherrypy.NotFound()
-
-    def find_handler(self, path_info):
-        """Find the right page handler, and set request.config."""
-        import routes
-
-        request = cherrypy.serving.request
-
-        config = routes.request_config()
-        config.mapper = self.mapper
-        if hasattr(request, 'wsgi_environ'):
-            config.environ = request.wsgi_environ
-        config.host = request.headers.get('Host', None)
-        config.protocol = request.scheme
-        config.redirect = self.redirect
-
-        result = self.mapper.match(path_info)
-
-        config.mapper_dict = result
-        params = {}
-        if result:
-            params = result.copy()
-        if not self.full_result:
-            params.pop('controller', None)
-            params.pop('action', None)
-        request.params.update(params)
-
-        # Get config for the root object/path.
-        request.config = base = cherrypy.config.copy()
-        curpath = ''
-
-        def merge(nodeconf):
-            if 'tools.staticdir.dir' in nodeconf:
-                nodeconf['tools.staticdir.section'] = curpath or '/'
-            base.update(nodeconf)
-
-        app = request.app
-        root = app.root
-        if hasattr(root, '_cp_config'):
-            merge(root._cp_config)
-        if '/' in app.config:
-            merge(app.config['/'])
-
-        # Mix in values from app.config.
-        atoms = [x for x in path_info.split('/') if x]
-        if atoms:
-            last = atoms.pop()
-        else:
-            last = None
-        for atom in atoms:
-            curpath = '/'.join((curpath, atom))
-            if curpath in app.config:
-                merge(app.config[curpath])
-
-        handler = None
-        if result:
-            controller = result.get('controller')
-            controller = self.controllers.get(controller, controller)
-            if controller:
-                if isinstance(controller, classtype):
-                    controller = controller()
-                # Get config from the controller.
-                if hasattr(controller, '_cp_config'):
-                    merge(controller._cp_config)
-
-            action = result.get('action')
-            if action is not None:
-                handler = getattr(controller, action, None)
-                # Get config from the handler
-                if hasattr(handler, '_cp_config'):
-                    merge(handler._cp_config)
-            else:
-                handler = controller
-
-        # Do the last path atom here so it can
-        # override the controller's _cp_config.
-        if last:
-            curpath = '/'.join((curpath, last))
-            if curpath in app.config:
-                merge(app.config[curpath])
-
-        return handler
-
-
-def XMLRPCDispatcher(next_dispatcher=Dispatcher()):
-    from cherrypy.lib import xmlrpcutil
-
-    def xmlrpc_dispatch(path_info):
-        path_info = xmlrpcutil.patched_path(path_info)
-        return next_dispatcher(path_info)
-    return xmlrpc_dispatch
-
-
-def VirtualHost(next_dispatcher=Dispatcher(), use_x_forwarded_host=True,
-                **domains):
-    """
-    Select a different handler based on the Host header.
-
-    This can be useful when running multiple sites within one CP server.
-    It allows several domains to point to different parts of a single
-    website structure. For example::
-
-        http://www.domain.example  ->  root
-        http://www.domain2.example  ->  root/domain2/
-        http://www.domain2.example:443  ->  root/secure
-
-    can be accomplished via the following config::
-
-        [/]
-        request.dispatch = cherrypy.dispatch.VirtualHost(
-            **{'www.domain2.example': '/domain2',
-               'www.domain2.example:443': '/secure',
-              })
-
-    next_dispatcher
-        The next dispatcher object in the dispatch chain.
-        The VirtualHost dispatcher adds a prefix to the URL and calls
-        another dispatcher. Defaults to cherrypy.dispatch.Dispatcher().
-
-    use_x_forwarded_host
-        If True (the default), any "X-Forwarded-Host"
-        request header will be used instead of the "Host" header. This
-        is commonly added by HTTP servers (such as Apache) when proxying.
-
-    ``**domains``
-        A dict of {host header value: virtual prefix} pairs.
-        The incoming "Host" request header is looked up in this dict,
-        and, if a match is found, the corresponding "virtual prefix"
-        value will be prepended to the URL path before calling the
-        next dispatcher. Note that you often need separate entries
-        for "example.com" and "www.example.com". In addition, "Host"
-        headers may contain the port number.
-    """
-    from cherrypy.lib import httputil
-
-    def vhost_dispatch(path_info):
-        request = cherrypy.serving.request
-        header = request.headers.get
-
-        domain = header('Host', '')
-        if use_x_forwarded_host:
-            domain = header('X-Forwarded-Host', domain)
-
-        prefix = domains.get(domain, '')
-        if prefix:
-            path_info = httputil.urljoin(prefix, path_info)
-
-        result = next_dispatcher(path_info)
-
-        # Touch up staticdir config. See
-        # https://github.com/cherrypy/cherrypy/issues/614.
-        section = request.config.get('tools.staticdir.section')
-        if section:
-            section = section[len(prefix):]
-            request.config['tools.staticdir.section'] = section
-
-        return result
-    return vhost_dispatch
diff --git a/libraries/cherrypy/_cperror.py b/libraries/cherrypy/_cperror.py
deleted file mode 100644
index e2a8fad8..00000000
--- a/libraries/cherrypy/_cperror.py
+++ /dev/null
@@ -1,619 +0,0 @@
-"""Exception classes for CherryPy.
-
-CherryPy provides (and uses) exceptions for declaring that the HTTP response
-should be a status other than the default "200 OK". You can ``raise`` them like
-normal Python exceptions. You can also call them and they will raise
-themselves; this means you can set an
-:class:`HTTPError<cherrypy._cperror.HTTPError>`
-or :class:`HTTPRedirect<cherrypy._cperror.HTTPRedirect>` as the
-:attr:`request.handler<cherrypy._cprequest.Request.handler>`.
-
-.. _redirectingpost:
-
-Redirecting POST
-================
-
-When you GET a resource and are redirected by the server to another Location,
-there's generally no problem since GET is both a "safe method" (there should
-be no side-effects) and an "idempotent method" (multiple calls are no different
-than a single call).
-
-POST, however, is neither safe nor idempotent--if you
-charge a credit card, you don't want to be charged twice by a redirect!
-
-For this reason, *none* of the 3xx responses permit a user-agent (browser) to
-resubmit a POST on redirection without first confirming the action with the
-user:
-
-=====    =================================    ===========
-300      Multiple Choices                     Confirm with the user
-301      Moved Permanently                    Confirm with the user
-302      Found (Object moved temporarily)     Confirm with the user
-303      See Other                            GET the new URI; no confirmation
-304      Not modified                         for conditional GET only;
-                                              POST should not raise this error
-305      Use Proxy                            Confirm with the user
-307      Temporary Redirect                   Confirm with the user
-=====    =================================    ===========
-
-However, browsers have historically implemented these restrictions poorly;
-in particular, many browsers do not force the user to confirm 301, 302
-or 307 when redirecting POST. For this reason, CherryPy defaults to 303,
-which most user-agents appear to have implemented correctly. Therefore, if
-you raise HTTPRedirect for a POST request, the user-agent will most likely
-attempt to GET the new URI (without asking for confirmation from the user).
-We realize this is confusing for developers, but it's the safest thing we
-could do. You are of course free to raise ``HTTPRedirect(uri, status=302)``
-or any other 3xx status if you know what you're doing, but given the
-environment, we couldn't let any of those be the default.
-
-Custom Error Handling
-=====================
-
-.. image:: /refman/cperrors.gif
-
-Anticipated HTTP responses
---------------------------
-
-The 'error_page' config namespace can be used to provide custom HTML output for
-expected responses (like 404 Not Found). Supply a filename from which the
-output will be read. The contents will be interpolated with the values
-%(status)s, %(message)s, %(traceback)s, and %(version)s using plain old Python
-`string formatting
-<http://docs.python.org/2/library/stdtypes.html#string-formatting-operations>`_.
-
-::
-
-    _cp_config = {
-        'error_page.404': os.path.join(localDir, "static/index.html")
-    }
-
-
-Beginning in version 3.1, you may also provide a function or other callable as
-an error_page entry. It will be passed the same status, message, traceback and
-version arguments that are interpolated into templates::
-
-    def error_page_402(status, message, traceback, version):
-        return "Error %s - Well, I'm very sorry but you haven't paid!" % status
-    cherrypy.config.update({'error_page.402': error_page_402})
-
-Also in 3.1, in addition to the numbered error codes, you may also supply
-"error_page.default" to handle all codes which do not have their own error_page
-entry.
-
-
-
-Unanticipated errors
---------------------
-
-CherryPy also has a generic error handling mechanism: whenever an unanticipated
-error occurs in your code, it will call
-:func:`Request.error_response<cherrypy._cprequest.Request.error_response>` to
-set the response status, headers, and body. By default, this is the same
-output as
-:class:`HTTPError(500) <cherrypy._cperror.HTTPError>`. If you want to provide
-some other behavior, you generally replace "request.error_response".
-
-Here is some sample code that shows how to display a custom error message and
-send an e-mail containing the error::
-
-    from cherrypy import _cperror
-
-    def handle_error():
-        cherrypy.response.status = 500
-        cherrypy.response.body = [
-            "<html><body>Sorry, an error occurred</body></html>"
-        ]
-        sendMail('error@domain.com',
-                 'Error in your web app',
-                 _cperror.format_exc())
-
-    @cherrypy.config(**{'request.error_response': handle_error})
-    class Root:
-        pass
-
-Note that you have to explicitly set
-:attr:`response.body <cherrypy._cprequest.Response.body>`
-and not simply return an error message as a result.
-"""
-
-import io
-import contextlib
-from sys import exc_info as _exc_info
-from traceback import format_exception as _format_exception
-from xml.sax import saxutils
-
-import six
-from six.moves import urllib
-
-from more_itertools import always_iterable
-
-import cherrypy
-from cherrypy._cpcompat import escape_html
-from cherrypy._cpcompat import ntob
-from cherrypy._cpcompat import tonative
-from cherrypy._helper import classproperty
-from cherrypy.lib import httputil as _httputil
-
-
-class CherryPyException(Exception):
-
-    """A base class for CherryPy exceptions."""
-    pass
-
-
-class InternalRedirect(CherryPyException):
-
-    """Exception raised to switch to the handler for a different URL.
-
-    This exception will redirect processing to another path within the site
-    (without informing the client). Provide the new path as an argument when
-    raising the exception. Provide any params in the querystring for the new
-    URL.
-    """
-
-    def __init__(self, path, query_string=''):
-        self.request = cherrypy.serving.request
-
-        self.query_string = query_string
-        if '?' in path:
-            # Separate any params included in the path
-            path, self.query_string = path.split('?', 1)
-
-        # Note that urljoin will "do the right thing" whether url is:
-        #  1. a URL relative to root (e.g. "/dummy")
-        #  2. a URL relative to the current path
-        # Note that any query string will be discarded.
-        path = urllib.parse.urljoin(self.request.path_info, path)
-
-        # Set a 'path' member attribute so that code which traps this
-        # error can have access to it.
-        self.path = path
-
-        CherryPyException.__init__(self, path, self.query_string)
-
-
-class HTTPRedirect(CherryPyException):
-
-    """Exception raised when the request should be redirected.
-
-    This exception will force a HTTP redirect to the URL or URL's you give it.
-    The new URL must be passed as the first argument to the Exception,
-    e.g., HTTPRedirect(newUrl). Multiple URLs are allowed in a list.
-    If a URL is absolute, it will be used as-is. If it is relative, it is
-    assumed to be relative to the current cherrypy.request.path_info.
-
-    If one of the provided URL is a unicode object, it will be encoded
-    using the default encoding or the one passed in parameter.
-
-    There are multiple types of redirect, from which you can select via the
-    ``status`` argument. If you do not provide a ``status`` arg, it defaults to
-    303 (or 302 if responding with HTTP/1.0).
-
-    Examples::
-
-        raise cherrypy.HTTPRedirect("")
-        raise cherrypy.HTTPRedirect("/abs/path", 307)
-        raise cherrypy.HTTPRedirect(["path1", "path2?a=1&b=2"], 301)
-
-    See :ref:`redirectingpost` for additional caveats.
-    """
-
-    urls = None
-    """The list of URL's to emit."""
-
-    encoding = 'utf-8'
-    """The encoding when passed urls are not native strings"""
-
-    def __init__(self, urls, status=None, encoding=None):
-        self.urls = abs_urls = [
-            # Note that urljoin will "do the right thing" whether url is:
-            #  1. a complete URL with host (e.g. "http://www.example.com/test")
-            #  2. a URL relative to root (e.g. "/dummy")
-            #  3. a URL relative to the current path
-            # Note that any query string in cherrypy.request is discarded.
-            urllib.parse.urljoin(
-                cherrypy.url(),
-                tonative(url, encoding or self.encoding),
-            )
-            for url in always_iterable(urls)
-        ]
-
-        status = (
-            int(status)
-            if status is not None
-            else self.default_status
-        )
-        if not 300 <= status <= 399:
-            raise ValueError('status must be between 300 and 399.')
-
-        CherryPyException.__init__(self, abs_urls, status)
-
-    @classproperty
-    def default_status(cls):
-        """
-        The default redirect status for the request.
-
-        RFC 2616 indicates a 301 response code fits our goal; however,
-        browser support for 301 is quite messy. Use 302/303 instead. See
-        http://www.alanflavell.org.uk/www/post-redirect.html
-        """
-        return 303 if cherrypy.serving.request.protocol >= (1, 1) else 302
-
-    @property
-    def status(self):
-        """The integer HTTP status code to emit."""
-        _, status = self.args[:2]
-        return status
-
-    def set_response(self):
-        """Modify cherrypy.response status, headers, and body to represent
-        self.
-
-        CherryPy uses this internally, but you can also use it to create an
-        HTTPRedirect object and set its output without *raising* the exception.
-        """
-        response = cherrypy.serving.response
-        response.status = status = self.status
-
-        if status in (300, 301, 302, 303, 307):
-            response.headers['Content-Type'] = 'text/html;charset=utf-8'
-            # "The ... URI SHOULD be given by the Location field
-            # in the response."
-            response.headers['Location'] = self.urls[0]
-
-            # "Unless the request method was HEAD, the entity of the response
-            # SHOULD contain a short hypertext note with a hyperlink to the
-            # new URI(s)."
-            msg = {
-                300: 'This resource can be found at ',
-                301: 'This resource has permanently moved to ',
-                302: 'This resource resides temporarily at ',
-                303: 'This resource can be found at ',
-                307: 'This resource has moved temporarily to ',
-            }[status]
-            msg += '<a href=%s>%s</a>.'
-            msgs = [
-                msg % (saxutils.quoteattr(u), escape_html(u))
-                for u in self.urls
-            ]
-            response.body = ntob('<br />\n'.join(msgs), 'utf-8')
-            # Previous code may have set C-L, so we have to reset it
-            # (allow finalize to set it).
-            response.headers.pop('Content-Length', None)
-        elif status == 304:
-            # Not Modified.
-            # "The response MUST include the following header fields:
-            # Date, unless its omission is required by section 14.18.1"
-            # The "Date" header should have been set in Response.__init__
-
-            # "...the response SHOULD NOT include other entity-headers."
-            for key in ('Allow', 'Content-Encoding', 'Content-Language',
-                        'Content-Length', 'Content-Location', 'Content-MD5',
-                        'Content-Range', 'Content-Type', 'Expires',
-                        'Last-Modified'):
-                if key in response.headers:
-                    del response.headers[key]
-
-            # "The 304 response MUST NOT contain a message-body."
-            response.body = None
-            # Previous code may have set C-L, so we have to reset it.
-            response.headers.pop('Content-Length', None)
-        elif status == 305:
-            # Use Proxy.
-            # self.urls[0] should be the URI of the proxy.
-            response.headers['Location'] = ntob(self.urls[0], 'utf-8')
-            response.body = None
-            # Previous code may have set C-L, so we have to reset it.
-            response.headers.pop('Content-Length', None)
-        else:
-            raise ValueError('The %s status code is unknown.' % status)
-
-    def __call__(self):
-        """Use this exception as a request.handler (raise self)."""
-        raise self
-
-
-def clean_headers(status):
-    """Remove any headers which should not apply to an error response."""
-    response = cherrypy.serving.response
-
-    # Remove headers which applied to the original content,
-    # but do not apply to the error page.
-    respheaders = response.headers
-    for key in ['Accept-Ranges', 'Age', 'ETag', 'Location', 'Retry-After',
-                'Vary', 'Content-Encoding', 'Content-Length', 'Expires',
-                'Content-Location', 'Content-MD5', 'Last-Modified']:
-        if key in respheaders:
-            del respheaders[key]
-
-    if status != 416:
-        # A server sending a response with status code 416 (Requested
-        # range not satisfiable) SHOULD include a Content-Range field
-        # with a byte-range-resp-spec of "*". The instance-length
-        # specifies the current length of the selected resource.
-        # A response with status code 206 (Partial Content) MUST NOT
-        # include a Content-Range field with a byte-range- resp-spec of "*".
-        if 'Content-Range' in respheaders:
-            del respheaders['Content-Range']
-
-
-class HTTPError(CherryPyException):
-
-    """Exception used to return an HTTP error code (4xx-5xx) to the client.
-
-    This exception can be used to automatically send a response using a
-    http status code, with an appropriate error page. It takes an optional
-    ``status`` argument (which must be between 400 and 599); it defaults to 500
-    ("Internal Server Error"). It also takes an optional ``message`` argument,
-    which will be returned in the response body. See
-    `RFC2616 <http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4>`_
-    for a complete list of available error codes and when to use them.
-
-    Examples::
-
-        raise cherrypy.HTTPError(403)
-        raise cherrypy.HTTPError(
-            "403 Forbidden", "You are not allowed to access this resource.")
-    """
-
-    status = None
-    """The HTTP status code. May be of type int or str (with a Reason-Phrase).
-    """
-
-    code = None
-    """The integer HTTP status code."""
-
-    reason = None
-    """The HTTP Reason-Phrase string."""
-
-    def __init__(self, status=500, message=None):
-        self.status = status
-        try:
-            self.code, self.reason, defaultmsg = _httputil.valid_status(status)
-        except ValueError:
-            raise self.__class__(500, _exc_info()[1].args[0])
-
-        if self.code < 400 or self.code > 599:
-            raise ValueError('status must be between 400 and 599.')
-
-        # See http://www.python.org/dev/peps/pep-0352/
-        # self.message = message
-        self._message = message or defaultmsg
-        CherryPyException.__init__(self, status, message)
-
-    def set_response(self):
-        """Modify cherrypy.response status, headers, and body to represent
-        self.
-
-        CherryPy uses this internally, but you can also use it to create an
-        HTTPError object and set its output without *raising* the exception.
-        """
-        response = cherrypy.serving.response
-
-        clean_headers(self.code)
-
-        # In all cases, finalize will be called after this method,
-        # so don't bother cleaning up response values here.
-        response.status = self.status
-        tb = None
-        if cherrypy.serving.request.show_tracebacks:
-            tb = format_exc()
-
-        response.headers.pop('Content-Length', None)
-
-        content = self.get_error_page(self.status, traceback=tb,
-                                      message=self._message)
-        response.body = content
-
-        _be_ie_unfriendly(self.code)
-
-    def get_error_page(self, *args, **kwargs):
-        return get_error_page(*args, **kwargs)
-
-    def __call__(self):
-        """Use this exception as a request.handler (raise self)."""
-        raise self
-
-    @classmethod
-    @contextlib.contextmanager
-    def handle(cls, exception, status=500, message=''):
-        """Translate exception into an HTTPError."""
-        try:
-            yield
-        except exception as exc:
-            raise cls(status, message or str(exc))
-
-
-class NotFound(HTTPError):
-
-    """Exception raised when a URL could not be mapped to any handler (404).
-
-    This is equivalent to raising
-    :class:`HTTPError("404 Not Found") <cherrypy._cperror.HTTPError>`.
-    """
-
-    def __init__(self, path=None):
-        if path is None:
-            request = cherrypy.serving.request
-            path = request.script_name + request.path_info
-        self.args = (path,)
-        HTTPError.__init__(self, 404, "The path '%s' was not found." % path)
-
-
-_HTTPErrorTemplate = '''<!DOCTYPE html PUBLIC
-"-//W3C//DTD XHTML 1.0 Transitional//EN"
-"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
-<html>
-<head>
-    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"></meta>
-    <title>%(status)s</title>
-    <style type="text/css">
-    #powered_by {
-        margin-top: 20px;
-        border-top: 2px solid black;
-        font-style: italic;
-    }
-
-    #traceback {
-        color: red;
-    }
-    </style>
-</head>
-    <body>
-        <h2>%(status)s</h2>
-        <p>%(message)s</p>
-        <pre id="traceback">%(traceback)s</pre>
-    <div id="powered_by">
-      <span>
-        Powered by <a href="http://www.cherrypy.org">CherryPy %(version)s</a>
-      </span>
-    </div>
-    </body>
-</html>
-'''
-
-
-def get_error_page(status, **kwargs):
-    """Return an HTML page, containing a pretty error response.
-
-    status should be an int or a str.
-    kwargs will be interpolated into the page template.
-    """
-    try:
-        code, reason, message = _httputil.valid_status(status)
-    except ValueError:
-        raise cherrypy.HTTPError(500, _exc_info()[1].args[0])
-
-    # We can't use setdefault here, because some
-    # callers send None for kwarg values.
-    if kwargs.get('status') is None:
-        kwargs['status'] = '%s %s' % (code, reason)
-    if kwargs.get('message') is None:
-        kwargs['message'] = message
-    if kwargs.get('traceback') is None:
-        kwargs['traceback'] = ''
-    if kwargs.get('version') is None:
-        kwargs['version'] = cherrypy.__version__
-
-    for k, v in six.iteritems(kwargs):
-        if v is None:
-            kwargs[k] = ''
-        else:
-            kwargs[k] = escape_html(kwargs[k])
-
-    # Use a custom template or callable for the error page?
-    pages = cherrypy.serving.request.error_page
-    error_page = pages.get(code) or pages.get('default')
-
-    # Default template, can be overridden below.
-    template = _HTTPErrorTemplate
-    if error_page:
-        try:
-            if hasattr(error_page, '__call__'):
-                # The caller function may be setting headers manually,
-                # so we delegate to it completely. We may be returning
-                # an iterator as well as a string here.
-                #
-                # We *must* make sure any content is not unicode.
-                result = error_page(**kwargs)
-                if cherrypy.lib.is_iterator(result):
-                    from cherrypy.lib.encoding import UTF8StreamEncoder
-                    return UTF8StreamEncoder(result)
-                elif isinstance(result, six.text_type):
-                    return result.encode('utf-8')
-                else:
-                    if not isinstance(result, bytes):
-                        raise ValueError(
-                            'error page function did not '
-                            'return a bytestring, six.text_type or an '
-                            'iterator - returned object of type %s.'
-                            % (type(result).__name__))
-                    return result
-            else:
-                # Load the template from this path.
-                template = io.open(error_page, newline='').read()
-        except Exception:
-            e = _format_exception(*_exc_info())[-1]
-            m = kwargs['message']
-            if m:
-                m += '<br />'
-            m += 'In addition, the custom error page failed:\n<br />%s' % e
-            kwargs['message'] = m
-
-    response = cherrypy.serving.response
-    response.headers['Content-Type'] = 'text/html;charset=utf-8'
-    result = template % kwargs
-    return result.encode('utf-8')
-
-
-_ie_friendly_error_sizes = {
-    400: 512, 403: 256, 404: 512, 405: 256,
-    406: 512, 408: 512, 409: 512, 410: 256,
-    500: 512, 501: 512, 505: 512,
-}
-
-
-def _be_ie_unfriendly(status):
-    response = cherrypy.serving.response
-
-    # For some statuses, Internet Explorer 5+ shows "friendly error
-    # messages" instead of our response.body if the body is smaller
-    # than a given size. Fix this by returning a body over that size
-    # (by adding whitespace).
-    # See http://support.microsoft.com/kb/q218155/
-    s = _ie_friendly_error_sizes.get(status, 0)
-    if s:
-        s += 1
-        # Since we are issuing an HTTP error status, we assume that
-        # the entity is short, and we should just collapse it.
-        content = response.collapse_body()
-        content_length = len(content)
-        if content_length and content_length < s:
-            # IN ADDITION: the response must be written to IE
-            # in one chunk or it will still get replaced! Bah.
-            content = content + (b' ' * (s - content_length))
-        response.body = content
-        response.headers['Content-Length'] = str(len(content))
-
-
-def format_exc(exc=None):
-    """Return exc (or sys.exc_info if None), formatted."""
-    try:
-        if exc is None:
-            exc = _exc_info()
-        if exc == (None, None, None):
-            return ''
-        import traceback
-        return ''.join(traceback.format_exception(*exc))
-    finally:
-        del exc
-
-
-def bare_error(extrabody=None):
-    """Produce status, headers, body for a critical error.
-
-    Returns a triple without calling any other questionable functions,
-    so it should be as error-free as possible. Call it from an HTTP server
-    if you get errors outside of the request.
-
-    If extrabody is None, a friendly but rather unhelpful error message
-    is set in the body. If extrabody is a string, it will be appended
-    as-is to the body.
-    """
-
-    # The whole point of this function is to be a last line-of-defense
-    # in handling errors. That is, it must not raise any errors itself;
-    # it cannot be allowed to fail. Therefore, don't add to it!
-    # In particular, don't call any other CP functions.
-
-    body = b'Unrecoverable error in the server.'
-    if extrabody is not None:
-        if not isinstance(extrabody, bytes):
-            extrabody = extrabody.encode('utf-8')
-        body += b'\n' + extrabody
-
-    return (b'500 Internal Server Error',
-            [(b'Content-Type', b'text/plain'),
-             (b'Content-Length', ntob(str(len(body)), 'ISO-8859-1'))],
-            [body])
diff --git a/libraries/cherrypy/_cplogging.py b/libraries/cherrypy/_cplogging.py
deleted file mode 100644
index 53b9addb..00000000
--- a/libraries/cherrypy/_cplogging.py
+++ /dev/null
@@ -1,482 +0,0 @@
-"""
-Simple config
-=============
-
-Although CherryPy uses the :mod:`Python logging module <logging>`, it does so
-behind the scenes so that simple logging is simple, but complicated logging
-is still possible. "Simple" logging means that you can log to the screen
-(i.e. console/stdout) or to a file, and that you can easily have separate
-error and access log files.
-
-Here are the simplified logging settings. You use these by adding lines to
-your config file or dict. You should set these at either the global level or
-per application (see next), but generally not both.
-
- * ``log.screen``: Set this to True to have both "error" and "access" messages
-   printed to stdout.
- * ``log.access_file``: Set this to an absolute filename where you want
-   "access" messages written.
- * ``log.error_file``: Set this to an absolute filename where you want "error"
-   messages written.
-
-Many events are automatically logged; to log your own application events, call
-:func:`cherrypy.log`.
-
-Architecture
-============
-
-Separate scopes
----------------
-
-CherryPy provides log managers at both the global and application layers.
-This means you can have one set of logging rules for your entire site,
-and another set of rules specific to each application. The global log
-manager is found at :func:`cherrypy.log`, and the log manager for each
-application is found at :attr:`app.log<cherrypy._cptree.Application.log>`.
-If you're inside a request, the latter is reachable from
-``cherrypy.request.app.log``; if you're outside a request, you'll have to
-obtain a reference to the ``app``: either the return value of
-:func:`tree.mount()<cherrypy._cptree.Tree.mount>` or, if you used
-:func:`quickstart()<cherrypy.quickstart>` instead, via
-``cherrypy.tree.apps['/']``.
-
-By default, the global logs are named "cherrypy.error" and "cherrypy.access",
-and the application logs are named "cherrypy.error.2378745" and
-"cherrypy.access.2378745" (the number is the id of the Application object).
-This means that the application logs "bubble up" to the site logs, so if your
-application has no log handlers, the site-level handlers will still log the
-messages.
-
-Errors vs. Access
------------------
-
-Each log manager handles both "access" messages (one per HTTP request) and
-"error" messages (everything else). Note that the "error" log is not just for
-errors! The format of access messages is highly formalized, but the error log
-isn't--it receives messages from a variety of sources (including full error
-tracebacks, if enabled).
-
-If you are logging the access log and error log to the same source, then there
-is a possibility that a specially crafted error message may replicate an access
-log message as described in CWE-117.  In this case it is the application
-developer's responsibility to manually escape data before
-using CherryPy's log()
-functionality, or they may create an application that is vulnerable to CWE-117.
-This would be achieved by using a custom handler escape any special characters,
-and attached as described below.
-
-Custom Handlers
-===============
-
-The simple settings above work by manipulating Python's standard :mod:`logging`
-module. So when you need something more complex, the full power of the standard
-module is yours to exploit. You can borrow or create custom handlers, formats,
-filters, and much more. Here's an example that skips the standard FileHandler
-and uses a RotatingFileHandler instead:
-
-::
-
-    #python
-    log = app.log
-
-    # Remove the default FileHandlers if present.
-    log.error_file = ""
-    log.access_file = ""
-
-    maxBytes = getattr(log, "rot_maxBytes", 10000000)
-    backupCount = getattr(log, "rot_backupCount", 1000)
-
-    # Make a new RotatingFileHandler for the error log.
-    fname = getattr(log, "rot_error_file", "error.log")
-    h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
-    h.setLevel(DEBUG)
-    h.setFormatter(_cplogging.logfmt)
-    log.error_log.addHandler(h)
-
-    # Make a new RotatingFileHandler for the access log.
-    fname = getattr(log, "rot_access_file", "access.log")
-    h = handlers.RotatingFileHandler(fname, 'a', maxBytes, backupCount)
-    h.setLevel(DEBUG)
-    h.setFormatter(_cplogging.logfmt)
-    log.access_log.addHandler(h)
-
-
-The ``rot_*`` attributes are pulled straight from the application log object.
-Since "log.*" config entries simply set attributes on the log object, you can
-add custom attributes to your heart's content. Note that these handlers are
-used ''instead'' of the default, simple handlers outlined above (so don't set
-the "log.error_file" config entry, for example).
-"""
-
-import datetime
-import logging
-import os
-import sys
-
-import six
-
-import cherrypy
-from cherrypy import _cperror
-
-
-# Silence the no-handlers "warning" (stderr write!) in stdlib logging
-logging.Logger.manager.emittedNoHandlerWarning = 1
-logfmt = logging.Formatter('%(message)s')
-
-
-class NullHandler(logging.Handler):
-
-    """A no-op logging handler to silence the logging.lastResort handler."""
-
-    def handle(self, record):
-        pass
-
-    def emit(self, record):
-        pass
-
-    def createLock(self):
-        self.lock = None
-
-
-class LogManager(object):
-
-    """An object to assist both simple and advanced logging.
-
-    ``cherrypy.log`` is an instance of this class.
-    """
-
-    appid = None
-    """The id() of the Application object which owns this log manager. If this
-    is a global log manager, appid is None."""
-
-    error_log = None
-    """The actual :class:`logging.Logger` instance for error messages."""
-
-    access_log = None
-    """The actual :class:`logging.Logger` instance for access messages."""
-
-    access_log_format = (
-        '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}"'
-        if six.PY3 else
-        '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'
-    )
-
-    logger_root = None
-    """The "top-level" logger name.
-
-    This string will be used as the first segment in the Logger names.
-    The default is "cherrypy", for example, in which case the Logger names
-    will be of the form::
-
-        cherrypy.error.<appid>
-        cherrypy.access.<appid>
-    """
-
-    def __init__(self, appid=None, logger_root='cherrypy'):
-        self.logger_root = logger_root
-        self.appid = appid
-        if appid is None:
-            self.error_log = logging.getLogger('%s.error' % logger_root)
-            self.access_log = logging.getLogger('%s.access' % logger_root)
-        else:
-            self.error_log = logging.getLogger(
-                '%s.error.%s' % (logger_root, appid))
-            self.access_log = logging.getLogger(
-                '%s.access.%s' % (logger_root, appid))
-        self.error_log.setLevel(logging.INFO)
-        self.access_log.setLevel(logging.INFO)
-
-        # Silence the no-handlers "warning" (stderr write!) in stdlib logging
-        self.error_log.addHandler(NullHandler())
-        self.access_log.addHandler(NullHandler())
-
-        cherrypy.engine.subscribe('graceful', self.reopen_files)
-
-    def reopen_files(self):
-        """Close and reopen all file handlers."""
-        for log in (self.error_log, self.access_log):
-            for h in log.handlers:
-                if isinstance(h, logging.FileHandler):
-                    h.acquire()
-                    h.stream.close()
-                    h.stream = open(h.baseFilename, h.mode)
-                    h.release()
-
-    def error(self, msg='', context='', severity=logging.INFO,
-              traceback=False):
-        """Write the given ``msg`` to the error log.
-
-        This is not just for errors! Applications may call this at any time
-        to log application-specific information.
-
-        If ``traceback`` is True, the traceback of the current exception
-        (if any) will be appended to ``msg``.
-        """
-        exc_info = None
-        if traceback:
-            exc_info = _cperror._exc_info()
-
-        self.error_log.log(
-            severity,
-            ' '.join((self.time(), context, msg)),
-            exc_info=exc_info,
-        )
-
-    def __call__(self, *args, **kwargs):
-        """An alias for ``error``."""
-        return self.error(*args, **kwargs)
-
-    def access(self):
-        """Write to the access log (in Apache/NCSA Combined Log format).
-
-        See the
-        `apache documentation
-        <http://httpd.apache.org/docs/current/logs.html#combined>`_
-        for format details.
-
-        CherryPy calls this automatically for you. Note there are no arguments;
-        it collects the data itself from
-        :class:`cherrypy.request<cherrypy._cprequest.Request>`.
-
-        Like Apache started doing in 2.0.46, non-printable and other special
-        characters in %r (and we expand that to all parts) are escaped using
-        \\xhh sequences, where hh stands for the hexadecimal representation
-        of the raw byte. Exceptions from this rule are " and \\, which are
-        escaped by prepending a backslash, and all whitespace characters,
-        which are written in their C-style notation (\\n, \\t, etc).
-        """
-        request = cherrypy.serving.request
-        remote = request.remote
-        response = cherrypy.serving.response
-        outheaders = response.headers
-        inheaders = request.headers
-        if response.output_status is None:
-            status = '-'
-        else:
-            status = response.output_status.split(b' ', 1)[0]
-            if six.PY3:
-                status = status.decode('ISO-8859-1')
-
-        atoms = {'h': remote.name or remote.ip,
-                 'l': '-',
-                 'u': getattr(request, 'login', None) or '-',
-                 't': self.time(),
-                 'r': request.request_line,
-                 's': status,
-                 'b': dict.get(outheaders, 'Content-Length', '') or '-',
-                 'f': dict.get(inheaders, 'Referer', ''),
-                 'a': dict.get(inheaders, 'User-Agent', ''),
-                 'o': dict.get(inheaders, 'Host', '-'),
-                 'i': request.unique_id,
-                 'z': LazyRfc3339UtcTime(),
-                 }
-        if six.PY3:
-            for k, v in atoms.items():
-                if not isinstance(v, str):
-                    v = str(v)
-                v = v.replace('"', '\\"').encode('utf8')
-                # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
-                # and backslash for us. All we have to do is strip the quotes.
-                v = repr(v)[2:-1]
-
-                # in python 3.0 the repr of bytes (as returned by encode)
-                # uses double \'s.  But then the logger escapes them yet, again
-                # resulting in quadruple slashes.  Remove the extra one here.
-                v = v.replace('\\\\', '\\')
-
-                # Escape double-quote.
-                atoms[k] = v
-
-            try:
-                self.access_log.log(
-                    logging.INFO, self.access_log_format.format(**atoms))
-            except Exception:
-                self(traceback=True)
-        else:
-            for k, v in atoms.items():
-                if isinstance(v, six.text_type):
-                    v = v.encode('utf8')
-                elif not isinstance(v, str):
-                    v = str(v)
-                # Fortunately, repr(str) escapes unprintable chars, \n, \t, etc
-                # and backslash for us. All we have to do is strip the quotes.
-                v = repr(v)[1:-1]
-                # Escape double-quote.
-                atoms[k] = v.replace('"', '\\"')
-
-            try:
-                self.access_log.log(
-                    logging.INFO, self.access_log_format % atoms)
-            except Exception:
-                self(traceback=True)
-
-    def time(self):
-        """Return now() in Apache Common Log Format (no timezone)."""
-        now = datetime.datetime.now()
-        monthnames = ['jan', 'feb', 'mar', 'apr', 'may', 'jun',
-                      'jul', 'aug', 'sep', 'oct', 'nov', 'dec']
-        month = monthnames[now.month - 1].capitalize()
-        return ('[%02d/%s/%04d:%02d:%02d:%02d]' %
-                (now.day, month, now.year, now.hour, now.minute, now.second))
-
-    def _get_builtin_handler(self, log, key):
-        for h in log.handlers:
-            if getattr(h, '_cpbuiltin', None) == key:
-                return h
-
-    # ------------------------- Screen handlers ------------------------- #
-    def _set_screen_handler(self, log, enable, stream=None):
-        h = self._get_builtin_handler(log, 'screen')
-        if enable:
-            if not h:
-                if stream is None:
-                    stream = sys.stderr
-                h = logging.StreamHandler(stream)
-                h.setFormatter(logfmt)
-                h._cpbuiltin = 'screen'
-                log.addHandler(h)
-        elif h:
-            log.handlers.remove(h)
-
-    @property
-    def screen(self):
-        """Turn stderr/stdout logging on or off.
-
-        If you set this to True, it'll add the appropriate StreamHandler for
-        you. If you set it to False, it will remove the handler.
-        """
-        h = self._get_builtin_handler
-        has_h = h(self.error_log, 'screen') or h(self.access_log, 'screen')
-        return bool(has_h)
-
-    @screen.setter
-    def screen(self, newvalue):
-        self._set_screen_handler(self.error_log, newvalue, stream=sys.stderr)
-        self._set_screen_handler(self.access_log, newvalue, stream=sys.stdout)
-
-    # -------------------------- File handlers -------------------------- #
-
-    def _add_builtin_file_handler(self, log, fname):
-        h = logging.FileHandler(fname)
-        h.setFormatter(logfmt)
-        h._cpbuiltin = 'file'
-        log.addHandler(h)
-
-    def _set_file_handler(self, log, filename):
-        h = self._get_builtin_handler(log, 'file')
-        if filename:
-            if h:
-                if h.baseFilename != os.path.abspath(filename):
-                    h.close()
-                    log.handlers.remove(h)
-                    self._add_builtin_file_handler(log, filename)
-            else:
-                self._add_builtin_file_handler(log, filename)
-        else:
-            if h:
-                h.close()
-                log.handlers.remove(h)
-
-    @property
-    def error_file(self):
-        """The filename for self.error_log.
-
-        If you set this to a string, it'll add the appropriate FileHandler for
-        you. If you set it to ``None`` or ``''``, it will remove the handler.
-        """
-        h = self._get_builtin_handler(self.error_log, 'file')
-        if h:
-            return h.baseFilename
-        return ''
-
-    @error_file.setter
-    def error_file(self, newvalue):
-        self._set_file_handler(self.error_log, newvalue)
-
-    @property
-    def access_file(self):
-        """The filename for self.access_log.
-
-        If you set this to a string, it'll add the appropriate FileHandler for
-        you. If you set it to ``None`` or ``''``, it will remove the handler.
-        """
-        h = self._get_builtin_handler(self.access_log, 'file')
-        if h:
-            return h.baseFilename
-        return ''
-
-    @access_file.setter
-    def access_file(self, newvalue):
-        self._set_file_handler(self.access_log, newvalue)
-
-    # ------------------------- WSGI handlers ------------------------- #
-
-    def _set_wsgi_handler(self, log, enable):
-        h = self._get_builtin_handler(log, 'wsgi')
-        if enable:
-            if not h:
-                h = WSGIErrorHandler()
-                h.setFormatter(logfmt)
-                h._cpbuiltin = 'wsgi'
-                log.addHandler(h)
-        elif h:
-            log.handlers.remove(h)
-
-    @property
-    def wsgi(self):
-        """Write errors to wsgi.errors.
-
-        If you set this to True, it'll add the appropriate
-        :class:`WSGIErrorHandler<cherrypy._cplogging.WSGIErrorHandler>` for you
-        (which writes errors to ``wsgi.errors``).
-        If you set it to False, it will remove the handler.
-        """
-        return bool(self._get_builtin_handler(self.error_log, 'wsgi'))
-
-    @wsgi.setter
-    def wsgi(self, newvalue):
-        self._set_wsgi_handler(self.error_log, newvalue)
-
-
-class WSGIErrorHandler(logging.Handler):
-
-    "A handler class which writes logging records to environ['wsgi.errors']."
-
-    def flush(self):
-        """Flushes the stream."""
-        try:
-            stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
-        except (AttributeError, KeyError):
-            pass
-        else:
-            stream.flush()
-
-    def emit(self, record):
-        """Emit a record."""
-        try:
-            stream = cherrypy.serving.request.wsgi_environ.get('wsgi.errors')
-        except (AttributeError, KeyError):
-            pass
-        else:
-            try:
-                msg = self.format(record)
-                fs = '%s\n'
-                import types
-                # if no unicode support...
-                if not hasattr(types, 'UnicodeType'):
-                    stream.write(fs % msg)
-                else:
-                    try:
-                        stream.write(fs % msg)
-                    except UnicodeError:
-                        stream.write(fs % msg.encode('UTF-8'))
-                self.flush()
-            except Exception:
-                self.handleError(record)
-
-
-class LazyRfc3339UtcTime(object):
-    def __str__(self):
-        """Return now() in RFC3339 UTC Format."""
-        now = datetime.datetime.now()
-        return now.isoformat('T') + 'Z'
diff --git a/libraries/cherrypy/_cpmodpy.py b/libraries/cherrypy/_cpmodpy.py
deleted file mode 100644
index ac91e625..00000000
--- a/libraries/cherrypy/_cpmodpy.py
+++ /dev/null
@@ -1,356 +0,0 @@
-"""Native adapter for serving CherryPy via mod_python
-
-Basic usage:
-
-##########################################
-# Application in a module called myapp.py
-##########################################
-
-import cherrypy
-
-class Root:
-    @cherrypy.expose
-    def index(self):
-        return 'Hi there, Ho there, Hey there'
-
-
-# We will use this method from the mod_python configuration
-# as the entry point to our application
-def setup_server():
-    cherrypy.tree.mount(Root())
-    cherrypy.config.update({'environment': 'production',
-                            'log.screen': False,
-                            'show_tracebacks': False})
-
-##########################################
-# mod_python settings for apache2
-# This should reside in your httpd.conf
-# or a file that will be loaded at
-# apache startup
-##########################################
-
-# Start
-DocumentRoot "/"
-Listen 8080
-LoadModule python_module /usr/lib/apache2/modules/mod_python.so
-
-<Location "/">
-    PythonPath "sys.path+['/path/to/my/application']"
-    SetHandler python-program
-    PythonHandler cherrypy._cpmodpy::handler
-    PythonOption cherrypy.setup myapp::setup_server
-    PythonDebug On
-</Location>
-# End
-
-The actual path to your mod_python.so is dependent on your
-environment. In this case we suppose a global mod_python
-installation on a Linux distribution such as Ubuntu.
-
-We do set the PythonPath configuration setting so that
-your application can be found by from the user running
-the apache2 instance. Of course if your application
-resides in the global site-package this won't be needed.
-
-Then restart apache2 and access http://127.0.0.1:8080
-"""
-
-import io
-import logging
-import os
-import re
-import sys
-
-import six
-
-from more_itertools import always_iterable
-
-import cherrypy
-from cherrypy._cperror import format_exc, bare_error
-from cherrypy.lib import httputil
-
-
-# ------------------------------ Request-handling
-
-
-def setup(req):
-    from mod_python import apache
-
-    # Run any setup functions defined by a "PythonOption cherrypy.setup"
-    # directive.
-    options = req.get_options()
-    if 'cherrypy.setup' in options:
-        for function in options['cherrypy.setup'].split():
-            atoms = function.split('::', 1)
-            if len(atoms) == 1:
-                mod = __import__(atoms[0], globals(), locals())
-            else:
-                modname, fname = atoms
-                mod = __import__(modname, globals(), locals(), [fname])
-                func = getattr(mod, fname)
-                func()
-
-    cherrypy.config.update({'log.screen': False,
-                            'tools.ignore_headers.on': True,
-                            'tools.ignore_headers.headers': ['Range'],
-                            })
-
-    engine = cherrypy.engine
-    if hasattr(engine, 'signal_handler'):
-        engine.signal_handler.unsubscribe()
-    if hasattr(engine, 'console_control_handler'):
-        engine.console_control_handler.unsubscribe()
-    engine.autoreload.unsubscribe()
-    cherrypy.server.unsubscribe()
-
-    @engine.subscribe('log')
-    def _log(msg, level):
-        newlevel = apache.APLOG_ERR
-        if logging.DEBUG >= level:
-            newlevel = apache.APLOG_DEBUG
-        elif logging.INFO >= level:
-            newlevel = apache.APLOG_INFO
-        elif logging.WARNING >= level:
-            newlevel = apache.APLOG_WARNING
-        # On Windows, req.server is required or the msg will vanish. See
-        # http://www.modpython.org/pipermail/mod_python/2003-October/014291.html
-        # Also, "When server is not specified...LogLevel does not apply..."
-        apache.log_error(msg, newlevel, req.server)
-
-    engine.start()
-
-    def cherrypy_cleanup(data):
-        engine.exit()
-    try:
-        # apache.register_cleanup wasn't available until 3.1.4.
-        apache.register_cleanup(cherrypy_cleanup)
-    except AttributeError:
-        req.server.register_cleanup(req, cherrypy_cleanup)
-
-
-class _ReadOnlyRequest:
-    expose = ('read', 'readline', 'readlines')
-
-    def __init__(self, req):
-        for method in self.expose:
-            self.__dict__[method] = getattr(req, method)
-
-
-recursive = False
-
-_isSetUp = False
-
-
-def handler(req):
-    from mod_python import apache
-    try:
-        global _isSetUp
-        if not _isSetUp:
-            setup(req)
-            _isSetUp = True
-
-        # Obtain a Request object from CherryPy
-        local = req.connection.local_addr
-        local = httputil.Host(
-            local[0], local[1], req.connection.local_host or '')
-        remote = req.connection.remote_addr
-        remote = httputil.Host(
-            remote[0], remote[1], req.connection.remote_host or '')
-
-        scheme = req.parsed_uri[0] or 'http'
-        req.get_basic_auth_pw()
-
-        try:
-            # apache.mpm_query only became available in mod_python 3.1
-            q = apache.mpm_query
-            threaded = q(apache.AP_MPMQ_IS_THREADED)
-            forked = q(apache.AP_MPMQ_IS_FORKED)
-        except AttributeError:
-            bad_value = ("You must provide a PythonOption '%s', "
-                         "either 'on' or 'off', when running a version "
-                         'of mod_python < 3.1')
-
-            options = req.get_options()
-
-            threaded = options.get('multithread', '').lower()
-            if threaded == 'on':
-                threaded = True
-            elif threaded == 'off':
-                threaded = False
-            else:
-                raise ValueError(bad_value % 'multithread')
-
-            forked = options.get('multiprocess', '').lower()
-            if forked == 'on':
-                forked = True
-            elif forked == 'off':
-                forked = False
-            else:
-                raise ValueError(bad_value % 'multiprocess')
-
-        sn = cherrypy.tree.script_name(req.uri or '/')
-        if sn is None:
-            send_response(req, '404 Not Found', [], '')
-        else:
-            app = cherrypy.tree.apps[sn]
-            method = req.method
-            path = req.uri
-            qs = req.args or ''
-            reqproto = req.protocol
-            headers = list(six.iteritems(req.headers_in))
-            rfile = _ReadOnlyRequest(req)
-            prev = None
-
-            try:
-                redirections = []
-                while True:
-                    request, response = app.get_serving(local, remote, scheme,
-                                                        'HTTP/1.1')
-                    request.login = req.user
-                    request.multithread = bool(threaded)
-                    request.multiprocess = bool(forked)
-                    request.app = app
-                    request.prev = prev
-
-                    # Run the CherryPy Request object and obtain the response
-                    try:
-                        request.run(method, path, qs, reqproto, headers, rfile)
-                        break
-                    except cherrypy.InternalRedirect:
-                        ir = sys.exc_info()[1]
-                        app.release_serving()
-                        prev = request
-
-                        if not recursive:
-                            if ir.path in redirections:
-                                raise RuntimeError(
-                                    'InternalRedirector visited the same URL '
-                                    'twice: %r' % ir.path)
-                            else:
-                                # Add the *previous* path_info + qs to
-                                # redirections.
-                                if qs:
-                                    qs = '?' + qs
-                                redirections.append(sn + path + qs)
-
-                        # Munge environment and try again.
-                        method = 'GET'
-                        path = ir.path
-                        qs = ir.query_string
-                        rfile = io.BytesIO()
-
-                send_response(
-                    req, response.output_status, response.header_list,
-                    response.body, response.stream)
-            finally:
-                app.release_serving()
-    except Exception:
-        tb = format_exc()
-        cherrypy.log(tb, 'MOD_PYTHON', severity=logging.ERROR)
-        s, h, b = bare_error()
-        send_response(req, s, h, b)
-    return apache.OK
-
-
-def send_response(req, status, headers, body, stream=False):
-    # Set response status
-    req.status = int(status[:3])
-
-    # Set response headers
-    req.content_type = 'text/plain'
-    for header, value in headers:
-        if header.lower() == 'content-type':
-            req.content_type = value
-            continue
-        req.headers_out.add(header, value)
-
-    if stream:
-        # Flush now so the status and headers are sent immediately.
-        req.flush()
-
-    # Set response body
-    for seg in always_iterable(body):
-        req.write(seg)
-
-
-# --------------- Startup tools for CherryPy + mod_python --------------- #
-try:
-    import subprocess
-
-    def popen(fullcmd):
-        p = subprocess.Popen(fullcmd, shell=True,
-                             stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
-                             close_fds=True)
-        return p.stdout
-except ImportError:
-    def popen(fullcmd):
-        pipein, pipeout = os.popen4(fullcmd)
-        return pipeout
-
-
-def read_process(cmd, args=''):
-    fullcmd = '%s %s' % (cmd, args)
-    pipeout = popen(fullcmd)
-    try:
-        firstline = pipeout.readline()
-        cmd_not_found = re.search(
-            b'(not recognized|No such file|not found)',
-            firstline,
-            re.IGNORECASE
-        )
-        if cmd_not_found:
-            raise IOError('%s must be on your system path.' % cmd)
-        output = firstline + pipeout.read()
-    finally:
-        pipeout.close()
-    return output
-
-
-class ModPythonServer(object):
-
-    template = """
-# Apache2 server configuration file for running CherryPy with mod_python.
-
-DocumentRoot "/"
-Listen %(port)s
-LoadModule python_module modules/mod_python.so
-
-<Location %(loc)s>
-    SetHandler python-program
-    PythonHandler %(handler)s
-    PythonDebug On
-%(opts)s
-</Location>
-"""
-
-    def __init__(self, loc='/', port=80, opts=None, apache_path='apache',
-                 handler='cherrypy._cpmodpy::handler'):
-        self.loc = loc
-        self.port = port
-        self.opts = opts
-        self.apache_path = apache_path
-        self.handler = handler
-
-    def start(self):
-        opts = ''.join(['    PythonOption %s %s\n' % (k, v)
-                        for k, v in self.opts])
-        conf_data = self.template % {'port': self.port,
-                                     'loc': self.loc,
-                                     'opts': opts,
-                                     'handler': self.handler,
-                                     }
-
-        mpconf = os.path.join(os.path.dirname(__file__), 'cpmodpy.conf')
-        f = open(mpconf, 'wb')
-        try:
-            f.write(conf_data)
-        finally:
-            f.close()
-
-        response = read_process(self.apache_path, '-k start -f %s' % mpconf)
-        self.ready = True
-        return response
-
-    def stop(self):
-        os.popen('apache -k stop')
-        self.ready = False
diff --git a/libraries/cherrypy/_cpnative_server.py b/libraries/cherrypy/_cpnative_server.py
deleted file mode 100644
index 55653c35..00000000
--- a/libraries/cherrypy/_cpnative_server.py
+++ /dev/null
@@ -1,160 +0,0 @@
-"""Native adapter for serving CherryPy via its builtin server."""
-
-import logging
-import sys
-import io
-
-import cheroot.server
-
-import cherrypy
-from cherrypy._cperror import format_exc, bare_error
-from cherrypy.lib import httputil
-
-
-class NativeGateway(cheroot.server.Gateway):
-    """Native gateway implementation allowing to bypass WSGI."""
-
-    recursive = False
-
-    def respond(self):
-        """Obtain response from CherryPy machinery and then send it."""
-        req = self.req
-        try:
-            # Obtain a Request object from CherryPy
-            local = req.server.bind_addr
-            local = httputil.Host(local[0], local[1], '')
-            remote = req.conn.remote_addr, req.conn.remote_port
-            remote = httputil.Host(remote[0], remote[1], '')
-
-            scheme = req.scheme
-            sn = cherrypy.tree.script_name(req.uri or '/')
-            if sn is None:
-                self.send_response('404 Not Found', [], [''])
-            else:
-                app = cherrypy.tree.apps[sn]
-                method = req.method
-                path = req.path
-                qs = req.qs or ''
-                headers = req.inheaders.items()
-                rfile = req.rfile
-                prev = None
-
-                try:
-                    redirections = []
-                    while True:
-                        request, response = app.get_serving(
-                            local, remote, scheme, 'HTTP/1.1')
-                        request.multithread = True
-                        request.multiprocess = False
-                        request.app = app
-                        request.prev = prev
-
-                        # Run the CherryPy Request object and obtain the
-                        # response
-                        try:
-                            request.run(method, path, qs,
-                                        req.request_protocol, headers, rfile)
-                            break
-                        except cherrypy.InternalRedirect:
-                            ir = sys.exc_info()[1]
-                            app.release_serving()
-                            prev = request
-
-                            if not self.recursive:
-                                if ir.path in redirections:
-                                    raise RuntimeError(
-                                        'InternalRedirector visited the same '
-                                        'URL twice: %r' % ir.path)
-                                else:
-                                    # Add the *previous* path_info + qs to
-                                    # redirections.
-                                    if qs:
-                                        qs = '?' + qs
-                                    redirections.append(sn + path + qs)
-
-                            # Munge environment and try again.
-                            method = 'GET'
-                            path = ir.path
-                            qs = ir.query_string
-                            rfile = io.BytesIO()
-
-                    self.send_response(
-                        response.output_status, response.header_list,
-                        response.body)
-                finally:
-                    app.release_serving()
-        except Exception:
-            tb = format_exc()
-            # print tb
-            cherrypy.log(tb, 'NATIVE_ADAPTER', severity=logging.ERROR)
-            s, h, b = bare_error()
-            self.send_response(s, h, b)
-
-    def send_response(self, status, headers, body):
-        """Send response to HTTP request."""
-        req = self.req
-
-        # Set response status
-        req.status = status or b'500 Server Error'
-
-        # Set response headers
-        for header, value in headers:
-            req.outheaders.append((header, value))
-        if (req.ready and not req.sent_headers):
-            req.sent_headers = True
-            req.send_headers()
-
-        # Set response body
-        for seg in body:
-            req.write(seg)
-
-
-class CPHTTPServer(cheroot.server.HTTPServer):
-    """Wrapper for cheroot.server.HTTPServer.
-
-    cheroot has been designed to not reference CherryPy in any way,
-    so that it can be used in other frameworks and applications.
-    Therefore, we wrap it here, so we can apply some attributes
-    from config -> cherrypy.server -> HTTPServer.
-    """
-
-    def __init__(self, server_adapter=cherrypy.server):
-        """Initialize CPHTTPServer."""
-        self.server_adapter = server_adapter
-
-        server_name = (self.server_adapter.socket_host or
-                       self.server_adapter.socket_file or
-                       None)
-
-        cheroot.server.HTTPServer.__init__(
-            self, server_adapter.bind_addr, NativeGateway,
-            minthreads=server_adapter.thread_pool,
-            maxthreads=server_adapter.thread_pool_max,
-            server_name=server_name)
-
-        self.max_request_header_size = (
-            self.server_adapter.max_request_header_size or 0)
-        self.max_request_body_size = (
-            self.server_adapter.max_request_body_size or 0)
-        self.request_queue_size = self.server_adapter.socket_queue_size
-        self.timeout = self.server_adapter.socket_timeout
-        self.shutdown_timeout = self.server_adapter.shutdown_timeout
-        self.protocol = self.server_adapter.protocol_version
-        self.nodelay = self.server_adapter.nodelay
-
-        ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
-        if self.server_adapter.ssl_context:
-            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
-            self.ssl_adapter = adapter_class(
-                self.server_adapter.ssl_certificate,
-                self.server_adapter.ssl_private_key,
-                self.server_adapter.ssl_certificate_chain,
-                self.server_adapter.ssl_ciphers)
-            self.ssl_adapter.context = self.server_adapter.ssl_context
-        elif self.server_adapter.ssl_certificate:
-            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
-            self.ssl_adapter = adapter_class(
-                self.server_adapter.ssl_certificate,
-                self.server_adapter.ssl_private_key,
-                self.server_adapter.ssl_certificate_chain,
-                self.server_adapter.ssl_ciphers)
diff --git a/libraries/cherrypy/_cpreqbody.py b/libraries/cherrypy/_cpreqbody.py
deleted file mode 100644
index 893fe5f5..00000000
--- a/libraries/cherrypy/_cpreqbody.py
+++ /dev/null
@@ -1,1000 +0,0 @@
-"""Request body processing for CherryPy.
-
-.. versionadded:: 3.2
-
-Application authors have complete control over the parsing of HTTP request
-entities. In short,
-:attr:`cherrypy.request.body<cherrypy._cprequest.Request.body>`
-is now always set to an instance of
-:class:`RequestBody<cherrypy._cpreqbody.RequestBody>`,
-and *that* class is a subclass of :class:`Entity<cherrypy._cpreqbody.Entity>`.
-
-When an HTTP request includes an entity body, it is often desirable to
-provide that information to applications in a form other than the raw bytes.
-Different content types demand different approaches. Examples:
-
- * For a GIF file, we want the raw bytes in a stream.
- * An HTML form is better parsed into its component fields, and each text field
-   decoded from bytes to unicode.
- * A JSON body should be deserialized into a Python dict or list.
-
-When the request contains a Content-Type header, the media type is used as a
-key to look up a value in the
-:attr:`request.body.processors<cherrypy._cpreqbody.Entity.processors>` dict.
-If the full media
-type is not found, then the major type is tried; for example, if no processor
-is found for the 'image/jpeg' type, then we look for a processor for the
-'image' types altogether. If neither the full type nor the major type has a
-matching processor, then a default processor is used
-(:func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>`). For most
-types, this means no processing is done, and the body is left unread as a
-raw byte stream. Processors are configurable in an 'on_start_resource' hook.
-
-Some processors, especially those for the 'text' types, attempt to decode bytes
-to unicode. If the Content-Type request header includes a 'charset' parameter,
-this is used to decode the entity. Otherwise, one or more default charsets may
-be attempted, although this decision is up to each processor. If a processor
-successfully decodes an Entity or Part, it should set the
-:attr:`charset<cherrypy._cpreqbody.Entity.charset>` attribute
-on the Entity or Part to the name of the successful charset, so that
-applications can easily re-encode or transcode the value if they wish.
-
-If the Content-Type of the request entity is of major type 'multipart', then
-the above parsing process, and possibly a decoding process, is performed for
-each part.
-
-For both the full entity and multipart parts, a Content-Disposition header may
-be used to fill :attr:`name<cherrypy._cpreqbody.Entity.name>` and
-:attr:`filename<cherrypy._cpreqbody.Entity.filename>` attributes on the
-request.body or the Part.
-
-.. _custombodyprocessors:
-
-Custom Processors
-=================
-
-You can add your own processors for any specific or major MIME type. Simply add
-it to the :attr:`processors<cherrypy._cprequest.Entity.processors>` dict in a
-hook/tool that runs at ``on_start_resource`` or ``before_request_body``.
-Here's the built-in JSON tool for an example::
-
-    def json_in(force=True, debug=False):
-        request = cherrypy.serving.request
-        def json_processor(entity):
-            '''Read application/json data into request.json.'''
-            if not entity.headers.get("Content-Length", ""):
-                raise cherrypy.HTTPError(411)
-
-            body = entity.fp.read()
-            try:
-                request.json = json_decode(body)
-            except ValueError:
-                raise cherrypy.HTTPError(400, 'Invalid JSON document')
-        if force:
-            request.body.processors.clear()
-            request.body.default_proc = cherrypy.HTTPError(
-                415, 'Expected an application/json content type')
-        request.body.processors['application/json'] = json_processor
-
-We begin by defining a new ``json_processor`` function to stick in the
-``processors`` dictionary. All processor functions take a single argument,
-the ``Entity`` instance they are to process. It will be called whenever a
-request is received (for those URI's where the tool is turned on) which
-has a ``Content-Type`` of "application/json".
-
-First, it checks for a valid ``Content-Length`` (raising 411 if not valid),
-then reads the remaining bytes on the socket. The ``fp`` object knows its
-own length, so it won't hang waiting for data that never arrives. It will
-return when all data has been read. Then, we decode those bytes using
-Python's built-in ``json`` module, and stick the decoded result onto
-``request.json`` . If it cannot be decoded, we raise 400.
-
-If the "force" argument is True (the default), the ``Tool`` clears the
-``processors`` dict so that request entities of other ``Content-Types``
-aren't parsed at all. Since there's no entry for those invalid MIME
-types, the ``default_proc`` method of ``cherrypy.request.body`` is
-called. But this does nothing by default (usually to provide the page
-handler an opportunity to handle it.)
-But in our case, we want to raise 415, so we replace
-``request.body.default_proc``
-with the error (``HTTPError`` instances, when called, raise themselves).
-
-If we were defining a custom processor, we can do so without making a ``Tool``.
-Just add the config entry::
-
-    request.body.processors = {'application/json': json_processor}
-
-Note that you can only replace the ``processors`` dict wholesale this way,
-not update the existing one.
-"""
-
-try:
-    from io import DEFAULT_BUFFER_SIZE
-except ImportError:
-    DEFAULT_BUFFER_SIZE = 8192
-import re
-import sys
-import tempfile
-try:
-    from urllib import unquote_plus
-except ImportError:
-    def unquote_plus(bs):
-        """Bytes version of urllib.parse.unquote_plus."""
-        bs = bs.replace(b'+', b' ')
-        atoms = bs.split(b'%')
-        for i in range(1, len(atoms)):
-            item = atoms[i]
-            try:
-                pct = int(item[:2], 16)
-                atoms[i] = bytes([pct]) + item[2:]
-            except ValueError:
-                pass
-        return b''.join(atoms)
-
-import six
-import cheroot.server
-
-import cherrypy
-from cherrypy._cpcompat import ntou, unquote
-from cherrypy.lib import httputil
-
-
-# ------------------------------- Processors -------------------------------- #
-
-def process_urlencoded(entity):
-    """Read application/x-www-form-urlencoded data into entity.params."""
-    qs = entity.fp.read()
-    for charset in entity.attempt_charsets:
-        try:
-            params = {}
-            for aparam in qs.split(b'&'):
-                for pair in aparam.split(b';'):
-                    if not pair:
-                        continue
-
-                    atoms = pair.split(b'=', 1)
-                    if len(atoms) == 1:
-                        atoms.append(b'')
-
-                    key = unquote_plus(atoms[0]).decode(charset)
-                    value = unquote_plus(atoms[1]).decode(charset)
-
-                    if key in params:
-                        if not isinstance(params[key], list):
-                            params[key] = [params[key]]
-                        params[key].append(value)
-                    else:
-                        params[key] = value
-        except UnicodeDecodeError:
-            pass
-        else:
-            entity.charset = charset
-            break
-    else:
-        raise cherrypy.HTTPError(
-            400, 'The request entity could not be decoded. The following '
-            'charsets were attempted: %s' % repr(entity.attempt_charsets))
-
-    # Now that all values have been successfully parsed and decoded,
-    # apply them to the entity.params dict.
-    for key, value in params.items():
-        if key in entity.params:
-            if not isinstance(entity.params[key], list):
-                entity.params[key] = [entity.params[key]]
-            entity.params[key].append(value)
-        else:
-            entity.params[key] = value
-
-
-def process_multipart(entity):
-    """Read all multipart parts into entity.parts."""
-    ib = ''
-    if 'boundary' in entity.content_type.params:
-        # http://tools.ietf.org/html/rfc2046#section-5.1.1
-        # "The grammar for parameters on the Content-type field is such that it
-        # is often necessary to enclose the boundary parameter values in quotes
-        # on the Content-type line"
-        ib = entity.content_type.params['boundary'].strip('"')
-
-    if not re.match('^[ -~]{0,200}[!-~]$', ib):
-        raise ValueError('Invalid boundary in multipart form: %r' % (ib,))
-
-    ib = ('--' + ib).encode('ascii')
-
-    # Find the first marker
-    while True:
-        b = entity.readline()
-        if not b:
-            return
-
-        b = b.strip()
-        if b == ib:
-            break
-
-    # Read all parts
-    while True:
-        part = entity.part_class.from_fp(entity.fp, ib)
-        entity.parts.append(part)
-        part.process()
-        if part.fp.done:
-            break
-
-
-def process_multipart_form_data(entity):
-    """Read all multipart/form-data parts into entity.parts or entity.params.
-    """
-    process_multipart(entity)
-
-    kept_parts = []
-    for part in entity.parts:
-        if part.name is None:
-            kept_parts.append(part)
-        else:
-            if part.filename is None:
-                # It's a regular field
-                value = part.fullvalue()
-            else:
-                # It's a file upload. Retain the whole part so consumer code
-                # has access to its .file and .filename attributes.
-                value = part
-
-            if part.name in entity.params:
-                if not isinstance(entity.params[part.name], list):
-                    entity.params[part.name] = [entity.params[part.name]]
-                entity.params[part.name].append(value)
-            else:
-                entity.params[part.name] = value
-
-    entity.parts = kept_parts
-
-
-def _old_process_multipart(entity):
-    """The behavior of 3.2 and lower. Deprecated and will be changed in 3.3."""
-    process_multipart(entity)
-
-    params = entity.params
-
-    for part in entity.parts:
-        if part.name is None:
-            key = ntou('parts')
-        else:
-            key = part.name
-
-        if part.filename is None:
-            # It's a regular field
-            value = part.fullvalue()
-        else:
-            # It's a file upload. Retain the whole part so consumer code
-            # has access to its .file and .filename attributes.
-            value = part
-
-        if key in params:
-            if not isinstance(params[key], list):
-                params[key] = [params[key]]
-            params[key].append(value)
-        else:
-            params[key] = value
-
-
-# -------------------------------- Entities --------------------------------- #
-class Entity(object):
-
-    """An HTTP request body, or MIME multipart body.
-
-    This class collects information about the HTTP request entity. When a
-    given entity is of MIME type "multipart", each part is parsed into its own
-    Entity instance, and the set of parts stored in
-    :attr:`entity.parts<cherrypy._cpreqbody.Entity.parts>`.
-
-    Between the ``before_request_body`` and ``before_handler`` tools, CherryPy
-    tries to process the request body (if any) by calling
-    :func:`request.body.process<cherrypy._cpreqbody.RequestBody.process>`.
-    This uses the ``content_type`` of the Entity to look up a suitable
-    processor in
-    :attr:`Entity.processors<cherrypy._cpreqbody.Entity.processors>`,
-    a dict.
-    If a matching processor cannot be found for the complete Content-Type,
-    it tries again using the major type. For example, if a request with an
-    entity of type "image/jpeg" arrives, but no processor can be found for
-    that complete type, then one is sought for the major type "image". If a
-    processor is still not found, then the
-    :func:`default_proc<cherrypy._cpreqbody.Entity.default_proc>` method
-    of the Entity is called (which does nothing by default; you can
-    override this too).
-
-    CherryPy includes processors for the "application/x-www-form-urlencoded"
-    type, the "multipart/form-data" type, and the "multipart" major type.
-    CherryPy 3.2 processes these types almost exactly as older versions.
-    Parts are passed as arguments to the page handler using their
-    ``Content-Disposition.name`` if given, otherwise in a generic "parts"
-    argument. Each such part is either a string, or the
-    :class:`Part<cherrypy._cpreqbody.Part>` itself if it's a file. (In this
-    case it will have ``file`` and ``filename`` attributes, or possibly a
-    ``value`` attribute). Each Part is itself a subclass of
-    Entity, and has its own ``process`` method and ``processors`` dict.
-
-    There is a separate processor for the "multipart" major type which is more
-    flexible, and simply stores all multipart parts in
-    :attr:`request.body.parts<cherrypy._cpreqbody.Entity.parts>`. You can
-    enable it with::
-
-        cherrypy.request.body.processors['multipart'] = \
-            _cpreqbody.process_multipart
-
-    in an ``on_start_resource`` tool.
-    """
-
-    # http://tools.ietf.org/html/rfc2046#section-4.1.2:
-    # "The default character set, which must be assumed in the
-    # absence of a charset parameter, is US-ASCII."
-    # However, many browsers send data in utf-8 with no charset.
-    attempt_charsets = ['utf-8']
-    r"""A list of strings, each of which should be a known encoding.
-
-    When the Content-Type of the request body warrants it, each of the given
-    encodings will be tried in order. The first one to successfully decode the
-    entity without raising an error is stored as
-    :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
-    to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
-    `HTTP/1.1
-    <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
-    but ``['us-ascii', 'utf-8']`` for multipart parts.
-    """
-
-    charset = None
-    """The successful decoding; see "attempt_charsets" above."""
-
-    content_type = None
-    """The value of the Content-Type request header.
-
-    If the Entity is part of a multipart payload, this will be the Content-Type
-    given in the MIME headers for this part.
-    """
-
-    default_content_type = 'application/x-www-form-urlencoded'
-    """This defines a default ``Content-Type`` to use if no Content-Type header
-    is given. The empty string is used for RequestBody, which results in the
-    request body not being read or parsed at all. This is by design; a missing
-    ``Content-Type`` header in the HTTP request entity is an error at best,
-    and a security hole at worst. For multipart parts, however, the MIME spec
-    declares that a part with no Content-Type defaults to "text/plain"
-    (see :class:`Part<cherrypy._cpreqbody.Part>`).
-    """
-
-    filename = None
-    """The ``Content-Disposition.filename`` header, if available."""
-
-    fp = None
-    """The readable socket file object."""
-
-    headers = None
-    """A dict of request/multipart header names and values.
-
-    This is a copy of the ``request.headers`` for the ``request.body``;
-    for multipart parts, it is the set of headers for that part.
-    """
-
-    length = None
-    """The value of the ``Content-Length`` header, if provided."""
-
-    name = None
-    """The "name" parameter of the ``Content-Disposition`` header, if any."""
-
-    params = None
-    """
-    If the request Content-Type is 'application/x-www-form-urlencoded' or
-    multipart, this will be a dict of the params pulled from the entity
-    body; that is, it will be the portion of request.params that come
-    from the message body (sometimes called "POST params", although they
-    can be sent with various HTTP method verbs). This value is set between
-    the 'before_request_body' and 'before_handler' hooks (assuming that
-    process_request_body is True)."""
-
-    processors = {'application/x-www-form-urlencoded': process_urlencoded,
-                  'multipart/form-data': process_multipart_form_data,
-                  'multipart': process_multipart,
-                  }
-    """A dict of Content-Type names to processor methods."""
-
-    parts = None
-    """A list of Part instances if ``Content-Type`` is of major type
-    "multipart"."""
-
-    part_class = None
-    """The class used for multipart parts.
-
-    You can replace this with custom subclasses to alter the processing of
-    multipart parts.
-    """
-
-    def __init__(self, fp, headers, params=None, parts=None):
-        # Make an instance-specific copy of the class processors
-        # so Tools, etc. can replace them per-request.
-        self.processors = self.processors.copy()
-
-        self.fp = fp
-        self.headers = headers
-
-        if params is None:
-            params = {}
-        self.params = params
-
-        if parts is None:
-            parts = []
-        self.parts = parts
-
-        # Content-Type
-        self.content_type = headers.elements('Content-Type')
-        if self.content_type:
-            self.content_type = self.content_type[0]
-        else:
-            self.content_type = httputil.HeaderElement.from_str(
-                self.default_content_type)
-
-        # Copy the class 'attempt_charsets', prepending any Content-Type
-        # charset
-        dec = self.content_type.params.get('charset', None)
-        if dec:
-            self.attempt_charsets = [dec] + [c for c in self.attempt_charsets
-                                             if c != dec]
-        else:
-            self.attempt_charsets = self.attempt_charsets[:]
-
-        # Length
-        self.length = None
-        clen = headers.get('Content-Length', None)
-        # If Transfer-Encoding is 'chunked', ignore any Content-Length.
-        if (
-            clen is not None and
-            'chunked' not in headers.get('Transfer-Encoding', '')
-        ):
-            try:
-                self.length = int(clen)
-            except ValueError:
-                pass
-
-        # Content-Disposition
-        self.name = None
-        self.filename = None
-        disp = headers.elements('Content-Disposition')
-        if disp:
-            disp = disp[0]
-            if 'name' in disp.params:
-                self.name = disp.params['name']
-                if self.name.startswith('"') and self.name.endswith('"'):
-                    self.name = self.name[1:-1]
-            if 'filename' in disp.params:
-                self.filename = disp.params['filename']
-                if (
-                    self.filename.startswith('"') and
-                    self.filename.endswith('"')
-                ):
-                    self.filename = self.filename[1:-1]
-            if 'filename*' in disp.params:
-                # @see https://tools.ietf.org/html/rfc5987
-                encoding, lang, filename = disp.params['filename*'].split("'")
-                self.filename = unquote(str(filename), encoding)
-
-    def read(self, size=None, fp_out=None):
-        return self.fp.read(size, fp_out)
-
-    def readline(self, size=None):
-        return self.fp.readline(size)
-
-    def readlines(self, sizehint=None):
-        return self.fp.readlines(sizehint)
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        line = self.readline()
-        if not line:
-            raise StopIteration
-        return line
-
-    def next(self):
-        return self.__next__()
-
-    def read_into_file(self, fp_out=None):
-        """Read the request body into fp_out (or make_file() if None).
-
-        Return fp_out.
-        """
-        if fp_out is None:
-            fp_out = self.make_file()
-        self.read(fp_out=fp_out)
-        return fp_out
-
-    def make_file(self):
-        """Return a file-like object into which the request body will be read.
-
-        By default, this will return a TemporaryFile. Override as needed.
-        See also :attr:`cherrypy._cpreqbody.Part.maxrambytes`."""
-        return tempfile.TemporaryFile()
-
-    def fullvalue(self):
-        """Return this entity as a string, whether stored in a file or not."""
-        if self.file:
-            # It was stored in a tempfile. Read it.
-            self.file.seek(0)
-            value = self.file.read()
-            self.file.seek(0)
-        else:
-            value = self.value
-        value = self.decode_entity(value)
-        return value
-
-    def decode_entity(self, value):
-        """Return a given byte encoded value as a string"""
-        for charset in self.attempt_charsets:
-            try:
-                value = value.decode(charset)
-            except UnicodeDecodeError:
-                pass
-            else:
-                self.charset = charset
-                return value
-        else:
-            raise cherrypy.HTTPError(
-                400,
-                'The request entity could not be decoded. The following '
-                'charsets were attempted: %s' % repr(self.attempt_charsets)
-            )
-
-    def process(self):
-        """Execute the best-match processor for the given media type."""
-        proc = None
-        ct = self.content_type.value
-        try:
-            proc = self.processors[ct]
-        except KeyError:
-            toptype = ct.split('/', 1)[0]
-            try:
-                proc = self.processors[toptype]
-            except KeyError:
-                pass
-        if proc is None:
-            self.default_proc()
-        else:
-            proc(self)
-
-    def default_proc(self):
-        """Called if a more-specific processor is not found for the
-        ``Content-Type``.
-        """
-        # Leave the fp alone for someone else to read. This works fine
-        # for request.body, but the Part subclasses need to override this
-        # so they can move on to the next part.
-        pass
-
-
-class Part(Entity):
-
-    """A MIME part entity, part of a multipart entity."""
-
-    # "The default character set, which must be assumed in the absence of a
-    # charset parameter, is US-ASCII."
-    attempt_charsets = ['us-ascii', 'utf-8']
-    r"""A list of strings, each of which should be a known encoding.
-
-    When the Content-Type of the request body warrants it, each of the given
-    encodings will be tried in order. The first one to successfully decode the
-    entity without raising an error is stored as
-    :attr:`entity.charset<cherrypy._cpreqbody.Entity.charset>`. This defaults
-    to ``['utf-8']`` (plus 'ISO-8859-1' for "text/\*" types, as required by
-    `HTTP/1.1
-    <http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1>`_),
-    but ``['us-ascii', 'utf-8']`` for multipart parts.
-    """
-
-    boundary = None
-    """The MIME multipart boundary."""
-
-    default_content_type = 'text/plain'
-    """This defines a default ``Content-Type`` to use if no Content-Type header
-    is given. The empty string is used for RequestBody, which results in the
-    request body not being read or parsed at all. This is by design; a missing
-    ``Content-Type`` header in the HTTP request entity is an error at best,
-    and a security hole at worst. For multipart parts, however (this class),
-    the MIME spec declares that a part with no Content-Type defaults to
-    "text/plain".
-    """
-
-    # This is the default in stdlib cgi. We may want to increase it.
-    maxrambytes = 1000
-    """The threshold of bytes after which point the ``Part`` will store
-    its data in a file (generated by
-    :func:`make_file<cherrypy._cprequest.Entity.make_file>`)
-    instead of a string. Defaults to 1000, just like the :mod:`cgi`
-    module in Python's standard library.
-    """
-
-    def __init__(self, fp, headers, boundary):
-        Entity.__init__(self, fp, headers)
-        self.boundary = boundary
-        self.file = None
-        self.value = None
-
-    @classmethod
-    def from_fp(cls, fp, boundary):
-        headers = cls.read_headers(fp)
-        return cls(fp, headers, boundary)
-
-    @classmethod
-    def read_headers(cls, fp):
-        headers = httputil.HeaderMap()
-        while True:
-            line = fp.readline()
-            if not line:
-                # No more data--illegal end of headers
-                raise EOFError('Illegal end of headers.')
-
-            if line == b'\r\n':
-                # Normal end of headers
-                break
-            if not line.endswith(b'\r\n'):
-                raise ValueError('MIME requires CRLF terminators: %r' % line)
-
-            if line[0] in b' \t':
-                # It's a continuation line.
-                v = line.strip().decode('ISO-8859-1')
-            else:
-                k, v = line.split(b':', 1)
-                k = k.strip().decode('ISO-8859-1')
-                v = v.strip().decode('ISO-8859-1')
-
-            existing = headers.get(k)
-            if existing:
-                v = ', '.join((existing, v))
-            headers[k] = v
-
-        return headers
-
-    def read_lines_to_boundary(self, fp_out=None):
-        """Read bytes from self.fp and return or write them to a file.
-
-        If the 'fp_out' argument is None (the default), all bytes read are
-        returned in a single byte string.
-
-        If the 'fp_out' argument is not None, it must be a file-like
-        object that supports the 'write' method; all bytes read will be
-        written to the fp, and that fp is returned.
-        """
-        endmarker = self.boundary + b'--'
-        delim = b''
-        prev_lf = True
-        lines = []
-        seen = 0
-        while True:
-            line = self.fp.readline(1 << 16)
-            if not line:
-                raise EOFError('Illegal end of multipart body.')
-            if line.startswith(b'--') and prev_lf:
-                strippedline = line.strip()
-                if strippedline == self.boundary:
-                    break
-                if strippedline == endmarker:
-                    self.fp.finish()
-                    break
-
-            line = delim + line
-
-            if line.endswith(b'\r\n'):
-                delim = b'\r\n'
-                line = line[:-2]
-                prev_lf = True
-            elif line.endswith(b'\n'):
-                delim = b'\n'
-                line = line[:-1]
-                prev_lf = True
-            else:
-                delim = b''
-                prev_lf = False
-
-            if fp_out is None:
-                lines.append(line)
-                seen += len(line)
-                if seen > self.maxrambytes:
-                    fp_out = self.make_file()
-                    for line in lines:
-                        fp_out.write(line)
-            else:
-                fp_out.write(line)
-
-        if fp_out is None:
-            result = b''.join(lines)
-            return result
-        else:
-            fp_out.seek(0)
-            return fp_out
-
-    def default_proc(self):
-        """Called if a more-specific processor is not found for the
-        ``Content-Type``.
-        """
-        if self.filename:
-            # Always read into a file if a .filename was given.
-            self.file = self.read_into_file()
-        else:
-            result = self.read_lines_to_boundary()
-            if isinstance(result, bytes):
-                self.value = result
-            else:
-                self.file = result
-
-    def read_into_file(self, fp_out=None):
-        """Read the request body into fp_out (or make_file() if None).
-
-        Return fp_out.
-        """
-        if fp_out is None:
-            fp_out = self.make_file()
-        self.read_lines_to_boundary(fp_out=fp_out)
-        return fp_out
-
-
-Entity.part_class = Part
-
-inf = float('inf')
-
-
-class SizedReader:
-
-    def __init__(self, fp, length, maxbytes, bufsize=DEFAULT_BUFFER_SIZE,
-                 has_trailers=False):
-        # Wrap our fp in a buffer so peek() works
-        self.fp = fp
-        self.length = length
-        self.maxbytes = maxbytes
-        self.buffer = b''
-        self.bufsize = bufsize
-        self.bytes_read = 0
-        self.done = False
-        self.has_trailers = has_trailers
-
-    def read(self, size=None, fp_out=None):
-        """Read bytes from the request body and return or write them to a file.
-
-        A number of bytes less than or equal to the 'size' argument are read
-        off the socket. The actual number of bytes read are tracked in
-        self.bytes_read. The number may be smaller than 'size' when 1) the
-        client sends fewer bytes, 2) the 'Content-Length' request header
-        specifies fewer bytes than requested, or 3) the number of bytes read
-        exceeds self.maxbytes (in which case, 413 is raised).
-
-        If the 'fp_out' argument is None (the default), all bytes read are
-        returned in a single byte string.
-
-        If the 'fp_out' argument is not None, it must be a file-like
-        object that supports the 'write' method; all bytes read will be
-        written to the fp, and None is returned.
-        """
-
-        if self.length is None:
-            if size is None:
-                remaining = inf
-            else:
-                remaining = size
-        else:
-            remaining = self.length - self.bytes_read
-            if size and size < remaining:
-                remaining = size
-        if remaining == 0:
-            self.finish()
-            if fp_out is None:
-                return b''
-            else:
-                return None
-
-        chunks = []
-
-        # Read bytes from the buffer.
-        if self.buffer:
-            if remaining is inf:
-                data = self.buffer
-                self.buffer = b''
-            else:
-                data = self.buffer[:remaining]
-                self.buffer = self.buffer[remaining:]
-            datalen = len(data)
-            remaining -= datalen
-
-            # Check lengths.
-            self.bytes_read += datalen
-            if self.maxbytes and self.bytes_read > self.maxbytes:
-                raise cherrypy.HTTPError(413)
-
-            # Store the data.
-            if fp_out is None:
-                chunks.append(data)
-            else:
-                fp_out.write(data)
-
-        # Read bytes from the socket.
-        while remaining > 0:
-            chunksize = min(remaining, self.bufsize)
-            try:
-                data = self.fp.read(chunksize)
-            except Exception:
-                e = sys.exc_info()[1]
-                if e.__class__.__name__ == 'MaxSizeExceeded':
-                    # Post data is too big
-                    raise cherrypy.HTTPError(
-                        413, 'Maximum request length: %r' % e.args[1])
-                else:
-                    raise
-            if not data:
-                self.finish()
-                break
-            datalen = len(data)
-            remaining -= datalen
-
-            # Check lengths.
-            self.bytes_read += datalen
-            if self.maxbytes and self.bytes_read > self.maxbytes:
-                raise cherrypy.HTTPError(413)
-
-            # Store the data.
-            if fp_out is None:
-                chunks.append(data)
-            else:
-                fp_out.write(data)
-
-        if fp_out is None:
-            return b''.join(chunks)
-
-    def readline(self, size=None):
-        """Read a line from the request body and return it."""
-        chunks = []
-        while size is None or size > 0:
-            chunksize = self.bufsize
-            if size is not None and size < self.bufsize:
-                chunksize = size
-            data = self.read(chunksize)
-            if not data:
-                break
-            pos = data.find(b'\n') + 1
-            if pos:
-                chunks.append(data[:pos])
-                remainder = data[pos:]
-                self.buffer += remainder
-                self.bytes_read -= len(remainder)
-                break
-            else:
-                chunks.append(data)
-        return b''.join(chunks)
-
-    def readlines(self, sizehint=None):
-        """Read lines from the request body and return them."""
-        if self.length is not None:
-            if sizehint is None:
-                sizehint = self.length - self.bytes_read
-            else:
-                sizehint = min(sizehint, self.length - self.bytes_read)
-
-        lines = []
-        seen = 0
-        while True:
-            line = self.readline()
-            if not line:
-                break
-            lines.append(line)
-            seen += len(line)
-            if seen >= sizehint:
-                break
-        return lines
-
-    def finish(self):
-        self.done = True
-        if self.has_trailers and hasattr(self.fp, 'read_trailer_lines'):
-            self.trailers = {}
-
-            try:
-                for line in self.fp.read_trailer_lines():
-                    if line[0] in b' \t':
-                        # It's a continuation line.
-                        v = line.strip()
-                    else:
-                        try:
-                            k, v = line.split(b':', 1)
-                        except ValueError:
-                            raise ValueError('Illegal header line.')
-                        k = k.strip().title()
-                        v = v.strip()
-
-                    if k in cheroot.server.comma_separated_headers:
-                        existing = self.trailers.get(k)
-                        if existing:
-                            v = b', '.join((existing, v))
-                    self.trailers[k] = v
-            except Exception:
-                e = sys.exc_info()[1]
-                if e.__class__.__name__ == 'MaxSizeExceeded':
-                    # Post data is too big
-                    raise cherrypy.HTTPError(
-                        413, 'Maximum request length: %r' % e.args[1])
-                else:
-                    raise
-
-
-class RequestBody(Entity):
-
-    """The entity of the HTTP request."""
-
-    bufsize = 8 * 1024
-    """The buffer size used when reading the socket."""
-
-    # Don't parse the request body at all if the client didn't provide
-    # a Content-Type header. See
-    # https://github.com/cherrypy/cherrypy/issues/790
-    default_content_type = ''
-    """This defines a default ``Content-Type`` to use if no Content-Type header
-    is given. The empty string is used for RequestBody, which results in the
-    request body not being read or parsed at all. This is by design; a missing
-    ``Content-Type`` header in the HTTP request entity is an error at best,
-    and a security hole at worst. For multipart parts, however, the MIME spec
-    declares that a part with no Content-Type defaults to "text/plain"
-    (see :class:`Part<cherrypy._cpreqbody.Part>`).
-    """
-
-    maxbytes = None
-    """Raise ``MaxSizeExceeded`` if more bytes than this are read from
-    the socket.
-    """
-
-    def __init__(self, fp, headers, params=None, request_params=None):
-        Entity.__init__(self, fp, headers, params)
-
-        # http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7.1
-        # When no explicit charset parameter is provided by the
-        # sender, media subtypes of the "text" type are defined
-        # to have a default charset value of "ISO-8859-1" when
-        # received via HTTP.
-        if self.content_type.value.startswith('text/'):
-            for c in ('ISO-8859-1', 'iso-8859-1', 'Latin-1', 'latin-1'):
-                if c in self.attempt_charsets:
-                    break
-            else:
-                self.attempt_charsets.append('ISO-8859-1')
-
-        # Temporary fix while deprecating passing .parts as .params.
-        self.processors['multipart'] = _old_process_multipart
-
-        if request_params is None:
-            request_params = {}
-        self.request_params = request_params
-
-    def process(self):
-        """Process the request entity based on its Content-Type."""
-        # "The presence of a message-body in a request is signaled by the
-        # inclusion of a Content-Length or Transfer-Encoding header field in
-        # the request's message-headers."
-        # It is possible to send a POST request with no body, for example;
-        # however, app developers are responsible in that case to set
-        # cherrypy.request.process_body to False so this method isn't called.
-        h = cherrypy.serving.request.headers
-        if 'Content-Length' not in h and 'Transfer-Encoding' not in h:
-            raise cherrypy.HTTPError(411)
-
-        self.fp = SizedReader(self.fp, self.length,
-                              self.maxbytes, bufsize=self.bufsize,
-                              has_trailers='Trailer' in h)
-        super(RequestBody, self).process()
-
-        # Body params should also be a part of the request_params
-        # add them in here.
-        request_params = self.request_params
-        for key, value in self.params.items():
-            # Python 2 only: keyword arguments must be byte strings (type
-            # 'str').
-            if sys.version_info < (3, 0):
-                if isinstance(key, six.text_type):
-                    key = key.encode('ISO-8859-1')
-
-            if key in request_params:
-                if not isinstance(request_params[key], list):
-                    request_params[key] = [request_params[key]]
-                request_params[key].append(value)
-            else:
-                request_params[key] = value
diff --git a/libraries/cherrypy/_cprequest.py b/libraries/cherrypy/_cprequest.py
deleted file mode 100644
index 3cc0c811..00000000
--- a/libraries/cherrypy/_cprequest.py
+++ /dev/null
@@ -1,930 +0,0 @@
-import sys
-import time
-
-import uuid
-
-import six
-from six.moves.http_cookies import SimpleCookie, CookieError
-
-from more_itertools import consume
-
-import cherrypy
-from cherrypy._cpcompat import ntob
-from cherrypy import _cpreqbody
-from cherrypy._cperror import format_exc, bare_error
-from cherrypy.lib import httputil, reprconf, encoding
-
-
-class Hook(object):
-
-    """A callback and its metadata: failsafe, priority, and kwargs."""
-
-    callback = None
-    """
-    The bare callable that this Hook object is wrapping, which will
-    be called when the Hook is called."""
-
-    failsafe = False
-    """
-    If True, the callback is guaranteed to run even if other callbacks
-    from the same call point raise exceptions."""
-
-    priority = 50
-    """
-    Defines the order of execution for a list of Hooks. Priority numbers
-    should be limited to the closed interval [0, 100], but values outside
-    this range are acceptable, as are fractional values."""
-
-    kwargs = {}
-    """
-    A set of keyword arguments that will be passed to the
-    callable on each call."""
-
-    def __init__(self, callback, failsafe=None, priority=None, **kwargs):
-        self.callback = callback
-
-        if failsafe is None:
-            failsafe = getattr(callback, 'failsafe', False)
-        self.failsafe = failsafe
-
-        if priority is None:
-            priority = getattr(callback, 'priority', 50)
-        self.priority = priority
-
-        self.kwargs = kwargs
-
-    def __lt__(self, other):
-        """
-        Hooks sort by priority, ascending, such that
-        hooks of lower priority are run first.
-        """
-        return self.priority < other.priority
-
-    def __call__(self):
-        """Run self.callback(**self.kwargs)."""
-        return self.callback(**self.kwargs)
-
-    def __repr__(self):
-        cls = self.__class__
-        return ('%s.%s(callback=%r, failsafe=%r, priority=%r, %s)'
-                % (cls.__module__, cls.__name__, self.callback,
-                   self.failsafe, self.priority,
-                   ', '.join(['%s=%r' % (k, v)
-                              for k, v in self.kwargs.items()])))
-
-
-class HookMap(dict):
-
-    """A map of call points to lists of callbacks (Hook objects)."""
-
-    def __new__(cls, points=None):
-        d = dict.__new__(cls)
-        for p in points or []:
-            d[p] = []
-        return d
-
-    def __init__(self, *a, **kw):
-        pass
-
-    def attach(self, point, callback, failsafe=None, priority=None, **kwargs):
-        """Append a new Hook made from the supplied arguments."""
-        self[point].append(Hook(callback, failsafe, priority, **kwargs))
-
-    def run(self, point):
-        """Execute all registered Hooks (callbacks) for the given point."""
-        exc = None
-        hooks = self[point]
-        hooks.sort()
-        for hook in hooks:
-            # Some hooks are guaranteed to run even if others at
-            # the same hookpoint fail. We will still log the failure,
-            # but proceed on to the next hook. The only way
-            # to stop all processing from one of these hooks is
-            # to raise SystemExit and stop the whole server.
-            if exc is None or hook.failsafe:
-                try:
-                    hook()
-                except (KeyboardInterrupt, SystemExit):
-                    raise
-                except (cherrypy.HTTPError, cherrypy.HTTPRedirect,
-                        cherrypy.InternalRedirect):
-                    exc = sys.exc_info()[1]
-                except Exception:
-                    exc = sys.exc_info()[1]
-                    cherrypy.log(traceback=True, severity=40)
-        if exc:
-            raise exc
-
-    def __copy__(self):
-        newmap = self.__class__()
-        # We can't just use 'update' because we want copies of the
-        # mutable values (each is a list) as well.
-        for k, v in self.items():
-            newmap[k] = v[:]
-        return newmap
-    copy = __copy__
-
-    def __repr__(self):
-        cls = self.__class__
-        return '%s.%s(points=%r)' % (
-            cls.__module__,
-            cls.__name__,
-            list(self)
-        )
-
-
-# Config namespace handlers
-
-def hooks_namespace(k, v):
-    """Attach bare hooks declared in config."""
-    # Use split again to allow multiple hooks for a single
-    # hookpoint per path (e.g. "hooks.before_handler.1").
-    # Little-known fact you only get from reading source ;)
-    hookpoint = k.split('.', 1)[0]
-    if isinstance(v, six.string_types):
-        v = cherrypy.lib.reprconf.attributes(v)
-    if not isinstance(v, Hook):
-        v = Hook(v)
-    cherrypy.serving.request.hooks[hookpoint].append(v)
-
-
-def request_namespace(k, v):
-    """Attach request attributes declared in config."""
-    # Provides config entries to set request.body attrs (like
-    # attempt_charsets).
-    if k[:5] == 'body.':
-        setattr(cherrypy.serving.request.body, k[5:], v)
-    else:
-        setattr(cherrypy.serving.request, k, v)
-
-
-def response_namespace(k, v):
-    """Attach response attributes declared in config."""
-    # Provides config entries to set default response headers
-    # http://cherrypy.org/ticket/889
-    if k[:8] == 'headers.':
-        cherrypy.serving.response.headers[k.split('.', 1)[1]] = v
-    else:
-        setattr(cherrypy.serving.response, k, v)
-
-
-def error_page_namespace(k, v):
-    """Attach error pages declared in config."""
-    if k != 'default':
-        k = int(k)
-    cherrypy.serving.request.error_page[k] = v
-
-
-hookpoints = ['on_start_resource', 'before_request_body',
-              'before_handler', 'before_finalize',
-              'on_end_resource', 'on_end_request',
-              'before_error_response', 'after_error_response']
-
-
-class Request(object):
-
-    """An HTTP request.
-
-    This object represents the metadata of an HTTP request message;
-    that is, it contains attributes which describe the environment
-    in which the request URL, headers, and body were sent (if you
-    want tools to interpret the headers and body, those are elsewhere,
-    mostly in Tools). This 'metadata' consists of socket data,
-    transport characteristics, and the Request-Line. This object
-    also contains data regarding the configuration in effect for
-    the given URL, and the execution plan for generating a response.
-    """
-
-    prev = None
-    """
-    The previous Request object (if any). This should be None
-    unless we are processing an InternalRedirect."""
-
-    # Conversation/connection attributes
-    local = httputil.Host('127.0.0.1', 80)
-    'An httputil.Host(ip, port, hostname) object for the server socket.'
-
-    remote = httputil.Host('127.0.0.1', 1111)
-    'An httputil.Host(ip, port, hostname) object for the client socket.'
-
-    scheme = 'http'
-    """
-    The protocol used between client and server. In most cases,
-    this will be either 'http' or 'https'."""
-
-    server_protocol = 'HTTP/1.1'
-    """
-    The HTTP version for which the HTTP server is at least
-    conditionally compliant."""
-
-    base = ''
-    """The (scheme://host) portion of the requested URL.
-    In some cases (e.g. when proxying via mod_rewrite), this may contain
-    path segments which cherrypy.url uses when constructing url's, but
-    which otherwise are ignored by CherryPy. Regardless, this value
-    MUST NOT end in a slash."""
-
-    # Request-Line attributes
-    request_line = ''
-    """
-    The complete Request-Line received from the client. This is a
-    single string consisting of the request method, URI, and protocol
-    version (joined by spaces). Any final CRLF is removed."""
-
-    method = 'GET'
-    """
-    Indicates the HTTP method to be performed on the resource identified
-    by the Request-URI. Common methods include GET, HEAD, POST, PUT, and
-    DELETE. CherryPy allows any extension method; however, various HTTP
-    servers and gateways may restrict the set of allowable methods.
-    CherryPy applications SHOULD restrict the set (on a per-URI basis)."""
-
-    query_string = ''
-    """
-    The query component of the Request-URI, a string of information to be
-    interpreted by the resource. The query portion of a URI follows the
-    path component, and is separated by a '?'. For example, the URI
-    'http://www.cherrypy.org/wiki?a=3&b=4' has the query component,
-    'a=3&b=4'."""
-
-    query_string_encoding = 'utf8'
-    """
-    The encoding expected for query string arguments after % HEX HEX decoding).
-    If a query string is provided that cannot be decoded with this encoding,
-    404 is raised (since technically it's a different URI). If you want
-    arbitrary encodings to not error, set this to 'Latin-1'; you can then
-    encode back to bytes and re-decode to whatever encoding you like later.
-    """
-
-    protocol = (1, 1)
-    """The HTTP protocol version corresponding to the set
-    of features which should be allowed in the response. If BOTH
-    the client's request message AND the server's level of HTTP
-    compliance is HTTP/1.1, this attribute will be the tuple (1, 1).
-    If either is 1.0, this attribute will be the tuple (1, 0).
-    Lower HTTP protocol versions are not explicitly supported."""
-
-    params = {}
-    """
-    A dict which combines query string (GET) and request entity (POST)
-    variables. This is populated in two stages: GET params are added
-    before the 'on_start_resource' hook, and POST params are added
-    between the 'before_request_body' and 'before_handler' hooks."""
-
-    # Message attributes
-    header_list = []
-    """
-    A list of the HTTP request headers as (name, value) tuples.
-    In general, you should use request.headers (a dict) instead."""
-
-    headers = httputil.HeaderMap()
-    """
-    A dict-like object containing the request headers. Keys are header
-    names (in Title-Case format); however, you may get and set them in
-    a case-insensitive manner. That is, headers['Content-Type'] and
-    headers['content-type'] refer to the same value. Values are header
-    values (decoded according to :rfc:`2047` if necessary). See also:
-    httputil.HeaderMap, httputil.HeaderElement."""
-
-    cookie = SimpleCookie()
-    """See help(Cookie)."""
-
-    rfile = None
-    """
-    If the request included an entity (body), it will be available
-    as a stream in this attribute. However, the rfile will normally
-    be read for you between the 'before_request_body' hook and the
-    'before_handler' hook, and the resulting string is placed into
-    either request.params or the request.body attribute.
-
-    You may disable the automatic consumption of the rfile by setting
-    request.process_request_body to False, either in config for the desired
-    path, or in an 'on_start_resource' or 'before_request_body' hook.
-
-    WARNING: In almost every case, you should not attempt to read from the
-    rfile stream after CherryPy's automatic mechanism has read it. If you
-    turn off the automatic parsing of rfile, you should read exactly the
-    number of bytes specified in request.headers['Content-Length'].
-    Ignoring either of these warnings may result in a hung request thread
-    or in corruption of the next (pipelined) request.
-    """
-
-    process_request_body = True
-    """
-    If True, the rfile (if any) is automatically read and parsed,
-    and the result placed into request.params or request.body."""
-
-    methods_with_bodies = ('POST', 'PUT', 'PATCH')
-    """
-    A sequence of HTTP methods for which CherryPy will automatically
-    attempt to read a body from the rfile. If you are going to change
-    this property, modify it on the configuration (recommended)
-    or on the "hook point" `on_start_resource`.
-    """
-
-    body = None
-    """
-    If the request Content-Type is 'application/x-www-form-urlencoded'
-    or multipart, this will be None. Otherwise, this will be an instance
-    of :class:`RequestBody<cherrypy._cpreqbody.RequestBody>` (which you
-    can .read()); this value is set between the 'before_request_body' and
-    'before_handler' hooks (assuming that process_request_body is True)."""
-
-    # Dispatch attributes
-    dispatch = cherrypy.dispatch.Dispatcher()
-    """
-    The object which looks up the 'page handler' callable and collects
-    config for the current request based on the path_info, other
-    request attributes, and the application architecture. The core
-    calls the dispatcher as early as possible, passing it a 'path_info'
-    argument.
-
-    The default dispatcher discovers the page handler by matching path_info
-    to a hierarchical arrangement of objects, starting at request.app.root.
-    See help(cherrypy.dispatch) for more information."""
-
-    script_name = ''
-    """
-    The 'mount point' of the application which is handling this request.
-
-    This attribute MUST NOT end in a slash. If the script_name refers to
-    the root of the URI, it MUST be an empty string (not "/").
-    """
-
-    path_info = '/'
-    """
-    The 'relative path' portion of the Request-URI. This is relative
-    to the script_name ('mount point') of the application which is
-    handling this request."""
-
-    login = None
-    """
-    When authentication is used during the request processing this is
-    set to 'False' if it failed and to the 'username' value if it succeeded.
-    The default 'None' implies that no authentication happened."""
-
-    # Note that cherrypy.url uses "if request.app:" to determine whether
-    # the call is during a real HTTP request or not. So leave this None.
-    app = None
-    """The cherrypy.Application object which is handling this request."""
-
-    handler = None
-    """
-    The function, method, or other callable which CherryPy will call to
-    produce the response. The discovery of the handler and the arguments
-    it will receive are determined by the request.dispatch object.
-    By default, the handler is discovered by walking a tree of objects
-    starting at request.app.root, and is then passed all HTTP params
-    (from the query string and POST body) as keyword arguments."""
-
-    toolmaps = {}
-    """
-    A nested dict of all Toolboxes and Tools in effect for this request,
-    of the form: {Toolbox.namespace: {Tool.name: config dict}}."""
-
-    config = None
-    """
-    A flat dict of all configuration entries which apply to the
-    current request. These entries are collected from global config,
-    application config (based on request.path_info), and from handler
-    config (exactly how is governed by the request.dispatch object in
-    effect for this request; by default, handler config can be attached
-    anywhere in the tree between request.app.root and the final handler,
-    and inherits downward)."""
-
-    is_index = None
-    """
-    This will be True if the current request is mapped to an 'index'
-    resource handler (also, a 'default' handler if path_info ends with
-    a slash). The value may be used to automatically redirect the
-    user-agent to a 'more canonical' URL which either adds or removes
-    the trailing slash. See cherrypy.tools.trailing_slash."""
-
-    hooks = HookMap(hookpoints)
-    """
-    A HookMap (dict-like object) of the form: {hookpoint: [hook, ...]}.
-    Each key is a str naming the hook point, and each value is a list
-    of hooks which will be called at that hook point during this request.
-    The list of hooks is generally populated as early as possible (mostly
-    from Tools specified in config), but may be extended at any time.
-    See also: _cprequest.Hook, _cprequest.HookMap, and cherrypy.tools."""
-
-    error_response = cherrypy.HTTPError(500).set_response
-    """
-    The no-arg callable which will handle unexpected, untrapped errors
-    during request processing. This is not used for expected exceptions
-    (like NotFound, HTTPError, or HTTPRedirect) which are raised in
-    response to expected conditions (those should be customized either
-    via request.error_page or by overriding HTTPError.set_response).
-    By default, error_response uses HTTPError(500) to return a generic
-    error response to the user-agent."""
-
-    error_page = {}
-    """
-    A dict of {error code: response filename or callable} pairs.
-
-    The error code must be an int representing a given HTTP error code,
-    or the string 'default', which will be used if no matching entry
-    is found for a given numeric code.
-
-    If a filename is provided, the file should contain a Python string-
-    formatting template, and can expect by default to receive format
-    values with the mapping keys %(status)s, %(message)s, %(traceback)s,
-    and %(version)s. The set of format mappings can be extended by
-    overriding HTTPError.set_response.
-
-    If a callable is provided, it will be called by default with keyword
-    arguments 'status', 'message', 'traceback', and 'version', as for a
-    string-formatting template. The callable must return a string or
-    iterable of strings which will be set to response.body. It may also
-    override headers or perform any other processing.
-
-    If no entry is given for an error code, and no 'default' entry exists,
-    a default template will be used.
-    """
-
-    show_tracebacks = True
-    """
-    If True, unexpected errors encountered during request processing will
-    include a traceback in the response body."""
-
-    show_mismatched_params = True
-    """
-    If True, mismatched parameters encountered during PageHandler invocation
-    processing will be included in the response body."""
-
-    throws = (KeyboardInterrupt, SystemExit, cherrypy.InternalRedirect)
-    """The sequence of exceptions which Request.run does not trap."""
-
-    throw_errors = False
-    """
-    If True, Request.run will not trap any errors (except HTTPRedirect and
-    HTTPError, which are more properly called 'exceptions', not errors)."""
-
-    closed = False
-    """True once the close method has been called, False otherwise."""
-
-    stage = None
-    """
-    A string containing the stage reached in the request-handling process.
-    This is useful when debugging a live server with hung requests."""
-
-    unique_id = None
-    """A lazy object generating and memorizing UUID4 on ``str()`` render."""
-
-    namespaces = reprconf.NamespaceSet(
-        **{'hooks': hooks_namespace,
-           'request': request_namespace,
-           'response': response_namespace,
-           'error_page': error_page_namespace,
-           'tools': cherrypy.tools,
-           })
-
-    def __init__(self, local_host, remote_host, scheme='http',
-                 server_protocol='HTTP/1.1'):
-        """Populate a new Request object.
-
-        local_host should be an httputil.Host object with the server info.
-        remote_host should be an httputil.Host object with the client info.
-        scheme should be a string, either "http" or "https".
-        """
-        self.local = local_host
-        self.remote = remote_host
-        self.scheme = scheme
-        self.server_protocol = server_protocol
-
-        self.closed = False
-
-        # Put a *copy* of the class error_page into self.
-        self.error_page = self.error_page.copy()
-
-        # Put a *copy* of the class namespaces into self.
-        self.namespaces = self.namespaces.copy()
-
-        self.stage = None
-
-        self.unique_id = LazyUUID4()
-
-    def close(self):
-        """Run cleanup code. (Core)"""
-        if not self.closed:
-            self.closed = True
-            self.stage = 'on_end_request'
-            self.hooks.run('on_end_request')
-            self.stage = 'close'
-
-    def run(self, method, path, query_string, req_protocol, headers, rfile):
-        r"""Process the Request. (Core)
-
-        method, path, query_string, and req_protocol should be pulled directly
-        from the Request-Line (e.g. "GET /path?key=val HTTP/1.0").
-
-        path
-            This should be %XX-unquoted, but query_string should not be.
-
-            When using Python 2, they both MUST be byte strings,
-            not unicode strings.
-
-            When using Python 3, they both MUST be unicode strings,
-            not byte strings, and preferably not bytes \x00-\xFF
-            disguised as unicode.
-
-        headers
-            A list of (name, value) tuples.
-
-        rfile
-            A file-like object containing the HTTP request entity.
-
-        When run() is done, the returned object should have 3 attributes:
-
-          * status, e.g. "200 OK"
-          * header_list, a list of (name, value) tuples
-          * body, an iterable yielding strings
-
-        Consumer code (HTTP servers) should then access these response
-        attributes to build the outbound stream.
-
-        """
-        response = cherrypy.serving.response
-        self.stage = 'run'
-        try:
-            self.error_response = cherrypy.HTTPError(500).set_response
-
-            self.method = method
-            path = path or '/'
-            self.query_string = query_string or ''
-            self.params = {}
-
-            # Compare request and server HTTP protocol versions, in case our
-            # server does not support the requested protocol. Limit our output
-            # to min(req, server). We want the following output:
-            #     request    server     actual written   supported response
-            #     protocol   protocol  response protocol    feature set
-            # a     1.0        1.0           1.0                1.0
-            # b     1.0        1.1           1.1                1.0
-            # c     1.1        1.0           1.0                1.0
-            # d     1.1        1.1           1.1                1.1
-            # Notice that, in (b), the response will be "HTTP/1.1" even though
-            # the client only understands 1.0. RFC 2616 10.5.6 says we should
-            # only return 505 if the _major_ version is different.
-            rp = int(req_protocol[5]), int(req_protocol[7])
-            sp = int(self.server_protocol[5]), int(self.server_protocol[7])
-            self.protocol = min(rp, sp)
-            response.headers.protocol = self.protocol
-
-            # Rebuild first line of the request (e.g. "GET /path HTTP/1.0").
-            url = path
-            if query_string:
-                url += '?' + query_string
-            self.request_line = '%s %s %s' % (method, url, req_protocol)
-
-            self.header_list = list(headers)
-            self.headers = httputil.HeaderMap()
-
-            self.rfile = rfile
-            self.body = None
-
-            self.cookie = SimpleCookie()
-            self.handler = None
-
-            # path_info should be the path from the
-            # app root (script_name) to the handler.
-            self.script_name = self.app.script_name
-            self.path_info = pi = path[len(self.script_name):]
-
-            self.stage = 'respond'
-            self.respond(pi)
-
-        except self.throws:
-            raise
-        except Exception:
-            if self.throw_errors:
-                raise
-            else:
-                # Failure in setup, error handler or finalize. Bypass them.
-                # Can't use handle_error because we may not have hooks yet.
-                cherrypy.log(traceback=True, severity=40)
-                if self.show_tracebacks:
-                    body = format_exc()
-                else:
-                    body = ''
-                r = bare_error(body)
-                response.output_status, response.header_list, response.body = r
-
-        if self.method == 'HEAD':
-            # HEAD requests MUST NOT return a message-body in the response.
-            response.body = []
-
-        try:
-            cherrypy.log.access()
-        except Exception:
-            cherrypy.log.error(traceback=True)
-
-        return response
-
-    def respond(self, path_info):
-        """Generate a response for the resource at self.path_info. (Core)"""
-        try:
-            try:
-                try:
-                    self._do_respond(path_info)
-                except (cherrypy.HTTPRedirect, cherrypy.HTTPError):
-                    inst = sys.exc_info()[1]
-                    inst.set_response()
-                    self.stage = 'before_finalize (HTTPError)'
-                    self.hooks.run('before_finalize')
-                    cherrypy.serving.response.finalize()
-            finally:
-                self.stage = 'on_end_resource'
-                self.hooks.run('on_end_resource')
-        except self.throws:
-            raise
-        except Exception:
-            if self.throw_errors:
-                raise
-            self.handle_error()
-
-    def _do_respond(self, path_info):
-        response = cherrypy.serving.response
-
-        if self.app is None:
-            raise cherrypy.NotFound()
-
-        self.hooks = self.__class__.hooks.copy()
-        self.toolmaps = {}
-
-        # Get the 'Host' header, so we can HTTPRedirect properly.
-        self.stage = 'process_headers'
-        self.process_headers()
-
-        self.stage = 'get_resource'
-        self.get_resource(path_info)
-
-        self.body = _cpreqbody.RequestBody(
-            self.rfile, self.headers, request_params=self.params)
-
-        self.namespaces(self.config)
-
-        self.stage = 'on_start_resource'
-        self.hooks.run('on_start_resource')
-
-        # Parse the querystring
-        self.stage = 'process_query_string'
-        self.process_query_string()
-
-        # Process the body
-        if self.process_request_body:
-            if self.method not in self.methods_with_bodies:
-                self.process_request_body = False
-        self.stage = 'before_request_body'
-        self.hooks.run('before_request_body')
-        if self.process_request_body:
-            self.body.process()
-
-        # Run the handler
-        self.stage = 'before_handler'
-        self.hooks.run('before_handler')
-        if self.handler:
-            self.stage = 'handler'
-            response.body = self.handler()
-
-        # Finalize
-        self.stage = 'before_finalize'
-        self.hooks.run('before_finalize')
-        response.finalize()
-
-    def process_query_string(self):
-        """Parse the query string into Python structures. (Core)"""
-        try:
-            p = httputil.parse_query_string(
-                self.query_string, encoding=self.query_string_encoding)
-        except UnicodeDecodeError:
-            raise cherrypy.HTTPError(
-                404, 'The given query string could not be processed. Query '
-                'strings for this resource must be encoded with %r.' %
-                self.query_string_encoding)
-
-        # Python 2 only: keyword arguments must be byte strings (type 'str').
-        if six.PY2:
-            for key, value in p.items():
-                if isinstance(key, six.text_type):
-                    del p[key]
-                    p[key.encode(self.query_string_encoding)] = value
-        self.params.update(p)
-
-    def process_headers(self):
-        """Parse HTTP header data into Python structures. (Core)"""
-        # Process the headers into self.headers
-        headers = self.headers
-        for name, value in self.header_list:
-            # Call title() now (and use dict.__method__(headers))
-            # so title doesn't have to be called twice.
-            name = name.title()
-            value = value.strip()
-
-            headers[name] = httputil.decode_TEXT_maybe(value)
-
-            # Some clients, notably Konquoror, supply multiple
-            # cookies on different lines with the same key. To
-            # handle this case, store all cookies in self.cookie.
-            if name == 'Cookie':
-                try:
-                    self.cookie.load(value)
-                except CookieError as exc:
-                    raise cherrypy.HTTPError(400, str(exc))
-
-        if not dict.__contains__(headers, 'Host'):
-            # All Internet-based HTTP/1.1 servers MUST respond with a 400
-            # (Bad Request) status code to any HTTP/1.1 request message
-            # which lacks a Host header field.
-            if self.protocol >= (1, 1):
-                msg = "HTTP/1.1 requires a 'Host' request header."
-                raise cherrypy.HTTPError(400, msg)
-        host = dict.get(headers, 'Host')
-        if not host:
-            host = self.local.name or self.local.ip
-        self.base = '%s://%s' % (self.scheme, host)
-
-    def get_resource(self, path):
-        """Call a dispatcher (which sets self.handler and .config). (Core)"""
-        # First, see if there is a custom dispatch at this URI. Custom
-        # dispatchers can only be specified in app.config, not in _cp_config
-        # (since custom dispatchers may not even have an app.root).
-        dispatch = self.app.find_config(
-            path, 'request.dispatch', self.dispatch)
-
-        # dispatch() should set self.handler and self.config
-        dispatch(path)
-
-    def handle_error(self):
-        """Handle the last unanticipated exception. (Core)"""
-        try:
-            self.hooks.run('before_error_response')
-            if self.error_response:
-                self.error_response()
-            self.hooks.run('after_error_response')
-            cherrypy.serving.response.finalize()
-        except cherrypy.HTTPRedirect:
-            inst = sys.exc_info()[1]
-            inst.set_response()
-            cherrypy.serving.response.finalize()
-
-
-class ResponseBody(object):
-
-    """The body of the HTTP response (the response entity)."""
-
-    unicode_err = ('Page handlers MUST return bytes. Use tools.encode '
-                   'if you wish to return unicode.')
-
-    def __get__(self, obj, objclass=None):
-        if obj is None:
-            # When calling on the class instead of an instance...
-            return self
-        else:
-            return obj._body
-
-    def __set__(self, obj, value):
-        # Convert the given value to an iterable object.
-        if isinstance(value, six.text_type):
-            raise ValueError(self.unicode_err)
-        elif isinstance(value, list):
-            # every item in a list must be bytes...
-            if any(isinstance(item, six.text_type) for item in value):
-                raise ValueError(self.unicode_err)
-
-        obj._body = encoding.prepare_iter(value)
-
-
-class Response(object):
-
-    """An HTTP Response, including status, headers, and body."""
-
-    status = ''
-    """The HTTP Status-Code and Reason-Phrase."""
-
-    header_list = []
-    """
-    A list of the HTTP response headers as (name, value) tuples.
-    In general, you should use response.headers (a dict) instead. This
-    attribute is generated from response.headers and is not valid until
-    after the finalize phase."""
-
-    headers = httputil.HeaderMap()
-    """
-    A dict-like object containing the response headers. Keys are header
-    names (in Title-Case format); however, you may get and set them in
-    a case-insensitive manner. That is, headers['Content-Type'] and
-    headers['content-type'] refer to the same value. Values are header
-    values (decoded according to :rfc:`2047` if necessary).
-
-    .. seealso:: classes :class:`HeaderMap`, :class:`HeaderElement`
-    """
-
-    cookie = SimpleCookie()
-    """See help(Cookie)."""
-
-    body = ResponseBody()
-    """The body (entity) of the HTTP response."""
-
-    time = None
-    """The value of time.time() when created. Use in HTTP dates."""
-
-    stream = False
-    """If False, buffer the response body."""
-
-    def __init__(self):
-        self.status = None
-        self.header_list = None
-        self._body = []
-        self.time = time.time()
-
-        self.headers = httputil.HeaderMap()
-        # Since we know all our keys are titled strings, we can
-        # bypass HeaderMap.update and get a big speed boost.
-        dict.update(self.headers, {
-            'Content-Type': 'text/html',
-            'Server': 'CherryPy/' + cherrypy.__version__,
-            'Date': httputil.HTTPDate(self.time),
-        })
-        self.cookie = SimpleCookie()
-
-    def collapse_body(self):
-        """Collapse self.body to a single string; replace it and return it."""
-        new_body = b''.join(self.body)
-        self.body = new_body
-        return new_body
-
-    def _flush_body(self):
-        """
-        Discard self.body but consume any generator such that
-        any finalization can occur, such as is required by
-        caching.tee_output().
-        """
-        consume(iter(self.body))
-
-    def finalize(self):
-        """Transform headers (and cookies) into self.header_list. (Core)"""
-        try:
-            code, reason, _ = httputil.valid_status(self.status)
-        except ValueError:
-            raise cherrypy.HTTPError(500, sys.exc_info()[1].args[0])
-
-        headers = self.headers
-
-        self.status = '%s %s' % (code, reason)
-        self.output_status = ntob(str(code), 'ascii') + \
-            b' ' + headers.encode(reason)
-
-        if self.stream:
-            # The upshot: wsgiserver will chunk the response if
-            # you pop Content-Length (or set it explicitly to None).
-            # Note that lib.static sets C-L to the file's st_size.
-            if dict.get(headers, 'Content-Length') is None:
-                dict.pop(headers, 'Content-Length', None)
-        elif code < 200 or code in (204, 205, 304):
-            # "All 1xx (informational), 204 (no content),
-            # and 304 (not modified) responses MUST NOT
-            # include a message-body."
-            dict.pop(headers, 'Content-Length', None)
-            self._flush_body()
-            self.body = b''
-        else:
-            # Responses which are not streamed should have a Content-Length,
-            # but allow user code to set Content-Length if desired.
-            if dict.get(headers, 'Content-Length') is None:
-                content = self.collapse_body()
-                dict.__setitem__(headers, 'Content-Length', len(content))
-
-        # Transform our header dict into a list of tuples.
-        self.header_list = h = headers.output()
-
-        cookie = self.cookie.output()
-        if cookie:
-            for line in cookie.split('\r\n'):
-                name, value = line.split(': ', 1)
-                if isinstance(name, six.text_type):
-                    name = name.encode('ISO-8859-1')
-                if isinstance(value, six.text_type):
-                    value = headers.encode(value)
-                h.append((name, value))
-
-
-class LazyUUID4(object):
-    def __str__(self):
-        """Return UUID4 and keep it for future calls."""
-        return str(self.uuid4)
-
-    @property
-    def uuid4(self):
-        """Provide unique id on per-request basis using UUID4.
-
-        It's evaluated lazily on render.
-        """
-        try:
-            self._uuid4
-        except AttributeError:
-            # evaluate on first access
-            self._uuid4 = uuid.uuid4()
-
-        return self._uuid4
diff --git a/libraries/cherrypy/_cpserver.py b/libraries/cherrypy/_cpserver.py
deleted file mode 100644
index 0f60e2c8..00000000
--- a/libraries/cherrypy/_cpserver.py
+++ /dev/null
@@ -1,252 +0,0 @@
-"""Manage HTTP servers with CherryPy."""
-
-import six
-
-import cherrypy
-from cherrypy.lib.reprconf import attributes
-from cherrypy._cpcompat import text_or_bytes
-from cherrypy.process.servers import ServerAdapter
-
-
-__all__ = ('Server', )
-
-
-class Server(ServerAdapter):
-    """An adapter for an HTTP server.
-
-    You can set attributes (like socket_host and socket_port)
-    on *this* object (which is probably cherrypy.server), and call
-    quickstart. For example::
-
-        cherrypy.server.socket_port = 80
-        cherrypy.quickstart()
-    """
-
-    socket_port = 8080
-    """The TCP port on which to listen for connections."""
-
-    _socket_host = '127.0.0.1'
-
-    @property
-    def socket_host(self):  # noqa: D401; irrelevant for properties
-        """The hostname or IP address on which to listen for connections.
-
-        Host values may be any IPv4 or IPv6 address, or any valid hostname.
-        The string 'localhost' is a synonym for '127.0.0.1' (or '::1', if
-        your hosts file prefers IPv6). The string '0.0.0.0' is a special
-        IPv4 entry meaning "any active interface" (INADDR_ANY), and '::'
-        is the similar IN6ADDR_ANY for IPv6. The empty string or None are
-        not allowed.
-        """
-        return self._socket_host
-
-    @socket_host.setter
-    def socket_host(self, value):
-        if value == '':
-            raise ValueError("The empty string ('') is not an allowed value. "
-                             "Use '0.0.0.0' instead to listen on all active "
-                             'interfaces (INADDR_ANY).')
-        self._socket_host = value
-
-    socket_file = None
-    """If given, the name of the UNIX socket to use instead of TCP/IP.
-
-    When this option is not None, the `socket_host` and `socket_port` options
-    are ignored."""
-
-    socket_queue_size = 5
-    """The 'backlog' argument to socket.listen(); specifies the maximum number
-    of queued connections (default 5)."""
-
-    socket_timeout = 10
-    """The timeout in seconds for accepted connections (default 10)."""
-
-    accepted_queue_size = -1
-    """The maximum number of requests which will be queued up before
-    the server refuses to accept it (default -1, meaning no limit)."""
-
-    accepted_queue_timeout = 10
-    """The timeout in seconds for attempting to add a request to the
-    queue when the queue is full (default 10)."""
-
-    shutdown_timeout = 5
-    """The time to wait for HTTP worker threads to clean up."""
-
-    protocol_version = 'HTTP/1.1'
-    """The version string to write in the Status-Line of all HTTP responses,
-    for example, "HTTP/1.1" (the default). Depending on the HTTP server used,
-    this should also limit the supported features used in the response."""
-
-    thread_pool = 10
-    """The number of worker threads to start up in the pool."""
-
-    thread_pool_max = -1
-    """The maximum size of the worker-thread pool. Use -1 to indicate no limit.
-    """
-
-    max_request_header_size = 500 * 1024
-    """The maximum number of bytes allowable in the request headers.
-    If exceeded, the HTTP server should return "413 Request Entity Too Large".
-    """
-
-    max_request_body_size = 100 * 1024 * 1024
-    """The maximum number of bytes allowable in the request body. If exceeded,
-    the HTTP server should return "413 Request Entity Too Large"."""
-
-    instance = None
-    """If not None, this should be an HTTP server instance (such as
-    cheroot.wsgi.Server) which cherrypy.server will control.
-    Use this when you need
-    more control over object instantiation than is available in the various
-    configuration options."""
-
-    ssl_context = None
-    """When using PyOpenSSL, an instance of SSL.Context."""
-
-    ssl_certificate = None
-    """The filename of the SSL certificate to use."""
-
-    ssl_certificate_chain = None
-    """When using PyOpenSSL, the certificate chain to pass to
-    Context.load_verify_locations."""
-
-    ssl_private_key = None
-    """The filename of the private key to use with SSL."""
-
-    ssl_ciphers = None
-    """The ciphers list of SSL."""
-
-    if six.PY3:
-        ssl_module = 'builtin'
-        """The name of a registered SSL adaptation module to use with
-        the builtin WSGI server. Builtin options are: 'builtin' (to
-        use the SSL library built into recent versions of Python).
-        You may also register your own classes in the
-        cheroot.server.ssl_adapters dict."""
-    else:
-        ssl_module = 'pyopenssl'
-        """The name of a registered SSL adaptation module to use with the
-        builtin WSGI server. Builtin options are 'builtin' (to use the SSL
-        library built into recent versions of Python) and 'pyopenssl' (to
-        use the PyOpenSSL project, which you must install separately). You
-        may also register your own classes in the cheroot.server.ssl_adapters
-        dict."""
-
-    statistics = False
-    """Turns statistics-gathering on or off for aware HTTP servers."""
-
-    nodelay = True
-    """If True (the default since 3.1), sets the TCP_NODELAY socket option."""
-
-    wsgi_version = (1, 0)
-    """The WSGI version tuple to use with the builtin WSGI server.
-    The provided options are (1, 0) [which includes support for PEP 3333,
-    which declares it covers WSGI version 1.0.1 but still mandates the
-    wsgi.version (1, 0)] and ('u', 0), an experimental unicode version.
-    You may create and register your own experimental versions of the WSGI
-    protocol by adding custom classes to the cheroot.server.wsgi_gateways dict.
-    """
-
-    peercreds = False
-    """If True, peer cred lookup for UNIX domain socket will put to WSGI env.
-
-    This information will then be available through WSGI env vars:
-    * X_REMOTE_PID
-    * X_REMOTE_UID
-    * X_REMOTE_GID
-    """
-
-    peercreds_resolve = False
-    """If True, username/group will be looked up in the OS from peercreds.
-
-    This information will then be available through WSGI env vars:
-    * REMOTE_USER
-    * X_REMOTE_USER
-    * X_REMOTE_GROUP
-    """
-
-    def __init__(self):
-        """Initialize Server instance."""
-        self.bus = cherrypy.engine
-        self.httpserver = None
-        self.interrupt = None
-        self.running = False
-
-    def httpserver_from_self(self, httpserver=None):
-        """Return a (httpserver, bind_addr) pair based on self attributes."""
-        if httpserver is None:
-            httpserver = self.instance
-        if httpserver is None:
-            from cherrypy import _cpwsgi_server
-            httpserver = _cpwsgi_server.CPWSGIServer(self)
-        if isinstance(httpserver, text_or_bytes):
-            # Is anyone using this? Can I add an arg?
-            httpserver = attributes(httpserver)(self)
-        return httpserver, self.bind_addr
-
-    def start(self):
-        """Start the HTTP server."""
-        if not self.httpserver:
-            self.httpserver, self.bind_addr = self.httpserver_from_self()
-        super(Server, self).start()
-    start.priority = 75
-
-    @property
-    def bind_addr(self):
-        """Return bind address.
-
-        A (host, port) tuple for TCP sockets or a str for Unix domain sockts.
-        """
-        if self.socket_file:
-            return self.socket_file
-        if self.socket_host is None and self.socket_port is None:
-            return None
-        return (self.socket_host, self.socket_port)
-
-    @bind_addr.setter
-    def bind_addr(self, value):
-        if value is None:
-            self.socket_file = None
-            self.socket_host = None
-            self.socket_port = None
-        elif isinstance(value, text_or_bytes):
-            self.socket_file = value
-            self.socket_host = None
-            self.socket_port = None
-        else:
-            try:
-                self.socket_host, self.socket_port = value
-                self.socket_file = None
-            except ValueError:
-                raise ValueError('bind_addr must be a (host, port) tuple '
-                                 '(for TCP sockets) or a string (for Unix '
-                                 'domain sockets), not %r' % value)
-
-    def base(self):
-        """Return the base for this server.
-
-        e.i. scheme://host[:port] or sock file
-        """
-        if self.socket_file:
-            return self.socket_file
-
-        host = self.socket_host
-        if host in ('0.0.0.0', '::'):
-            # 0.0.0.0 is INADDR_ANY and :: is IN6ADDR_ANY.
-            # Look up the host name, which should be the
-            # safest thing to spit out in a URL.
-            import socket
-            host = socket.gethostname()
-
-        port = self.socket_port
-
-        if self.ssl_certificate:
-            scheme = 'https'
-            if port != 443:
-                host += ':%s' % port
-        else:
-            scheme = 'http'
-            if port != 80:
-                host += ':%s' % port
-
-        return '%s://%s' % (scheme, host)
diff --git a/libraries/cherrypy/_cptools.py b/libraries/cherrypy/_cptools.py
deleted file mode 100644
index 57460285..00000000
--- a/libraries/cherrypy/_cptools.py
+++ /dev/null
@@ -1,509 +0,0 @@
-"""CherryPy tools. A "tool" is any helper, adapted to CP.
-
-Tools are usually designed to be used in a variety of ways (although some
-may only offer one if they choose):
-
-    Library calls
-        All tools are callables that can be used wherever needed.
-        The arguments are straightforward and should be detailed within the
-        docstring.
-
-    Function decorators
-        All tools, when called, may be used as decorators which configure
-        individual CherryPy page handlers (methods on the CherryPy tree).
-        That is, "@tools.anytool()" should "turn on" the tool via the
-        decorated function's _cp_config attribute.
-
-    CherryPy config
-        If a tool exposes a "_setup" callable, it will be called
-        once per Request (if the feature is "turned on" via config).
-
-Tools may be implemented as any object with a namespace. The builtins
-are generally either modules or instances of the tools.Tool class.
-"""
-
-import six
-
-import cherrypy
-from cherrypy._helper import expose
-
-from cherrypy.lib import cptools, encoding, static, jsontools
-from cherrypy.lib import sessions as _sessions, xmlrpcutil as _xmlrpc
-from cherrypy.lib import caching as _caching
-from cherrypy.lib import auth_basic, auth_digest
-
-
-def _getargs(func):
-    """Return the names of all static arguments to the given function."""
-    # Use this instead of importing inspect for less mem overhead.
-    import types
-    if six.PY3:
-        if isinstance(func, types.MethodType):
-            func = func.__func__
-        co = func.__code__
-    else:
-        if isinstance(func, types.MethodType):
-            func = func.im_func
-        co = func.func_code
-    return co.co_varnames[:co.co_argcount]
-
-
-_attr_error = (
-    'CherryPy Tools cannot be turned on directly. Instead, turn them '
-    'on via config, or use them as decorators on your page handlers.'
-)
-
-
-class Tool(object):
-
-    """A registered function for use with CherryPy request-processing hooks.
-
-    help(tool.callable) should give you more information about this Tool.
-    """
-
-    namespace = 'tools'
-
-    def __init__(self, point, callable, name=None, priority=50):
-        self._point = point
-        self.callable = callable
-        self._name = name
-        self._priority = priority
-        self.__doc__ = self.callable.__doc__
-        self._setargs()
-
-    @property
-    def on(self):
-        raise AttributeError(_attr_error)
-
-    @on.setter
-    def on(self, value):
-        raise AttributeError(_attr_error)
-
-    def _setargs(self):
-        """Copy func parameter names to obj attributes."""
-        try:
-            for arg in _getargs(self.callable):
-                setattr(self, arg, None)
-        except (TypeError, AttributeError):
-            if hasattr(self.callable, '__call__'):
-                for arg in _getargs(self.callable.__call__):
-                    setattr(self, arg, None)
-        # IronPython 1.0 raises NotImplementedError because
-        # inspect.getargspec tries to access Python bytecode
-        # in co_code attribute.
-        except NotImplementedError:
-            pass
-        # IronPython 1B1 may raise IndexError in some cases,
-        # but if we trap it here it doesn't prevent CP from working.
-        except IndexError:
-            pass
-
-    def _merged_args(self, d=None):
-        """Return a dict of configuration entries for this Tool."""
-        if d:
-            conf = d.copy()
-        else:
-            conf = {}
-
-        tm = cherrypy.serving.request.toolmaps[self.namespace]
-        if self._name in tm:
-            conf.update(tm[self._name])
-
-        if 'on' in conf:
-            del conf['on']
-
-        return conf
-
-    def __call__(self, *args, **kwargs):
-        """Compile-time decorator (turn on the tool in config).
-
-        For example::
-
-            @expose
-            @tools.proxy()
-            def whats_my_base(self):
-                return cherrypy.request.base
-        """
-        if args:
-            raise TypeError('The %r Tool does not accept positional '
-                            'arguments; you must use keyword arguments.'
-                            % self._name)
-
-        def tool_decorator(f):
-            if not hasattr(f, '_cp_config'):
-                f._cp_config = {}
-            subspace = self.namespace + '.' + self._name + '.'
-            f._cp_config[subspace + 'on'] = True
-            for k, v in kwargs.items():
-                f._cp_config[subspace + k] = v
-            return f
-        return tool_decorator
-
-    def _setup(self):
-        """Hook this tool into cherrypy.request.
-
-        The standard CherryPy request object will automatically call this
-        method when the tool is "turned on" in config.
-        """
-        conf = self._merged_args()
-        p = conf.pop('priority', None)
-        if p is None:
-            p = getattr(self.callable, 'priority', self._priority)
-        cherrypy.serving.request.hooks.attach(self._point, self.callable,
-                                              priority=p, **conf)
-
-
-class HandlerTool(Tool):
-
-    """Tool which is called 'before main', that may skip normal handlers.
-
-    If the tool successfully handles the request (by setting response.body),
-    if should return True. This will cause CherryPy to skip any 'normal' page
-    handler. If the tool did not handle the request, it should return False
-    to tell CherryPy to continue on and call the normal page handler. If the
-    tool is declared AS a page handler (see the 'handler' method), returning
-    False will raise NotFound.
-    """
-
-    def __init__(self, callable, name=None):
-        Tool.__init__(self, 'before_handler', callable, name)
-
-    def handler(self, *args, **kwargs):
-        """Use this tool as a CherryPy page handler.
-
-        For example::
-
-            class Root:
-                nav = tools.staticdir.handler(section="/nav", dir="nav",
-                                              root=absDir)
-        """
-        @expose
-        def handle_func(*a, **kw):
-            handled = self.callable(*args, **self._merged_args(kwargs))
-            if not handled:
-                raise cherrypy.NotFound()
-            return cherrypy.serving.response.body
-        return handle_func
-
-    def _wrapper(self, **kwargs):
-        if self.callable(**kwargs):
-            cherrypy.serving.request.handler = None
-
-    def _setup(self):
-        """Hook this tool into cherrypy.request.
-
-        The standard CherryPy request object will automatically call this
-        method when the tool is "turned on" in config.
-        """
-        conf = self._merged_args()
-        p = conf.pop('priority', None)
-        if p is None:
-            p = getattr(self.callable, 'priority', self._priority)
-        cherrypy.serving.request.hooks.attach(self._point, self._wrapper,
-                                              priority=p, **conf)
-
-
-class HandlerWrapperTool(Tool):
-
-    """Tool which wraps request.handler in a provided wrapper function.
-
-    The 'newhandler' arg must be a handler wrapper function that takes a
-    'next_handler' argument, plus ``*args`` and ``**kwargs``. Like all
-    page handler
-    functions, it must return an iterable for use as cherrypy.response.body.
-
-    For example, to allow your 'inner' page handlers to return dicts
-    which then get interpolated into a template::
-
-        def interpolator(next_handler, *args, **kwargs):
-            filename = cherrypy.request.config.get('template')
-            cherrypy.response.template = env.get_template(filename)
-            response_dict = next_handler(*args, **kwargs)
-            return cherrypy.response.template.render(**response_dict)
-        cherrypy.tools.jinja = HandlerWrapperTool(interpolator)
-    """
-
-    def __init__(self, newhandler, point='before_handler', name=None,
-                 priority=50):
-        self.newhandler = newhandler
-        self._point = point
-        self._name = name
-        self._priority = priority
-
-    def callable(self, *args, **kwargs):
-        innerfunc = cherrypy.serving.request.handler
-
-        def wrap(*args, **kwargs):
-            return self.newhandler(innerfunc, *args, **kwargs)
-        cherrypy.serving.request.handler = wrap
-
-
-class ErrorTool(Tool):
-
-    """Tool which is used to replace the default request.error_response."""
-
-    def __init__(self, callable, name=None):
-        Tool.__init__(self, None, callable, name)
-
-    def _wrapper(self):
-        self.callable(**self._merged_args())
-
-    def _setup(self):
-        """Hook this tool into cherrypy.request.
-
-        The standard CherryPy request object will automatically call this
-        method when the tool is "turned on" in config.
-        """
-        cherrypy.serving.request.error_response = self._wrapper
-
-
-#                              Builtin tools                              #
-
-
-class SessionTool(Tool):
-
-    """Session Tool for CherryPy.
-
-    sessions.locking
-        When 'implicit' (the default), the session will be locked for you,
-        just before running the page handler.
-
-        When 'early', the session will be locked before reading the request
-        body. This is off by default for safety reasons; for example,
-        a large upload would block the session, denying an AJAX
-        progress meter
-        (`issue <https://github.com/cherrypy/cherrypy/issues/630>`_).
-
-        When 'explicit' (or any other value), you need to call
-        cherrypy.session.acquire_lock() yourself before using
-        session data.
-    """
-
-    def __init__(self):
-        # _sessions.init must be bound after headers are read
-        Tool.__init__(self, 'before_request_body', _sessions.init)
-
-    def _lock_session(self):
-        cherrypy.serving.session.acquire_lock()
-
-    def _setup(self):
-        """Hook this tool into cherrypy.request.
-
-        The standard CherryPy request object will automatically call this
-        method when the tool is "turned on" in config.
-        """
-        hooks = cherrypy.serving.request.hooks
-
-        conf = self._merged_args()
-
-        p = conf.pop('priority', None)
-        if p is None:
-            p = getattr(self.callable, 'priority', self._priority)
-
-        hooks.attach(self._point, self.callable, priority=p, **conf)
-
-        locking = conf.pop('locking', 'implicit')
-        if locking == 'implicit':
-            hooks.attach('before_handler', self._lock_session)
-        elif locking == 'early':
-            # Lock before the request body (but after _sessions.init runs!)
-            hooks.attach('before_request_body', self._lock_session,
-                         priority=60)
-        else:
-            # Don't lock
-            pass
-
-        hooks.attach('before_finalize', _sessions.save)
-        hooks.attach('on_end_request', _sessions.close)
-
-    def regenerate(self):
-        """Drop the current session and make a new one (with a new id)."""
-        sess = cherrypy.serving.session
-        sess.regenerate()
-
-        # Grab cookie-relevant tool args
-        relevant = 'path', 'path_header', 'name', 'timeout', 'domain', 'secure'
-        conf = dict(
-            (k, v)
-            for k, v in self._merged_args().items()
-            if k in relevant
-        )
-        _sessions.set_response_cookie(**conf)
-
-
-class XMLRPCController(object):
-
-    """A Controller (page handler collection) for XML-RPC.
-
-    To use it, have your controllers subclass this base class (it will
-    turn on the tool for you).
-
-    You can also supply the following optional config entries::
-
-        tools.xmlrpc.encoding: 'utf-8'
-        tools.xmlrpc.allow_none: 0
-
-    XML-RPC is a rather discontinuous layer over HTTP; dispatching to the
-    appropriate handler must first be performed according to the URL, and
-    then a second dispatch step must take place according to the RPC method
-    specified in the request body. It also allows a superfluous "/RPC2"
-    prefix in the URL, supplies its own handler args in the body, and
-    requires a 200 OK "Fault" response instead of 404 when the desired
-    method is not found.
-
-    Therefore, XML-RPC cannot be implemented for CherryPy via a Tool alone.
-    This Controller acts as the dispatch target for the first half (based
-    on the URL); it then reads the RPC method from the request body and
-    does its own second dispatch step based on that method. It also reads
-    body params, and returns a Fault on error.
-
-    The XMLRPCDispatcher strips any /RPC2 prefix; if you aren't using /RPC2
-    in your URL's, you can safely skip turning on the XMLRPCDispatcher.
-    Otherwise, you need to use declare it in config::
-
-        request.dispatch: cherrypy.dispatch.XMLRPCDispatcher()
-    """
-
-    # Note we're hard-coding this into the 'tools' namespace. We could do
-    # a huge amount of work to make it relocatable, but the only reason why
-    # would be if someone actually disabled the default_toolbox. Meh.
-    _cp_config = {'tools.xmlrpc.on': True}
-
-    @expose
-    def default(self, *vpath, **params):
-        rpcparams, rpcmethod = _xmlrpc.process_body()
-
-        subhandler = self
-        for attr in str(rpcmethod).split('.'):
-            subhandler = getattr(subhandler, attr, None)
-
-        if subhandler and getattr(subhandler, 'exposed', False):
-            body = subhandler(*(vpath + rpcparams), **params)
-
-        else:
-            # https://github.com/cherrypy/cherrypy/issues/533
-            # if a method is not found, an xmlrpclib.Fault should be returned
-            # raising an exception here will do that; see
-            # cherrypy.lib.xmlrpcutil.on_error
-            raise Exception('method "%s" is not supported' % attr)
-
-        conf = cherrypy.serving.request.toolmaps['tools'].get('xmlrpc', {})
-        _xmlrpc.respond(body,
-                        conf.get('encoding', 'utf-8'),
-                        conf.get('allow_none', 0))
-        return cherrypy.serving.response.body
-
-
-class SessionAuthTool(HandlerTool):
-    pass
-
-
-class CachingTool(Tool):
-
-    """Caching Tool for CherryPy."""
-
-    def _wrapper(self, **kwargs):
-        request = cherrypy.serving.request
-        if _caching.get(**kwargs):
-            request.handler = None
-        else:
-            if request.cacheable:
-                # Note the devious technique here of adding hooks on the fly
-                request.hooks.attach('before_finalize', _caching.tee_output,
-                                     priority=100)
-    _wrapper.priority = 90
-
-    def _setup(self):
-        """Hook caching into cherrypy.request."""
-        conf = self._merged_args()
-
-        p = conf.pop('priority', None)
-        cherrypy.serving.request.hooks.attach('before_handler', self._wrapper,
-                                              priority=p, **conf)
-
-
-class Toolbox(object):
-
-    """A collection of Tools.
-
-    This object also functions as a config namespace handler for itself.
-    Custom toolboxes should be added to each Application's toolboxes dict.
-    """
-
-    def __init__(self, namespace):
-        self.namespace = namespace
-
-    def __setattr__(self, name, value):
-        # If the Tool._name is None, supply it from the attribute name.
-        if isinstance(value, Tool):
-            if value._name is None:
-                value._name = name
-            value.namespace = self.namespace
-        object.__setattr__(self, name, value)
-
-    def __enter__(self):
-        """Populate request.toolmaps from tools specified in config."""
-        cherrypy.serving.request.toolmaps[self.namespace] = map = {}
-
-        def populate(k, v):
-            toolname, arg = k.split('.', 1)
-            bucket = map.setdefault(toolname, {})
-            bucket[arg] = v
-        return populate
-
-    def __exit__(self, exc_type, exc_val, exc_tb):
-        """Run tool._setup() for each tool in our toolmap."""
-        map = cherrypy.serving.request.toolmaps.get(self.namespace)
-        if map:
-            for name, settings in map.items():
-                if settings.get('on', False):
-                    tool = getattr(self, name)
-                    tool._setup()
-
-    def register(self, point, **kwargs):
-        """
-        Return a decorator which registers the function
-        at the given hook point.
-        """
-        def decorator(func):
-            attr_name = kwargs.get('name', func.__name__)
-            tool = Tool(point, func, **kwargs)
-            setattr(self, attr_name, tool)
-            return func
-        return decorator
-
-
-default_toolbox = _d = Toolbox('tools')
-_d.session_auth = SessionAuthTool(cptools.session_auth)
-_d.allow = Tool('on_start_resource', cptools.allow)
-_d.proxy = Tool('before_request_body', cptools.proxy, priority=30)
-_d.response_headers = Tool('on_start_resource', cptools.response_headers)
-_d.log_tracebacks = Tool('before_error_response', cptools.log_traceback)
-_d.log_headers = Tool('before_error_response', cptools.log_request_headers)
-_d.log_hooks = Tool('on_end_request', cptools.log_hooks, priority=100)
-_d.err_redirect = ErrorTool(cptools.redirect)
-_d.etags = Tool('before_finalize', cptools.validate_etags, priority=75)
-_d.decode = Tool('before_request_body', encoding.decode)
-# the order of encoding, gzip, caching is important
-_d.encode = Tool('before_handler', encoding.ResponseEncoder, priority=70)
-_d.gzip = Tool('before_finalize', encoding.gzip, priority=80)
-_d.staticdir = HandlerTool(static.staticdir)
-_d.staticfile = HandlerTool(static.staticfile)
-_d.sessions = SessionTool()
-_d.xmlrpc = ErrorTool(_xmlrpc.on_error)
-_d.caching = CachingTool('before_handler', _caching.get, 'caching')
-_d.expires = Tool('before_finalize', _caching.expires)
-_d.ignore_headers = Tool('before_request_body', cptools.ignore_headers)
-_d.referer = Tool('before_request_body', cptools.referer)
-_d.trailing_slash = Tool('before_handler', cptools.trailing_slash, priority=60)
-_d.flatten = Tool('before_finalize', cptools.flatten)
-_d.accept = Tool('on_start_resource', cptools.accept)
-_d.redirect = Tool('on_start_resource', cptools.redirect)
-_d.autovary = Tool('on_start_resource', cptools.autovary, priority=0)
-_d.json_in = Tool('before_request_body', jsontools.json_in, priority=30)
-_d.json_out = Tool('before_handler', jsontools.json_out, priority=30)
-_d.auth_basic = Tool('before_handler', auth_basic.basic_auth, priority=1)
-_d.auth_digest = Tool('before_handler', auth_digest.digest_auth, priority=1)
-_d.params = Tool('before_handler', cptools.convert_params, priority=15)
-
-del _d, cptools, encoding, static
diff --git a/libraries/cherrypy/_cptree.py b/libraries/cherrypy/_cptree.py
deleted file mode 100644
index ceb54379..00000000
--- a/libraries/cherrypy/_cptree.py
+++ /dev/null
@@ -1,313 +0,0 @@
-"""CherryPy Application and Tree objects."""
-
-import os
-
-import six
-
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy import _cpconfig, _cplogging, _cprequest, _cpwsgi, tools
-from cherrypy.lib import httputil, reprconf
-
-
-class Application(object):
-    """A CherryPy Application.
-
-    Servers and gateways should not instantiate Request objects directly.
-    Instead, they should ask an Application object for a request object.
-
-    An instance of this class may also be used as a WSGI callable
-    (WSGI application object) for itself.
-    """
-
-    root = None
-    """The top-most container of page handlers for this app. Handlers should
-    be arranged in a hierarchy of attributes, matching the expected URI
-    hierarchy; the default dispatcher then searches this hierarchy for a
-    matching handler. When using a dispatcher other than the default,
-    this value may be None."""
-
-    config = {}
-    """A dict of {path: pathconf} pairs, where 'pathconf' is itself a dict
-    of {key: value} pairs."""
-
-    namespaces = reprconf.NamespaceSet()
-    toolboxes = {'tools': cherrypy.tools}
-
-    log = None
-    """A LogManager instance. See _cplogging."""
-
-    wsgiapp = None
-    """A CPWSGIApp instance. See _cpwsgi."""
-
-    request_class = _cprequest.Request
-    response_class = _cprequest.Response
-
-    relative_urls = False
-
-    def __init__(self, root, script_name='', config=None):
-        """Initialize Application with given root."""
-        self.log = _cplogging.LogManager(id(self), cherrypy.log.logger_root)
-        self.root = root
-        self.script_name = script_name
-        self.wsgiapp = _cpwsgi.CPWSGIApp(self)
-
-        self.namespaces = self.namespaces.copy()
-        self.namespaces['log'] = lambda k, v: setattr(self.log, k, v)
-        self.namespaces['wsgi'] = self.wsgiapp.namespace_handler
-
-        self.config = self.__class__.config.copy()
-        if config:
-            self.merge(config)
-
-    def __repr__(self):
-        """Generate a representation of the Application instance."""
-        return '%s.%s(%r, %r)' % (self.__module__, self.__class__.__name__,
-                                  self.root, self.script_name)
-
-    script_name_doc = """The URI "mount point" for this app. A mount point
-    is that portion of the URI which is constant for all URIs that are
-    serviced by this application; it does not include scheme, host, or proxy
-    ("virtual host") portions of the URI.
-
-    For example, if script_name is "/my/cool/app", then the URL
-    "http://www.example.com/my/cool/app/page1" might be handled by a
-    "page1" method on the root object.
-
-    The value of script_name MUST NOT end in a slash. If the script_name
-    refers to the root of the URI, it MUST be an empty string (not "/").
-
-    If script_name is explicitly set to None, then the script_name will be
-    provided for each call from request.wsgi_environ['SCRIPT_NAME'].
-    """
-
-    @property
-    def script_name(self):  # noqa: D401; irrelevant for properties
-        """The URI "mount point" for this app.
-
-        A mount point is that portion of the URI which is constant for all URIs
-        that are serviced by this application; it does not include scheme,
-        host, or proxy ("virtual host") portions of the URI.
-
-        For example, if script_name is "/my/cool/app", then the URL
-        "http://www.example.com/my/cool/app/page1" might be handled by a
-        "page1" method on the root object.
-
-        The value of script_name MUST NOT end in a slash. If the script_name
-        refers to the root of the URI, it MUST be an empty string (not "/").
-
-        If script_name is explicitly set to None, then the script_name will be
-        provided for each call from request.wsgi_environ['SCRIPT_NAME'].
-        """
-        if self._script_name is not None:
-            return self._script_name
-
-        # A `_script_name` with a value of None signals that the script name
-        # should be pulled from WSGI environ.
-        return cherrypy.serving.request.wsgi_environ['SCRIPT_NAME'].rstrip('/')
-
-    @script_name.setter
-    def script_name(self, value):
-        if value:
-            value = value.rstrip('/')
-        self._script_name = value
-
-    def merge(self, config):
-        """Merge the given config into self.config."""
-        _cpconfig.merge(self.config, config)
-
-        # Handle namespaces specified in config.
-        self.namespaces(self.config.get('/', {}))
-
-    def find_config(self, path, key, default=None):
-        """Return the most-specific value for key along path, or default."""
-        trail = path or '/'
-        while trail:
-            nodeconf = self.config.get(trail, {})
-
-            if key in nodeconf:
-                return nodeconf[key]
-
-            lastslash = trail.rfind('/')
-            if lastslash == -1:
-                break
-            elif lastslash == 0 and trail != '/':
-                trail = '/'
-            else:
-                trail = trail[:lastslash]
-
-        return default
-
-    def get_serving(self, local, remote, scheme, sproto):
-        """Create and return a Request and Response object."""
-        req = self.request_class(local, remote, scheme, sproto)
-        req.app = self
-
-        for name, toolbox in self.toolboxes.items():
-            req.namespaces[name] = toolbox
-
-        resp = self.response_class()
-        cherrypy.serving.load(req, resp)
-        cherrypy.engine.publish('acquire_thread')
-        cherrypy.engine.publish('before_request')
-
-        return req, resp
-
-    def release_serving(self):
-        """Release the current serving (request and response)."""
-        req = cherrypy.serving.request
-
-        cherrypy.engine.publish('after_request')
-
-        try:
-            req.close()
-        except Exception:
-            cherrypy.log(traceback=True, severity=40)
-
-        cherrypy.serving.clear()
-
-    def __call__(self, environ, start_response):
-        """Call a WSGI-callable."""
-        return self.wsgiapp(environ, start_response)
-
-
-class Tree(object):
-    """A registry of CherryPy applications, mounted at diverse points.
-
-    An instance of this class may also be used as a WSGI callable
-    (WSGI application object), in which case it dispatches to all
-    mounted apps.
-    """
-
-    apps = {}
-    """
-    A dict of the form {script name: application}, where "script name"
-    is a string declaring the URI mount point (no trailing slash), and
-    "application" is an instance of cherrypy.Application (or an arbitrary
-    WSGI callable if you happen to be using a WSGI server)."""
-
-    def __init__(self):
-        """Initialize registry Tree."""
-        self.apps = {}
-
-    def mount(self, root, script_name='', config=None):
-        """Mount a new app from a root object, script_name, and config.
-
-        root
-            An instance of a "controller class" (a collection of page
-            handler methods) which represents the root of the application.
-            This may also be an Application instance, or None if using
-            a dispatcher other than the default.
-
-        script_name
-            A string containing the "mount point" of the application.
-            This should start with a slash, and be the path portion of the
-            URL at which to mount the given root. For example, if root.index()
-            will handle requests to "http://www.example.com:8080/dept/app1/",
-            then the script_name argument would be "/dept/app1".
-
-            It MUST NOT end in a slash. If the script_name refers to the
-            root of the URI, it MUST be an empty string (not "/").
-
-        config
-            A file or dict containing application config.
-        """
-        if script_name is None:
-            raise TypeError(
-                "The 'script_name' argument may not be None. Application "
-                'objects may, however, possess a script_name of None (in '
-                'order to inpect the WSGI environ for SCRIPT_NAME upon each '
-                'request). You cannot mount such Applications on this Tree; '
-                'you must pass them to a WSGI server interface directly.')
-
-        # Next line both 1) strips trailing slash and 2) maps "/" -> "".
-        script_name = script_name.rstrip('/')
-
-        if isinstance(root, Application):
-            app = root
-            if script_name != '' and script_name != app.script_name:
-                raise ValueError(
-                    'Cannot specify a different script name and pass an '
-                    'Application instance to cherrypy.mount')
-            script_name = app.script_name
-        else:
-            app = Application(root, script_name)
-
-            # If mounted at "", add favicon.ico
-            needs_favicon = (
-                script_name == ''
-                and root is not None
-                and not hasattr(root, 'favicon_ico')
-            )
-            if needs_favicon:
-                favicon = os.path.join(
-                    os.getcwd(),
-                    os.path.dirname(__file__),
-                    'favicon.ico',
-                )
-                root.favicon_ico = tools.staticfile.handler(favicon)
-
-        if config:
-            app.merge(config)
-
-        self.apps[script_name] = app
-
-        return app
-
-    def graft(self, wsgi_callable, script_name=''):
-        """Mount a wsgi callable at the given script_name."""
-        # Next line both 1) strips trailing slash and 2) maps "/" -> "".
-        script_name = script_name.rstrip('/')
-        self.apps[script_name] = wsgi_callable
-
-    def script_name(self, path=None):
-        """Return the script_name of the app at the given path, or None.
-
-        If path is None, cherrypy.request is used.
-        """
-        if path is None:
-            try:
-                request = cherrypy.serving.request
-                path = httputil.urljoin(request.script_name,
-                                        request.path_info)
-            except AttributeError:
-                return None
-
-        while True:
-            if path in self.apps:
-                return path
-
-            if path == '':
-                return None
-
-            # Move one node up the tree and try again.
-            path = path[:path.rfind('/')]
-
-    def __call__(self, environ, start_response):
-        """Pre-initialize WSGI env and call WSGI-callable."""
-        # If you're calling this, then you're probably setting SCRIPT_NAME
-        # to '' (some WSGI servers always set SCRIPT_NAME to '').
-        # Try to look up the app using the full path.
-        env1x = environ
-        if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
-            env1x = _cpwsgi.downgrade_wsgi_ux_to_1x(environ)
-        path = httputil.urljoin(env1x.get('SCRIPT_NAME', ''),
-                                env1x.get('PATH_INFO', ''))
-        sn = self.script_name(path or '/')
-        if sn is None:
-            start_response('404 Not Found', [])
-            return []
-
-        app = self.apps[sn]
-
-        # Correct the SCRIPT_NAME and PATH_INFO environ entries.
-        environ = environ.copy()
-        if six.PY2 and environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
-            # Python 2/WSGI u.0: all strings MUST be of type unicode
-            enc = environ[ntou('wsgi.url_encoding')]
-            environ[ntou('SCRIPT_NAME')] = sn.decode(enc)
-            environ[ntou('PATH_INFO')] = path[len(sn.rstrip('/')):].decode(enc)
-        else:
-            environ['SCRIPT_NAME'] = sn
-            environ['PATH_INFO'] = path[len(sn.rstrip('/')):]
-        return app(environ, start_response)
diff --git a/libraries/cherrypy/_cpwsgi.py b/libraries/cherrypy/_cpwsgi.py
deleted file mode 100644
index 0b4942ff..00000000
--- a/libraries/cherrypy/_cpwsgi.py
+++ /dev/null
@@ -1,467 +0,0 @@
-"""WSGI interface (see PEP 333 and 3333).
-
-Note that WSGI environ keys and values are 'native strings'; that is,
-whatever the type of "" is. For Python 2, that's a byte string; for Python 3,
-it's a unicode string. But PEP 3333 says: "even if Python's str type is
-actually Unicode "under the hood", the content of native strings must
-still be translatable to bytes via the Latin-1 encoding!"
-"""
-
-import sys as _sys
-import io
-
-import six
-
-import cherrypy as _cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy import _cperror
-from cherrypy.lib import httputil
-from cherrypy.lib import is_closable_iterator
-
-
-def downgrade_wsgi_ux_to_1x(environ):
-    """Return a new environ dict for WSGI 1.x from the given WSGI u.x environ.
-    """
-    env1x = {}
-
-    url_encoding = environ[ntou('wsgi.url_encoding')]
-    for k, v in list(environ.items()):
-        if k in [ntou('PATH_INFO'), ntou('SCRIPT_NAME'), ntou('QUERY_STRING')]:
-            v = v.encode(url_encoding)
-        elif isinstance(v, six.text_type):
-            v = v.encode('ISO-8859-1')
-        env1x[k.encode('ISO-8859-1')] = v
-
-    return env1x
-
-
-class VirtualHost(object):
-
-    """Select a different WSGI application based on the Host header.
-
-    This can be useful when running multiple sites within one CP server.
-    It allows several domains to point to different applications. For example::
-
-        root = Root()
-        RootApp = cherrypy.Application(root)
-        Domain2App = cherrypy.Application(root)
-        SecureApp = cherrypy.Application(Secure())
-
-        vhost = cherrypy._cpwsgi.VirtualHost(
-            RootApp,
-            domains={
-                'www.domain2.example': Domain2App,
-                'www.domain2.example:443': SecureApp,
-            },
-        )
-
-        cherrypy.tree.graft(vhost)
-    """
-    default = None
-    """Required. The default WSGI application."""
-
-    use_x_forwarded_host = True
-    """If True (the default), any "X-Forwarded-Host"
-    request header will be used instead of the "Host" header. This
-    is commonly added by HTTP servers (such as Apache) when proxying."""
-
-    domains = {}
-    """A dict of {host header value: application} pairs.
-    The incoming "Host" request header is looked up in this dict,
-    and, if a match is found, the corresponding WSGI application
-    will be called instead of the default. Note that you often need
-    separate entries for "example.com" and "www.example.com".
-    In addition, "Host" headers may contain the port number.
-    """
-
-    def __init__(self, default, domains=None, use_x_forwarded_host=True):
-        self.default = default
-        self.domains = domains or {}
-        self.use_x_forwarded_host = use_x_forwarded_host
-
-    def __call__(self, environ, start_response):
-        domain = environ.get('HTTP_HOST', '')
-        if self.use_x_forwarded_host:
-            domain = environ.get('HTTP_X_FORWARDED_HOST', domain)
-
-        nextapp = self.domains.get(domain)
-        if nextapp is None:
-            nextapp = self.default
-        return nextapp(environ, start_response)
-
-
-class InternalRedirector(object):
-
-    """WSGI middleware that handles raised cherrypy.InternalRedirect."""
-
-    def __init__(self, nextapp, recursive=False):
-        self.nextapp = nextapp
-        self.recursive = recursive
-
-    def __call__(self, environ, start_response):
-        redirections = []
-        while True:
-            environ = environ.copy()
-            try:
-                return self.nextapp(environ, start_response)
-            except _cherrypy.InternalRedirect:
-                ir = _sys.exc_info()[1]
-                sn = environ.get('SCRIPT_NAME', '')
-                path = environ.get('PATH_INFO', '')
-                qs = environ.get('QUERY_STRING', '')
-
-                # Add the *previous* path_info + qs to redirections.
-                old_uri = sn + path
-                if qs:
-                    old_uri += '?' + qs
-                redirections.append(old_uri)
-
-                if not self.recursive:
-                    # Check to see if the new URI has been redirected to
-                    # already
-                    new_uri = sn + ir.path
-                    if ir.query_string:
-                        new_uri += '?' + ir.query_string
-                    if new_uri in redirections:
-                        ir.request.close()
-                        tmpl = (
-                            'InternalRedirector visited the same URL twice: %r'
-                        )
-                        raise RuntimeError(tmpl % new_uri)
-
-                # Munge the environment and try again.
-                environ['REQUEST_METHOD'] = 'GET'
-                environ['PATH_INFO'] = ir.path
-                environ['QUERY_STRING'] = ir.query_string
-                environ['wsgi.input'] = io.BytesIO()
-                environ['CONTENT_LENGTH'] = '0'
-                environ['cherrypy.previous_request'] = ir.request
-
-
-class ExceptionTrapper(object):
-
-    """WSGI middleware that traps exceptions."""
-
-    def __init__(self, nextapp, throws=(KeyboardInterrupt, SystemExit)):
-        self.nextapp = nextapp
-        self.throws = throws
-
-    def __call__(self, environ, start_response):
-        return _TrappedResponse(
-            self.nextapp,
-            environ,
-            start_response,
-            self.throws
-        )
-
-
-class _TrappedResponse(object):
-
-    response = iter([])
-
-    def __init__(self, nextapp, environ, start_response, throws):
-        self.nextapp = nextapp
-        self.environ = environ
-        self.start_response = start_response
-        self.throws = throws
-        self.started_response = False
-        self.response = self.trap(
-            self.nextapp, self.environ, self.start_response,
-        )
-        self.iter_response = iter(self.response)
-
-    def __iter__(self):
-        self.started_response = True
-        return self
-
-    def __next__(self):
-        return self.trap(next, self.iter_response)
-
-    # todo: https://pythonhosted.org/six/#six.Iterator
-    if six.PY2:
-        next = __next__
-
-    def close(self):
-        if hasattr(self.response, 'close'):
-            self.response.close()
-
-    def trap(self, func, *args, **kwargs):
-        try:
-            return func(*args, **kwargs)
-        except self.throws:
-            raise
-        except StopIteration:
-            raise
-        except Exception:
-            tb = _cperror.format_exc()
-            _cherrypy.log(tb, severity=40)
-            if not _cherrypy.request.show_tracebacks:
-                tb = ''
-            s, h, b = _cperror.bare_error(tb)
-            if six.PY3:
-                # What fun.
-                s = s.decode('ISO-8859-1')
-                h = [
-                    (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
-                    for k, v in h
-                ]
-            if self.started_response:
-                # Empty our iterable (so future calls raise StopIteration)
-                self.iter_response = iter([])
-            else:
-                self.iter_response = iter(b)
-
-            try:
-                self.start_response(s, h, _sys.exc_info())
-            except Exception:
-                # "The application must not trap any exceptions raised by
-                # start_response, if it called start_response with exc_info.
-                # Instead, it should allow such exceptions to propagate
-                # back to the server or gateway."
-                # But we still log and call close() to clean up ourselves.
-                _cherrypy.log(traceback=True, severity=40)
-                raise
-
-            if self.started_response:
-                return b''.join(b)
-            else:
-                return b
-
-
-#                           WSGI-to-CP Adapter                           #
-
-
-class AppResponse(object):
-
-    """WSGI response iterable for CherryPy applications."""
-
-    def __init__(self, environ, start_response, cpapp):
-        self.cpapp = cpapp
-        try:
-            if six.PY2:
-                if environ.get(ntou('wsgi.version')) == (ntou('u'), 0):
-                    environ = downgrade_wsgi_ux_to_1x(environ)
-            self.environ = environ
-            self.run()
-
-            r = _cherrypy.serving.response
-
-            outstatus = r.output_status
-            if not isinstance(outstatus, bytes):
-                raise TypeError('response.output_status is not a byte string.')
-
-            outheaders = []
-            for k, v in r.header_list:
-                if not isinstance(k, bytes):
-                    tmpl = 'response.header_list key %r is not a byte string.'
-                    raise TypeError(tmpl % k)
-                if not isinstance(v, bytes):
-                    tmpl = (
-                        'response.header_list value %r is not a byte string.'
-                    )
-                    raise TypeError(tmpl % v)
-                outheaders.append((k, v))
-
-            if six.PY3:
-                # According to PEP 3333, when using Python 3, the response
-                # status and headers must be bytes masquerading as unicode;
-                # that is, they must be of type "str" but are restricted to
-                # code points in the "latin-1" set.
-                outstatus = outstatus.decode('ISO-8859-1')
-                outheaders = [
-                    (k.decode('ISO-8859-1'), v.decode('ISO-8859-1'))
-                    for k, v in outheaders
-                ]
-
-            self.iter_response = iter(r.body)
-            self.write = start_response(outstatus, outheaders)
-        except BaseException:
-            self.close()
-            raise
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        return next(self.iter_response)
-
-    # todo: https://pythonhosted.org/six/#six.Iterator
-    if six.PY2:
-        next = __next__
-
-    def close(self):
-        """Close and de-reference the current request and response. (Core)"""
-        streaming = _cherrypy.serving.response.stream
-        self.cpapp.release_serving()
-
-        # We avoid the expense of examining the iterator to see if it's
-        # closable unless we are streaming the response, as that's the
-        # only situation where we are going to have an iterator which
-        # may not have been exhausted yet.
-        if streaming and is_closable_iterator(self.iter_response):
-            iter_close = self.iter_response.close
-            try:
-                iter_close()
-            except Exception:
-                _cherrypy.log(traceback=True, severity=40)
-
-    def run(self):
-        """Create a Request object using environ."""
-        env = self.environ.get
-
-        local = httputil.Host(
-            '',
-            int(env('SERVER_PORT', 80) or -1),
-            env('SERVER_NAME', ''),
-        )
-        remote = httputil.Host(
-            env('REMOTE_ADDR', ''),
-            int(env('REMOTE_PORT', -1) or -1),
-            env('REMOTE_HOST', ''),
-        )
-        scheme = env('wsgi.url_scheme')
-        sproto = env('ACTUAL_SERVER_PROTOCOL', 'HTTP/1.1')
-        request, resp = self.cpapp.get_serving(local, remote, scheme, sproto)
-
-        # LOGON_USER is served by IIS, and is the name of the
-        # user after having been mapped to a local account.
-        # Both IIS and Apache set REMOTE_USER, when possible.
-        request.login = env('LOGON_USER') or env('REMOTE_USER') or None
-        request.multithread = self.environ['wsgi.multithread']
-        request.multiprocess = self.environ['wsgi.multiprocess']
-        request.wsgi_environ = self.environ
-        request.prev = env('cherrypy.previous_request', None)
-
-        meth = self.environ['REQUEST_METHOD']
-
-        path = httputil.urljoin(
-            self.environ.get('SCRIPT_NAME', ''),
-            self.environ.get('PATH_INFO', ''),
-        )
-        qs = self.environ.get('QUERY_STRING', '')
-
-        path, qs = self.recode_path_qs(path, qs) or (path, qs)
-
-        rproto = self.environ.get('SERVER_PROTOCOL')
-        headers = self.translate_headers(self.environ)
-        rfile = self.environ['wsgi.input']
-        request.run(meth, path, qs, rproto, headers, rfile)
-
-    headerNames = {
-        'HTTP_CGI_AUTHORIZATION': 'Authorization',
-        'CONTENT_LENGTH': 'Content-Length',
-        'CONTENT_TYPE': 'Content-Type',
-        'REMOTE_HOST': 'Remote-Host',
-        'REMOTE_ADDR': 'Remote-Addr',
-    }
-
-    def recode_path_qs(self, path, qs):
-        if not six.PY3:
-            return
-
-        # This isn't perfect; if the given PATH_INFO is in the
-        # wrong encoding, it may fail to match the appropriate config
-        # section URI. But meh.
-        old_enc = self.environ.get('wsgi.url_encoding', 'ISO-8859-1')
-        new_enc = self.cpapp.find_config(
-            self.environ.get('PATH_INFO', ''),
-            'request.uri_encoding', 'utf-8',
-        )
-        if new_enc.lower() == old_enc.lower():
-            return
-
-        # Even though the path and qs are unicode, the WSGI server
-        # is required by PEP 3333 to coerce them to ISO-8859-1
-        # masquerading as unicode. So we have to encode back to
-        # bytes and then decode again using the "correct" encoding.
-        try:
-            return (
-                path.encode(old_enc).decode(new_enc),
-                qs.encode(old_enc).decode(new_enc),
-            )
-        except (UnicodeEncodeError, UnicodeDecodeError):
-            # Just pass them through without transcoding and hope.
-            pass
-
-    def translate_headers(self, environ):
-        """Translate CGI-environ header names to HTTP header names."""
-        for cgiName in environ:
-            # We assume all incoming header keys are uppercase already.
-            if cgiName in self.headerNames:
-                yield self.headerNames[cgiName], environ[cgiName]
-            elif cgiName[:5] == 'HTTP_':
-                # Hackish attempt at recovering original header names.
-                translatedHeader = cgiName[5:].replace('_', '-')
-                yield translatedHeader, environ[cgiName]
-
-
-class CPWSGIApp(object):
-
-    """A WSGI application object for a CherryPy Application."""
-
-    pipeline = [
-        ('ExceptionTrapper', ExceptionTrapper),
-        ('InternalRedirector', InternalRedirector),
-    ]
-    """A list of (name, wsgiapp) pairs. Each 'wsgiapp' MUST be a
-    constructor that takes an initial, positional 'nextapp' argument,
-    plus optional keyword arguments, and returns a WSGI application
-    (that takes environ and start_response arguments). The 'name' can
-    be any you choose, and will correspond to keys in self.config."""
-
-    head = None
-    """Rather than nest all apps in the pipeline on each call, it's only
-    done the first time, and the result is memoized into self.head. Set
-    this to None again if you change self.pipeline after calling self."""
-
-    config = {}
-    """A dict whose keys match names listed in the pipeline. Each
-    value is a further dict which will be passed to the corresponding
-    named WSGI callable (from the pipeline) as keyword arguments."""
-
-    response_class = AppResponse
-    """The class to instantiate and return as the next app in the WSGI chain.
-    """
-
-    def __init__(self, cpapp, pipeline=None):
-        self.cpapp = cpapp
-        self.pipeline = self.pipeline[:]
-        if pipeline:
-            self.pipeline.extend(pipeline)
-        self.config = self.config.copy()
-
-    def tail(self, environ, start_response):
-        """WSGI application callable for the actual CherryPy application.
-
-        You probably shouldn't call this; call self.__call__ instead,
-        so that any WSGI middleware in self.pipeline can run first.
-        """
-        return self.response_class(environ, start_response, self.cpapp)
-
-    def __call__(self, environ, start_response):
-        head = self.head
-        if head is None:
-            # Create and nest the WSGI apps in our pipeline (in reverse order).
-            # Then memoize the result in self.head.
-            head = self.tail
-            for name, callable in self.pipeline[::-1]:
-                conf = self.config.get(name, {})
-                head = callable(head, **conf)
-            self.head = head
-        return head(environ, start_response)
-
-    def namespace_handler(self, k, v):
-        """Config handler for the 'wsgi' namespace."""
-        if k == 'pipeline':
-            # Note this allows multiple 'wsgi.pipeline' config entries
-            # (but each entry will be processed in a 'random' order).
-            # It should also allow developers to set default middleware
-            # in code (passed to self.__init__) that deployers can add to
-            # (but not remove) via config.
-            self.pipeline.extend(v)
-        elif k == 'response_class':
-            self.response_class = v
-        else:
-            name, arg = k.split('.', 1)
-            bucket = self.config.setdefault(name, {})
-            bucket[arg] = v
diff --git a/libraries/cherrypy/_cpwsgi_server.py b/libraries/cherrypy/_cpwsgi_server.py
deleted file mode 100644
index 11dd846a..00000000
--- a/libraries/cherrypy/_cpwsgi_server.py
+++ /dev/null
@@ -1,110 +0,0 @@
-"""
-WSGI server interface (see PEP 333).
-
-This adds some CP-specific bits to the framework-agnostic cheroot package.
-"""
-import sys
-
-import cheroot.wsgi
-import cheroot.server
-
-import cherrypy
-
-
-class CPWSGIHTTPRequest(cheroot.server.HTTPRequest):
-    """Wrapper for cheroot.server.HTTPRequest.
-
-    This is a layer, which preserves URI parsing mode like it which was
-    before Cheroot v5.8.0.
-    """
-
-    def __init__(self, server, conn):
-        """Initialize HTTP request container instance.
-
-        Args:
-            server (cheroot.server.HTTPServer):
-                web server object receiving this request
-            conn (cheroot.server.HTTPConnection):
-                HTTP connection object for this request
-        """
-        super(CPWSGIHTTPRequest, self).__init__(
-            server, conn, proxy_mode=True
-        )
-
-
-class CPWSGIServer(cheroot.wsgi.Server):
-    """Wrapper for cheroot.wsgi.Server.
-
-    cheroot has been designed to not reference CherryPy in any way,
-    so that it can be used in other frameworks and applications. Therefore,
-    we wrap it here, so we can set our own mount points from cherrypy.tree
-    and apply some attributes from config -> cherrypy.server -> wsgi.Server.
-    """
-
-    fmt = 'CherryPy/{cherrypy.__version__} {cheroot.wsgi.Server.version}'
-    version = fmt.format(**globals())
-
-    def __init__(self, server_adapter=cherrypy.server):
-        """Initialize CPWSGIServer instance.
-
-        Args:
-            server_adapter (cherrypy._cpserver.Server): ...
-        """
-        self.server_adapter = server_adapter
-        self.max_request_header_size = (
-            self.server_adapter.max_request_header_size or 0
-        )
-        self.max_request_body_size = (
-            self.server_adapter.max_request_body_size or 0
-        )
-
-        server_name = (self.server_adapter.socket_host or
-                       self.server_adapter.socket_file or
-                       None)
-
-        self.wsgi_version = self.server_adapter.wsgi_version
-
-        super(CPWSGIServer, self).__init__(
-            server_adapter.bind_addr, cherrypy.tree,
-            self.server_adapter.thread_pool,
-            server_name,
-            max=self.server_adapter.thread_pool_max,
-            request_queue_size=self.server_adapter.socket_queue_size,
-            timeout=self.server_adapter.socket_timeout,
-            shutdown_timeout=self.server_adapter.shutdown_timeout,
-            accepted_queue_size=self.server_adapter.accepted_queue_size,
-            accepted_queue_timeout=self.server_adapter.accepted_queue_timeout,
-            peercreds_enabled=self.server_adapter.peercreds,
-            peercreds_resolve_enabled=self.server_adapter.peercreds_resolve,
-        )
-        self.ConnectionClass.RequestHandlerClass = CPWSGIHTTPRequest
-
-        self.protocol = self.server_adapter.protocol_version
-        self.nodelay = self.server_adapter.nodelay
-
-        if sys.version_info >= (3, 0):
-            ssl_module = self.server_adapter.ssl_module or 'builtin'
-        else:
-            ssl_module = self.server_adapter.ssl_module or 'pyopenssl'
-        if self.server_adapter.ssl_context:
-            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
-            self.ssl_adapter = adapter_class(
-                self.server_adapter.ssl_certificate,
-                self.server_adapter.ssl_private_key,
-                self.server_adapter.ssl_certificate_chain,
-                self.server_adapter.ssl_ciphers)
-            self.ssl_adapter.context = self.server_adapter.ssl_context
-        elif self.server_adapter.ssl_certificate:
-            adapter_class = cheroot.server.get_ssl_adapter_class(ssl_module)
-            self.ssl_adapter = adapter_class(
-                self.server_adapter.ssl_certificate,
-                self.server_adapter.ssl_private_key,
-                self.server_adapter.ssl_certificate_chain,
-                self.server_adapter.ssl_ciphers)
-
-        self.stats['Enabled'] = getattr(
-            self.server_adapter, 'statistics', False)
-
-    def error_log(self, msg='', level=20, traceback=False):
-        """Write given message to the error log."""
-        cherrypy.engine.log(msg, level, traceback)
diff --git a/libraries/cherrypy/_helper.py b/libraries/cherrypy/_helper.py
deleted file mode 100644
index 314550cb..00000000
--- a/libraries/cherrypy/_helper.py
+++ /dev/null
@@ -1,344 +0,0 @@
-"""Helper functions for CP apps."""
-
-import six
-from six.moves import urllib
-
-from cherrypy._cpcompat import text_or_bytes
-
-import cherrypy
-
-
-def expose(func=None, alias=None):
-    """Expose the function or class.
-
-    Optionally provide an alias or set of aliases.
-    """
-    def expose_(func):
-        func.exposed = True
-        if alias is not None:
-            if isinstance(alias, text_or_bytes):
-                parents[alias.replace('.', '_')] = func
-            else:
-                for a in alias:
-                    parents[a.replace('.', '_')] = func
-        return func
-
-    import sys
-    import types
-    decoratable_types = types.FunctionType, types.MethodType, type,
-    if six.PY2:
-        # Old-style classes are type types.ClassType.
-        decoratable_types += types.ClassType,
-    if isinstance(func, decoratable_types):
-        if alias is None:
-            # @expose
-            func.exposed = True
-            return func
-        else:
-            # func = expose(func, alias)
-            parents = sys._getframe(1).f_locals
-            return expose_(func)
-    elif func is None:
-        if alias is None:
-            # @expose()
-            parents = sys._getframe(1).f_locals
-            return expose_
-        else:
-            # @expose(alias="alias") or
-            # @expose(alias=["alias1", "alias2"])
-            parents = sys._getframe(1).f_locals
-            return expose_
-    else:
-        # @expose("alias") or
-        # @expose(["alias1", "alias2"])
-        parents = sys._getframe(1).f_locals
-        alias = func
-        return expose_
-
-
-def popargs(*args, **kwargs):
-    """Decorate _cp_dispatch.
-
-    (cherrypy.dispatch.Dispatcher.dispatch_method_name)
-
-    Optional keyword argument: handler=(Object or Function)
-
-    Provides a _cp_dispatch function that pops off path segments into
-    cherrypy.request.params under the names specified.  The dispatch
-    is then forwarded on to the next vpath element.
-
-    Note that any existing (and exposed) member function of the class that
-    popargs is applied to will override that value of the argument.  For
-    instance, if you have a method named "list" on the class decorated with
-    popargs, then accessing "/list" will call that function instead of popping
-    it off as the requested parameter.  This restriction applies to all
-    _cp_dispatch functions.  The only way around this restriction is to create
-    a "blank class" whose only function is to provide _cp_dispatch.
-
-    If there are path elements after the arguments, or more arguments
-    are requested than are available in the vpath, then the 'handler'
-    keyword argument specifies the next object to handle the parameterized
-    request.  If handler is not specified or is None, then self is used.
-    If handler is a function rather than an instance, then that function
-    will be called with the args specified and the return value from that
-    function used as the next object INSTEAD of adding the parameters to
-    cherrypy.request.args.
-
-    This decorator may be used in one of two ways:
-
-    As a class decorator:
-    @cherrypy.popargs('year', 'month', 'day')
-    class Blog:
-        def index(self, year=None, month=None, day=None):
-            #Process the parameters here; any url like
-            #/, /2009, /2009/12, or /2009/12/31
-            #will fill in the appropriate parameters.
-
-        def create(self):
-            #This link will still be available at /create.  Defined functions
-            #take precedence over arguments.
-
-    Or as a member of a class:
-    class Blog:
-        _cp_dispatch = cherrypy.popargs('year', 'month', 'day')
-        #...
-
-    The handler argument may be used to mix arguments with built in functions.
-    For instance, the following setup allows different activities at the
-    day, month, and year level:
-
-    class DayHandler:
-        def index(self, year, month, day):
-            #Do something with this day; probably list entries
-
-        def delete(self, year, month, day):
-            #Delete all entries for this day
-
-    @cherrypy.popargs('day', handler=DayHandler())
-    class MonthHandler:
-        def index(self, year, month):
-            #Do something with this month; probably list entries
-
-        def delete(self, year, month):
-            #Delete all entries for this month
-
-    @cherrypy.popargs('month', handler=MonthHandler())
-    class YearHandler:
-        def index(self, year):
-            #Do something with this year
-
-        #...
-
-    @cherrypy.popargs('year', handler=YearHandler())
-    class Root:
-        def index(self):
-            #...
-
-    """
-    # Since keyword arg comes after *args, we have to process it ourselves
-    # for lower versions of python.
-
-    handler = None
-    handler_call = False
-    for k, v in kwargs.items():
-        if k == 'handler':
-            handler = v
-        else:
-            tm = "cherrypy.popargs() got an unexpected keyword argument '{0}'"
-            raise TypeError(tm.format(k))
-
-    import inspect
-
-    if handler is not None \
-            and (hasattr(handler, '__call__') or inspect.isclass(handler)):
-        handler_call = True
-
-    def decorated(cls_or_self=None, vpath=None):
-        if inspect.isclass(cls_or_self):
-            # cherrypy.popargs is a class decorator
-            cls = cls_or_self
-            name = cherrypy.dispatch.Dispatcher.dispatch_method_name
-            setattr(cls, name, decorated)
-            return cls
-
-        # We're in the actual function
-        self = cls_or_self
-        parms = {}
-        for arg in args:
-            if not vpath:
-                break
-            parms[arg] = vpath.pop(0)
-
-        if handler is not None:
-            if handler_call:
-                return handler(**parms)
-            else:
-                cherrypy.request.params.update(parms)
-                return handler
-
-        cherrypy.request.params.update(parms)
-
-        # If we are the ultimate handler, then to prevent our _cp_dispatch
-        # from being called again, we will resolve remaining elements through
-        # getattr() directly.
-        if vpath:
-            return getattr(self, vpath.pop(0), None)
-        else:
-            return self
-
-    return decorated
-
-
-def url(path='', qs='', script_name=None, base=None, relative=None):
-    """Create an absolute URL for the given path.
-
-    If 'path' starts with a slash ('/'), this will return
-        (base + script_name + path + qs).
-    If it does not start with a slash, this returns
-        (base + script_name [+ request.path_info] + path + qs).
-
-    If script_name is None, cherrypy.request will be used
-    to find a script_name, if available.
-
-    If base is None, cherrypy.request.base will be used (if available).
-    Note that you can use cherrypy.tools.proxy to change this.
-
-    Finally, note that this function can be used to obtain an absolute URL
-    for the current request path (minus the querystring) by passing no args.
-    If you call url(qs=cherrypy.request.query_string), you should get the
-    original browser URL (assuming no internal redirections).
-
-    If relative is None or not provided, request.app.relative_urls will
-    be used (if available, else False). If False, the output will be an
-    absolute URL (including the scheme, host, vhost, and script_name).
-    If True, the output will instead be a URL that is relative to the
-    current request path, perhaps including '..' atoms. If relative is
-    the string 'server', the output will instead be a URL that is
-    relative to the server root; i.e., it will start with a slash.
-    """
-    if isinstance(qs, (tuple, list, dict)):
-        qs = urllib.parse.urlencode(qs)
-    if qs:
-        qs = '?' + qs
-
-    if cherrypy.request.app:
-        if not path.startswith('/'):
-            # Append/remove trailing slash from path_info as needed
-            # (this is to support mistyped URL's without redirecting;
-            # if you want to redirect, use tools.trailing_slash).
-            pi = cherrypy.request.path_info
-            if cherrypy.request.is_index is True:
-                if not pi.endswith('/'):
-                    pi = pi + '/'
-            elif cherrypy.request.is_index is False:
-                if pi.endswith('/') and pi != '/':
-                    pi = pi[:-1]
-
-            if path == '':
-                path = pi
-            else:
-                path = urllib.parse.urljoin(pi, path)
-
-        if script_name is None:
-            script_name = cherrypy.request.script_name
-        if base is None:
-            base = cherrypy.request.base
-
-        newurl = base + script_name + normalize_path(path) + qs
-    else:
-        # No request.app (we're being called outside a request).
-        # We'll have to guess the base from server.* attributes.
-        # This will produce very different results from the above
-        # if you're using vhosts or tools.proxy.
-        if base is None:
-            base = cherrypy.server.base()
-
-        path = (script_name or '') + path
-        newurl = base + normalize_path(path) + qs
-
-    # At this point, we should have a fully-qualified absolute URL.
-
-    if relative is None:
-        relative = getattr(cherrypy.request.app, 'relative_urls', False)
-
-    # See http://www.ietf.org/rfc/rfc2396.txt
-    if relative == 'server':
-        # "A relative reference beginning with a single slash character is
-        # termed an absolute-path reference, as defined by <abs_path>..."
-        # This is also sometimes called "server-relative".
-        newurl = '/' + '/'.join(newurl.split('/', 3)[3:])
-    elif relative:
-        # "A relative reference that does not begin with a scheme name
-        # or a slash character is termed a relative-path reference."
-        old = url(relative=False).split('/')[:-1]
-        new = newurl.split('/')
-        while old and new:
-            a, b = old[0], new[0]
-            if a != b:
-                break
-            old.pop(0)
-            new.pop(0)
-        new = (['..'] * len(old)) + new
-        newurl = '/'.join(new)
-
-    return newurl
-
-
-def normalize_path(path):
-    """Resolve given path from relative into absolute form."""
-    if './' not in path:
-        return path
-
-    # Normalize the URL by removing ./ and ../
-    atoms = []
-    for atom in path.split('/'):
-        if atom == '.':
-            pass
-        elif atom == '..':
-            # Don't pop from empty list
-            # (i.e. ignore redundant '..')
-            if atoms:
-                atoms.pop()
-        elif atom:
-            atoms.append(atom)
-
-    newpath = '/'.join(atoms)
-    # Preserve leading '/'
-    if path.startswith('/'):
-        newpath = '/' + newpath
-
-    return newpath
-
-
-####
-# Inlined from jaraco.classes 1.4.3
-# Ref #1673
-class _ClassPropertyDescriptor(object):
-    """Descript for read-only class-based property.
-
-    Turns a classmethod-decorated func into a read-only property of that class
-    type (means the value cannot be set).
-    """
-
-    def __init__(self, fget, fset=None):
-        """Initialize a class property descriptor.
-
-        Instantiated by ``_helper.classproperty``.
-        """
-        self.fget = fget
-        self.fset = fset
-
-    def __get__(self, obj, klass=None):
-        """Return property value."""
-        if klass is None:
-            klass = type(obj)
-        return self.fget.__get__(obj, klass)()
-
-
-def classproperty(func):  # noqa: D401; irrelevant for properties
-    """Decorator like classmethod to implement a static class property."""
-    if not isinstance(func, (classmethod, staticmethod)):
-        func = classmethod(func)
-
-    return _ClassPropertyDescriptor(func)
-####
diff --git a/libraries/cherrypy/daemon.py b/libraries/cherrypy/daemon.py
deleted file mode 100644
index 74488c06..00000000
--- a/libraries/cherrypy/daemon.py
+++ /dev/null
@@ -1,107 +0,0 @@
-"""The CherryPy daemon."""
-
-import sys
-
-import cherrypy
-from cherrypy.process import plugins, servers
-from cherrypy import Application
-
-
-def start(configfiles=None, daemonize=False, environment=None,
-          fastcgi=False, scgi=False, pidfile=None, imports=None,
-          cgi=False):
-    """Subscribe all engine plugins and start the engine."""
-    sys.path = [''] + sys.path
-    for i in imports or []:
-        exec('import %s' % i)
-
-    for c in configfiles or []:
-        cherrypy.config.update(c)
-        # If there's only one app mounted, merge config into it.
-        if len(cherrypy.tree.apps) == 1:
-            for app in cherrypy.tree.apps.values():
-                if isinstance(app, Application):
-                    app.merge(c)
-
-    engine = cherrypy.engine
-
-    if environment is not None:
-        cherrypy.config.update({'environment': environment})
-
-    # Only daemonize if asked to.
-    if daemonize:
-        # Don't print anything to stdout/sterr.
-        cherrypy.config.update({'log.screen': False})
-        plugins.Daemonizer(engine).subscribe()
-
-    if pidfile:
-        plugins.PIDFile(engine, pidfile).subscribe()
-
-    if hasattr(engine, 'signal_handler'):
-        engine.signal_handler.subscribe()
-    if hasattr(engine, 'console_control_handler'):
-        engine.console_control_handler.subscribe()
-
-    if (fastcgi and (scgi or cgi)) or (scgi and cgi):
-        cherrypy.log.error('You may only specify one of the cgi, fastcgi, and '
-                           'scgi options.', 'ENGINE')
-        sys.exit(1)
-    elif fastcgi or scgi or cgi:
-        # Turn off autoreload when using *cgi.
-        cherrypy.config.update({'engine.autoreload.on': False})
-        # Turn off the default HTTP server (which is subscribed by default).
-        cherrypy.server.unsubscribe()
-
-        addr = cherrypy.server.bind_addr
-        cls = (
-            servers.FlupFCGIServer if fastcgi else
-            servers.FlupSCGIServer if scgi else
-            servers.FlupCGIServer
-        )
-        f = cls(application=cherrypy.tree, bindAddress=addr)
-        s = servers.ServerAdapter(engine, httpserver=f, bind_addr=addr)
-        s.subscribe()
-
-    # Always start the engine; this will start all other services
-    try:
-        engine.start()
-    except Exception:
-        # Assume the error has been logged already via bus.log.
-        sys.exit(1)
-    else:
-        engine.block()
-
-
-def run():
-    """Run cherryd CLI."""
-    from optparse import OptionParser
-
-    p = OptionParser()
-    p.add_option('-c', '--config', action='append', dest='config',
-                 help='specify config file(s)')
-    p.add_option('-d', action='store_true', dest='daemonize',
-                 help='run the server as a daemon')
-    p.add_option('-e', '--environment', dest='environment', default=None,
-                 help='apply the given config environment')
-    p.add_option('-f', action='store_true', dest='fastcgi',
-                 help='start a fastcgi server instead of the default HTTP '
-                      'server')
-    p.add_option('-s', action='store_true', dest='scgi',
-                 help='start a scgi server instead of the default HTTP server')
-    p.add_option('-x', action='store_true', dest='cgi',
-                 help='start a cgi server instead of the default HTTP server')
-    p.add_option('-i', '--import', action='append', dest='imports',
-                 help='specify modules to import')
-    p.add_option('-p', '--pidfile', dest='pidfile', default=None,
-                 help='store the process id in the given file')
-    p.add_option('-P', '--Path', action='append', dest='Path',
-                 help='add the given paths to sys.path')
-    options, args = p.parse_args()
-
-    if options.Path:
-        for p in options.Path:
-            sys.path.insert(0, p)
-
-    start(options.config, options.daemonize,
-          options.environment, options.fastcgi, options.scgi,
-          options.pidfile, options.imports, options.cgi)
diff --git a/libraries/cherrypy/favicon.ico b/libraries/cherrypy/favicon.ico
deleted file mode 100644
index f0d7e61badad3f332cf1e663efb97c0b5be80f5e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 1406
zcmb`Hd05U_6vscWSJJB#$ugQ@jA;zAXDv%YAxlVP&ytAjTU10ymNpe4LY9=2_SI5K
zC3>Y)vZNK!rhR_zJd<bU|M}kMoO{0Cd!GB;KkoU0NLVT=2)RAxa?lm%Cxjr;TL_un
z3VorFg&_WomX;PQBmFpaDwn?M(PWhuk)2hAmSzH76vOeACShffNm)rfo~iK|`@7<l
z97Rw-F{L$?MC3~`w{s_5nul444-L&tm<D=KQCCfMeH|u7X&CxBQCVF}Ku!kswmEpk
zq+ns~j;2N&v4wf)=_FFu*od~b4Gs0p1m~oYTv$M3V++>tfiw$m?BBmX0|pFW;J|@s
zYHBiQ&>#j69?Xy-Ll`=AD8q&gWBBmlj2JNjEiElZjvUFTQKJ|=dNgCkjA889v5Xrx
z4sC61baZqWKYlzDCQM-B#EDFrGznc@T_#VSjGmqzQ>IK|>eQ)Bn>G!7eSHiJ446KB
zIx}X>VCKx37#bQfYt}4g&z{YkIdhmhcP>UoM$DTxkNNZGvtYpjjE#+1xNspRCMGOe
zw1~xv7h`H_%915ZSh{p6%a$!;`SRtgSh0eYD_62=)hf))%vim8HEY(aVeQ(rtXsDZ
zb8~anuV0Uag#{ZnY+&QYjaXV*vT4&MHgDdHm6a7+wrpYR)~#&YwvFxEx3go%4tDO`
z$*x_y*u8r<d-m*M@7}%a+qVyEYisuJ-_L;q2e7fR!PeFmJ3BiL9z4jQLx-@px99NT
z!yGws1P2EPjvhVAv17+Le*8F&j*gr-aRMhNCr+L`Dg2pJoIZV;GiT1=?Cgw-iwmx<
zuDH3m;qLCv*|TTy@bJLX(-SW*FV3Ai$NBT;xp3hE-rn9^ym*mImoDMs<HP04m$`D~
z3ckL+T)ldgYuB!E{rYwM{QS6a;|4cx-sIM;Tim{V8-IU)?%cUUKtKS2fq~q;dzYY~
zAnx6}M{sa3At52$zki?5&``p{!Uzu!Cn6$($jC^dqN0e7jwU81hS=Cx;^N|nkB=uI
zA%O=E9`NwtLmoYP#N)@0NlZ*6DJh93Po9vRoJ>ke3QwOtB{embw6rwR)6;qO>=_vu
z89aafoEI-%keQi@R4V1=%a>$jW%26OE3&h*$;rv#_3PK<=H`-@mq&hnK5yQ<p`f6E
zw{PF_?%g}yzkkn%4<9HjEac<Ij}#RZQCwV1Nl6K%rKOaWl~G<^&ZkeG`26`ZU%q^y
zqN0MYU%&G0+czpJE2*lgqPn`8nwlDFYis%b{X2Dab=23_)6mc$%v2*yO-(d6HzS*U
z0dgG$&-ej}LNBpc{SO)MBTEYMVzF4hjZi6d5R#;wK*i!-5`O<pW+8GyxA@oY?c};D
z_D~ayM<VcE>T(K__B|98+X@Zp^73MZjvW!HsVT|~sc)O^ZGM)}O{A)-)@n=bmFdz?
zRaLc>D=T%Qr>f`&r)>x2Kb1tH)^h|wLs?1GQ@HOR^ik=lq13yT$@Z=qojU#WZ-LIg
Kbp8+jKgeIP@z;_7

diff --git a/libraries/cherrypy/lib/__init__.py b/libraries/cherrypy/lib/__init__.py
deleted file mode 100644
index f815f76a..00000000
--- a/libraries/cherrypy/lib/__init__.py
+++ /dev/null
@@ -1,96 +0,0 @@
-"""CherryPy Library."""
-
-
-def is_iterator(obj):
-    """Detect if the object provided implements the iterator protocol.
-
-    (i.e. like a generator).
-
-    This will return False for objects which are iterable,
-    but not iterators themselves.
-    """
-    from types import GeneratorType
-    if isinstance(obj, GeneratorType):
-        return True
-    elif not hasattr(obj, '__iter__'):
-        return False
-    else:
-        # Types which implement the protocol must return themselves when
-        # invoking 'iter' upon them.
-        return iter(obj) is obj
-
-
-def is_closable_iterator(obj):
-    """Detect if the given object is both closable and iterator."""
-    # Not an iterator.
-    if not is_iterator(obj):
-        return False
-
-    # A generator - the easiest thing to deal with.
-    import inspect
-    if inspect.isgenerator(obj):
-        return True
-
-    # A custom iterator. Look for a close method...
-    if not (hasattr(obj, 'close') and callable(obj.close)):
-        return False
-
-    #  ... which doesn't require any arguments.
-    try:
-        inspect.getcallargs(obj.close)
-    except TypeError:
-        return False
-    else:
-        return True
-
-
-class file_generator(object):
-    """Yield the given input (a file object) in chunks (default 64k).
-
-    (Core)
-    """
-
-    def __init__(self, input, chunkSize=65536):
-        """Initialize file_generator with file ``input`` for chunked access."""
-        self.input = input
-        self.chunkSize = chunkSize
-
-    def __iter__(self):
-        """Return iterator."""
-        return self
-
-    def __next__(self):
-        """Return next chunk of file."""
-        chunk = self.input.read(self.chunkSize)
-        if chunk:
-            return chunk
-        else:
-            if hasattr(self.input, 'close'):
-                self.input.close()
-            raise StopIteration()
-    next = __next__
-
-
-def file_generator_limited(fileobj, count, chunk_size=65536):
-    """Yield the given file object in chunks.
-
-    Stopps after `count` bytes has been emitted.
-    Default chunk size is 64kB. (Core)
-    """
-    remaining = count
-    while remaining > 0:
-        chunk = fileobj.read(min(chunk_size, remaining))
-        chunklen = len(chunk)
-        if chunklen == 0:
-            return
-        remaining -= chunklen
-        yield chunk
-
-
-def set_vary_header(response, header_name):
-    """Add a Vary header to a response."""
-    varies = response.headers.get('Vary', '')
-    varies = [x.strip() for x in varies.split(',') if x.strip()]
-    if header_name not in varies:
-        varies.append(header_name)
-    response.headers['Vary'] = ', '.join(varies)
diff --git a/libraries/cherrypy/lib/auth_basic.py b/libraries/cherrypy/lib/auth_basic.py
deleted file mode 100644
index ad379a26..00000000
--- a/libraries/cherrypy/lib/auth_basic.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# This file is part of CherryPy <http://www.cherrypy.org/>
-# -*- coding: utf-8 -*-
-# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
-"""HTTP Basic Authentication tool.
-
-This module provides a CherryPy 3.x tool which implements
-the server-side of HTTP Basic Access Authentication, as described in
-:rfc:`2617`.
-
-Example usage, using the built-in checkpassword_dict function which uses a dict
-as the credentials store::
-
-    userpassdict = {'bird' : 'bebop', 'ornette' : 'wayout'}
-    checkpassword = cherrypy.lib.auth_basic.checkpassword_dict(userpassdict)
-    basic_auth = {'tools.auth_basic.on': True,
-                  'tools.auth_basic.realm': 'earth',
-                  'tools.auth_basic.checkpassword': checkpassword,
-                  'tools.auth_basic.accept_charset': 'UTF-8',
-    }
-    app_config = { '/' : basic_auth }
-
-"""
-
-import binascii
-import unicodedata
-import base64
-
-import cherrypy
-from cherrypy._cpcompat import ntou, tonative
-
-
-__author__ = 'visteya'
-__date__ = 'April 2009'
-
-
-def checkpassword_dict(user_password_dict):
-    """Returns a checkpassword function which checks credentials
-    against a dictionary of the form: {username : password}.
-
-    If you want a simple dictionary-based authentication scheme, use
-    checkpassword_dict(my_credentials_dict) as the value for the
-    checkpassword argument to basic_auth().
-    """
-    def checkpassword(realm, user, password):
-        p = user_password_dict.get(user)
-        return p and p == password or False
-
-    return checkpassword
-
-
-def basic_auth(realm, checkpassword, debug=False, accept_charset='utf-8'):
-    """A CherryPy tool which hooks at before_handler to perform
-    HTTP Basic Access Authentication, as specified in :rfc:`2617`
-    and :rfc:`7617`.
-
-    If the request has an 'authorization' header with a 'Basic' scheme, this
-    tool attempts to authenticate the credentials supplied in that header.  If
-    the request has no 'authorization' header, or if it does but the scheme is
-    not 'Basic', or if authentication fails, the tool sends a 401 response with
-    a 'WWW-Authenticate' Basic header.
-
-    realm
-        A string containing the authentication realm.
-
-    checkpassword
-        A callable which checks the authentication credentials.
-        Its signature is checkpassword(realm, username, password). where
-        username and password are the values obtained from the request's
-        'authorization' header.  If authentication succeeds, checkpassword
-        returns True, else it returns False.
-
-    """
-
-    fallback_charset = 'ISO-8859-1'
-
-    if '"' in realm:
-        raise ValueError('Realm cannot contain the " (quote) character.')
-    request = cherrypy.serving.request
-
-    auth_header = request.headers.get('authorization')
-    if auth_header is not None:
-        # split() error, base64.decodestring() error
-        msg = 'Bad Request'
-        with cherrypy.HTTPError.handle((ValueError, binascii.Error), 400, msg):
-            scheme, params = auth_header.split(' ', 1)
-            if scheme.lower() == 'basic':
-                charsets = accept_charset, fallback_charset
-                decoded_params = base64.b64decode(params.encode('ascii'))
-                decoded_params = _try_decode(decoded_params, charsets)
-                decoded_params = ntou(decoded_params)
-                decoded_params = unicodedata.normalize('NFC', decoded_params)
-                decoded_params = tonative(decoded_params)
-                username, password = decoded_params.split(':', 1)
-                if checkpassword(realm, username, password):
-                    if debug:
-                        cherrypy.log('Auth succeeded', 'TOOLS.AUTH_BASIC')
-                    request.login = username
-                    return  # successful authentication
-
-    charset = accept_charset.upper()
-    charset_declaration = (
-        (', charset="%s"' % charset)
-        if charset != fallback_charset
-        else ''
-    )
-    # Respond with 401 status and a WWW-Authenticate header
-    cherrypy.serving.response.headers['www-authenticate'] = (
-        'Basic realm="%s"%s' % (realm, charset_declaration)
-    )
-    raise cherrypy.HTTPError(
-        401, 'You are not authorized to access that resource')
-
-
-def _try_decode(subject, charsets):
-    for charset in charsets[:-1]:
-        try:
-            return tonative(subject, charset)
-        except ValueError:
-            pass
-    return tonative(subject, charsets[-1])
diff --git a/libraries/cherrypy/lib/auth_digest.py b/libraries/cherrypy/lib/auth_digest.py
deleted file mode 100644
index 9b4f55c8..00000000
--- a/libraries/cherrypy/lib/auth_digest.py
+++ /dev/null
@@ -1,464 +0,0 @@
-# This file is part of CherryPy <http://www.cherrypy.org/>
-# -*- coding: utf-8 -*-
-# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
-"""HTTP Digest Authentication tool.
-
-An implementation of the server-side of HTTP Digest Access
-Authentication, which is described in :rfc:`2617`.
-
-Example usage, using the built-in get_ha1_dict_plain function which uses a dict
-of plaintext passwords as the credentials store::
-
-    userpassdict = {'alice' : '4x5istwelve'}
-    get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(userpassdict)
-    digest_auth = {'tools.auth_digest.on': True,
-                   'tools.auth_digest.realm': 'wonderland',
-                   'tools.auth_digest.get_ha1': get_ha1,
-                   'tools.auth_digest.key': 'a565c27146791cfb',
-                   'tools.auth_digest.accept_charset': 'UTF-8',
-    }
-    app_config = { '/' : digest_auth }
-"""
-
-import time
-import functools
-from hashlib import md5
-
-from six.moves.urllib.request import parse_http_list, parse_keqv_list
-
-import cherrypy
-from cherrypy._cpcompat import ntob, tonative
-
-
-__author__ = 'visteya'
-__date__ = 'April 2009'
-
-
-def md5_hex(s):
-    return md5(ntob(s, 'utf-8')).hexdigest()
-
-
-qop_auth = 'auth'
-qop_auth_int = 'auth-int'
-valid_qops = (qop_auth, qop_auth_int)
-
-valid_algorithms = ('MD5', 'MD5-sess')
-
-FALLBACK_CHARSET = 'ISO-8859-1'
-DEFAULT_CHARSET = 'UTF-8'
-
-
-def TRACE(msg):
-    cherrypy.log(msg, context='TOOLS.AUTH_DIGEST')
-
-# Three helper functions for users of the tool, providing three variants
-# of get_ha1() functions for three different kinds of credential stores.
-
-
-def get_ha1_dict_plain(user_password_dict):
-    """Returns a get_ha1 function which obtains a plaintext password from a
-    dictionary of the form: {username : password}.
-
-    If you want a simple dictionary-based authentication scheme, with plaintext
-    passwords, use get_ha1_dict_plain(my_userpass_dict) as the value for the
-    get_ha1 argument to digest_auth().
-    """
-    def get_ha1(realm, username):
-        password = user_password_dict.get(username)
-        if password:
-            return md5_hex('%s:%s:%s' % (username, realm, password))
-        return None
-
-    return get_ha1
-
-
-def get_ha1_dict(user_ha1_dict):
-    """Returns a get_ha1 function which obtains a HA1 password hash from a
-    dictionary of the form: {username : HA1}.
-
-    If you want a dictionary-based authentication scheme, but with
-    pre-computed HA1 hashes instead of plain-text passwords, use
-    get_ha1_dict(my_userha1_dict) as the value for the get_ha1
-    argument to digest_auth().
-    """
-    def get_ha1(realm, username):
-        return user_ha1_dict.get(username)
-
-    return get_ha1
-
-
-def get_ha1_file_htdigest(filename):
-    """Returns a get_ha1 function which obtains a HA1 password hash from a
-    flat file with lines of the same format as that produced by the Apache
-    htdigest utility. For example, for realm 'wonderland', username 'alice',
-    and password '4x5istwelve', the htdigest line would be::
-
-        alice:wonderland:3238cdfe91a8b2ed8e39646921a02d4c
-
-    If you want to use an Apache htdigest file as the credentials store,
-    then use get_ha1_file_htdigest(my_htdigest_file) as the value for the
-    get_ha1 argument to digest_auth().  It is recommended that the filename
-    argument be an absolute path, to avoid problems.
-    """
-    def get_ha1(realm, username):
-        result = None
-        f = open(filename, 'r')
-        for line in f:
-            u, r, ha1 = line.rstrip().split(':')
-            if u == username and r == realm:
-                result = ha1
-                break
-        f.close()
-        return result
-
-    return get_ha1
-
-
-def synthesize_nonce(s, key, timestamp=None):
-    """Synthesize a nonce value which resists spoofing and can be checked
-    for staleness. Returns a string suitable as the value for 'nonce' in
-    the www-authenticate header.
-
-    s
-        A string related to the resource, such as the hostname of the server.
-
-    key
-        A secret string known only to the server.
-
-    timestamp
-        An integer seconds-since-the-epoch timestamp
-
-    """
-    if timestamp is None:
-        timestamp = int(time.time())
-    h = md5_hex('%s:%s:%s' % (timestamp, s, key))
-    nonce = '%s:%s' % (timestamp, h)
-    return nonce
-
-
-def H(s):
-    """The hash function H"""
-    return md5_hex(s)
-
-
-def _try_decode_header(header, charset):
-    global FALLBACK_CHARSET
-
-    for enc in (charset, FALLBACK_CHARSET):
-        try:
-            return tonative(ntob(tonative(header, 'latin1'), 'latin1'), enc)
-        except ValueError as ve:
-            last_err = ve
-    else:
-        raise last_err
-
-
-class HttpDigestAuthorization(object):
-    """
-    Parses a Digest Authorization header and performs
-    re-calculation of the digest.
-    """
-
-    scheme = 'digest'
-
-    def errmsg(self, s):
-        return 'Digest Authorization header: %s' % s
-
-    @classmethod
-    def matches(cls, header):
-        scheme, _, _ = header.partition(' ')
-        return scheme.lower() == cls.scheme
-
-    def __init__(
-        self, auth_header, http_method,
-        debug=False, accept_charset=DEFAULT_CHARSET[:],
-    ):
-        self.http_method = http_method
-        self.debug = debug
-
-        if not self.matches(auth_header):
-            raise ValueError('Authorization scheme is not "Digest"')
-
-        self.auth_header = _try_decode_header(auth_header, accept_charset)
-
-        scheme, params = self.auth_header.split(' ', 1)
-
-        # make a dict of the params
-        items = parse_http_list(params)
-        paramsd = parse_keqv_list(items)
-
-        self.realm = paramsd.get('realm')
-        self.username = paramsd.get('username')
-        self.nonce = paramsd.get('nonce')
-        self.uri = paramsd.get('uri')
-        self.method = paramsd.get('method')
-        self.response = paramsd.get('response')  # the response digest
-        self.algorithm = paramsd.get('algorithm', 'MD5').upper()
-        self.cnonce = paramsd.get('cnonce')
-        self.opaque = paramsd.get('opaque')
-        self.qop = paramsd.get('qop')  # qop
-        self.nc = paramsd.get('nc')  # nonce count
-
-        # perform some correctness checks
-        if self.algorithm not in valid_algorithms:
-            raise ValueError(
-                self.errmsg("Unsupported value for algorithm: '%s'" %
-                            self.algorithm))
-
-        has_reqd = (
-            self.username and
-            self.realm and
-            self.nonce and
-            self.uri and
-            self.response
-        )
-        if not has_reqd:
-            raise ValueError(
-                self.errmsg('Not all required parameters are present.'))
-
-        if self.qop:
-            if self.qop not in valid_qops:
-                raise ValueError(
-                    self.errmsg("Unsupported value for qop: '%s'" % self.qop))
-            if not (self.cnonce and self.nc):
-                raise ValueError(
-                    self.errmsg('If qop is sent then '
-                                'cnonce and nc MUST be present'))
-        else:
-            if self.cnonce or self.nc:
-                raise ValueError(
-                    self.errmsg('If qop is not sent, '
-                                'neither cnonce nor nc can be present'))
-
-    def __str__(self):
-        return 'authorization : %s' % self.auth_header
-
-    def validate_nonce(self, s, key):
-        """Validate the nonce.
-        Returns True if nonce was generated by synthesize_nonce() and the
-        timestamp is not spoofed, else returns False.
-
-        s
-            A string related to the resource, such as the hostname of
-            the server.
-
-        key
-            A secret string known only to the server.
-
-        Both s and key must be the same values which were used to synthesize
-        the nonce we are trying to validate.
-        """
-        try:
-            timestamp, hashpart = self.nonce.split(':', 1)
-            s_timestamp, s_hashpart = synthesize_nonce(
-                s, key, timestamp).split(':', 1)
-            is_valid = s_hashpart == hashpart
-            if self.debug:
-                TRACE('validate_nonce: %s' % is_valid)
-            return is_valid
-        except ValueError:  # split() error
-            pass
-        return False
-
-    def is_nonce_stale(self, max_age_seconds=600):
-        """Returns True if a validated nonce is stale. The nonce contains a
-        timestamp in plaintext and also a secure hash of the timestamp.
-        You should first validate the nonce to ensure the plaintext
-        timestamp is not spoofed.
-        """
-        try:
-            timestamp, hashpart = self.nonce.split(':', 1)
-            if int(timestamp) + max_age_seconds > int(time.time()):
-                return False
-        except ValueError:  # int() error
-            pass
-        if self.debug:
-            TRACE('nonce is stale')
-        return True
-
-    def HA2(self, entity_body=''):
-        """Returns the H(A2) string. See :rfc:`2617` section 3.2.2.3."""
-        # RFC 2617 3.2.2.3
-        # If the "qop" directive's value is "auth" or is unspecified,
-        # then A2 is:
-        #    A2 = method ":" digest-uri-value
-        #
-        # If the "qop" value is "auth-int", then A2 is:
-        #    A2 = method ":" digest-uri-value ":" H(entity-body)
-        if self.qop is None or self.qop == 'auth':
-            a2 = '%s:%s' % (self.http_method, self.uri)
-        elif self.qop == 'auth-int':
-            a2 = '%s:%s:%s' % (self.http_method, self.uri, H(entity_body))
-        else:
-            # in theory, this should never happen, since I validate qop in
-            # __init__()
-            raise ValueError(self.errmsg('Unrecognized value for qop!'))
-        return H(a2)
-
-    def request_digest(self, ha1, entity_body=''):
-        """Calculates the Request-Digest. See :rfc:`2617` section 3.2.2.1.
-
-        ha1
-            The HA1 string obtained from the credentials store.
-
-        entity_body
-            If 'qop' is set to 'auth-int', then A2 includes a hash
-            of the "entity body".  The entity body is the part of the
-            message which follows the HTTP headers. See :rfc:`2617` section
-            4.3.  This refers to the entity the user agent sent in the
-            request which has the Authorization header. Typically GET
-            requests don't have an entity, and POST requests do.
-
-        """
-        ha2 = self.HA2(entity_body)
-        # Request-Digest -- RFC 2617 3.2.2.1
-        if self.qop:
-            req = '%s:%s:%s:%s:%s' % (
-                self.nonce, self.nc, self.cnonce, self.qop, ha2)
-        else:
-            req = '%s:%s' % (self.nonce, ha2)
-
-        # RFC 2617 3.2.2.2
-        #
-        # If the "algorithm" directive's value is "MD5" or is unspecified,
-        # then A1 is:
-        #    A1 = unq(username-value) ":" unq(realm-value) ":" passwd
-        #
-        # If the "algorithm" directive's value is "MD5-sess", then A1 is
-        # calculated only once - on the first request by the client following
-        # receipt of a WWW-Authenticate challenge from the server.
-        # A1 = H( unq(username-value) ":" unq(realm-value) ":" passwd )
-        #         ":" unq(nonce-value) ":" unq(cnonce-value)
-        if self.algorithm == 'MD5-sess':
-            ha1 = H('%s:%s:%s' % (ha1, self.nonce, self.cnonce))
-
-        digest = H('%s:%s' % (ha1, req))
-        return digest
-
-
-def _get_charset_declaration(charset):
-    global FALLBACK_CHARSET
-    charset = charset.upper()
-    return (
-        (', charset="%s"' % charset)
-        if charset != FALLBACK_CHARSET
-        else ''
-    )
-
-
-def www_authenticate(
-    realm, key, algorithm='MD5', nonce=None, qop=qop_auth,
-    stale=False, accept_charset=DEFAULT_CHARSET[:],
-):
-    """Constructs a WWW-Authenticate header for Digest authentication."""
-    if qop not in valid_qops:
-        raise ValueError("Unsupported value for qop: '%s'" % qop)
-    if algorithm not in valid_algorithms:
-        raise ValueError("Unsupported value for algorithm: '%s'" % algorithm)
-
-    HEADER_PATTERN = (
-        'Digest realm="%s", nonce="%s", algorithm="%s", qop="%s"%s%s'
-    )
-
-    if nonce is None:
-        nonce = synthesize_nonce(realm, key)
-
-    stale_param = ', stale="true"' if stale else ''
-
-    charset_declaration = _get_charset_declaration(accept_charset)
-
-    return HEADER_PATTERN % (
-        realm, nonce, algorithm, qop, stale_param, charset_declaration,
-    )
-
-
-def digest_auth(realm, get_ha1, key, debug=False, accept_charset='utf-8'):
-    """A CherryPy tool that hooks at before_handler to perform
-    HTTP Digest Access Authentication, as specified in :rfc:`2617`.
-
-    If the request has an 'authorization' header with a 'Digest' scheme,
-    this tool authenticates the credentials supplied in that header.
-    If the request has no 'authorization' header, or if it does but the
-    scheme is not "Digest", or if authentication fails, the tool sends
-    a 401 response with a 'WWW-Authenticate' Digest header.
-
-    realm
-        A string containing the authentication realm.
-
-    get_ha1
-        A callable that looks up a username in a credentials store
-        and returns the HA1 string, which is defined in the RFC to be
-        MD5(username : realm : password).  The function's signature is:
-        ``get_ha1(realm, username)``
-        where username is obtained from the request's 'authorization' header.
-        If username is not found in the credentials store, get_ha1() returns
-        None.
-
-    key
-        A secret string known only to the server, used in the synthesis
-        of nonces.
-
-    """
-    request = cherrypy.serving.request
-
-    auth_header = request.headers.get('authorization')
-
-    respond_401 = functools.partial(
-        _respond_401, realm, key, accept_charset, debug)
-
-    if not HttpDigestAuthorization.matches(auth_header or ''):
-        respond_401()
-
-    msg = 'The Authorization header could not be parsed.'
-    with cherrypy.HTTPError.handle(ValueError, 400, msg):
-        auth = HttpDigestAuthorization(
-            auth_header, request.method,
-            debug=debug, accept_charset=accept_charset,
-        )
-
-    if debug:
-        TRACE(str(auth))
-
-    if not auth.validate_nonce(realm, key):
-        respond_401()
-
-    ha1 = get_ha1(realm, auth.username)
-
-    if ha1 is None:
-        respond_401()
-
-    # note that for request.body to be available we need to
-    # hook in at before_handler, not on_start_resource like
-    # 3.1.x digest_auth does.
-    digest = auth.request_digest(ha1, entity_body=request.body)
-    if digest != auth.response:
-        respond_401()
-
-    # authenticated
-    if debug:
-        TRACE('digest matches auth.response')
-    # Now check if nonce is stale.
-    # The choice of ten minutes' lifetime for nonce is somewhat
-    # arbitrary
-    if auth.is_nonce_stale(max_age_seconds=600):
-        respond_401(stale=True)
-
-    request.login = auth.username
-    if debug:
-        TRACE('authentication of %s successful' % auth.username)
-
-
-def _respond_401(realm, key, accept_charset, debug, **kwargs):
-    """
-    Respond with 401 status and a WWW-Authenticate header
-    """
-    header = www_authenticate(
-        realm, key,
-        accept_charset=accept_charset,
-        **kwargs
-    )
-    if debug:
-        TRACE(header)
-    cherrypy.serving.response.headers['WWW-Authenticate'] = header
-    raise cherrypy.HTTPError(
-        401, 'You are not authorized to access that resource')
diff --git a/libraries/cherrypy/lib/caching.py b/libraries/cherrypy/lib/caching.py
deleted file mode 100644
index fed325a6..00000000
--- a/libraries/cherrypy/lib/caching.py
+++ /dev/null
@@ -1,482 +0,0 @@
-"""
-CherryPy implements a simple caching system as a pluggable Tool. This tool
-tries to be an (in-process) HTTP/1.1-compliant cache. It's not quite there
-yet, but it's probably good enough for most sites.
-
-In general, GET responses are cached (along with selecting headers) and, if
-another request arrives for the same resource, the caching Tool will return 304
-Not Modified if possible, or serve the cached response otherwise. It also sets
-request.cached to True if serving a cached representation, and sets
-request.cacheable to False (so it doesn't get cached again).
-
-If POST, PUT, or DELETE requests are made for a cached resource, they
-invalidate (delete) any cached response.
-
-Usage
-=====
-
-Configuration file example::
-
-    [/]
-    tools.caching.on = True
-    tools.caching.delay = 3600
-
-You may use a class other than the default
-:class:`MemoryCache<cherrypy.lib.caching.MemoryCache>` by supplying the config
-entry ``cache_class``; supply the full dotted name of the replacement class
-as the config value. It must implement the basic methods ``get``, ``put``,
-``delete``, and ``clear``.
-
-You may set any attribute, including overriding methods, on the cache
-instance by providing them in config. The above sets the
-:attr:`delay<cherrypy.lib.caching.MemoryCache.delay>` attribute, for example.
-"""
-
-import datetime
-import sys
-import threading
-import time
-
-import six
-
-import cherrypy
-from cherrypy.lib import cptools, httputil
-from cherrypy._cpcompat import Event
-
-
-class Cache(object):
-
-    """Base class for Cache implementations."""
-
-    def get(self):
-        """Return the current variant if in the cache, else None."""
-        raise NotImplemented
-
-    def put(self, obj, size):
-        """Store the current variant in the cache."""
-        raise NotImplemented
-
-    def delete(self):
-        """Remove ALL cached variants of the current resource."""
-        raise NotImplemented
-
-    def clear(self):
-        """Reset the cache to its initial, empty state."""
-        raise NotImplemented
-
-
-# ------------------------------ Memory Cache ------------------------------- #
-class AntiStampedeCache(dict):
-
-    """A storage system for cached items which reduces stampede collisions."""
-
-    def wait(self, key, timeout=5, debug=False):
-        """Return the cached value for the given key, or None.
-
-        If timeout is not None, and the value is already
-        being calculated by another thread, wait until the given timeout has
-        elapsed. If the value is available before the timeout expires, it is
-        returned. If not, None is returned, and a sentinel placed in the cache
-        to signal other threads to wait.
-
-        If timeout is None, no waiting is performed nor sentinels used.
-        """
-        value = self.get(key)
-        if isinstance(value, Event):
-            if timeout is None:
-                # Ignore the other thread and recalc it ourselves.
-                if debug:
-                    cherrypy.log('No timeout', 'TOOLS.CACHING')
-                return None
-
-            # Wait until it's done or times out.
-            if debug:
-                cherrypy.log('Waiting up to %s seconds' %
-                             timeout, 'TOOLS.CACHING')
-            value.wait(timeout)
-            if value.result is not None:
-                # The other thread finished its calculation. Use it.
-                if debug:
-                    cherrypy.log('Result!', 'TOOLS.CACHING')
-                return value.result
-            # Timed out. Stick an Event in the slot so other threads wait
-            # on this one to finish calculating the value.
-            if debug:
-                cherrypy.log('Timed out', 'TOOLS.CACHING')
-            e = threading.Event()
-            e.result = None
-            dict.__setitem__(self, key, e)
-
-            return None
-        elif value is None:
-            # Stick an Event in the slot so other threads wait
-            # on this one to finish calculating the value.
-            if debug:
-                cherrypy.log('Timed out', 'TOOLS.CACHING')
-            e = threading.Event()
-            e.result = None
-            dict.__setitem__(self, key, e)
-        return value
-
-    def __setitem__(self, key, value):
-        """Set the cached value for the given key."""
-        existing = self.get(key)
-        dict.__setitem__(self, key, value)
-        if isinstance(existing, Event):
-            # Set Event.result so other threads waiting on it have
-            # immediate access without needing to poll the cache again.
-            existing.result = value
-            existing.set()
-
-
-class MemoryCache(Cache):
-
-    """An in-memory cache for varying response content.
-
-    Each key in self.store is a URI, and each value is an AntiStampedeCache.
-    The response for any given URI may vary based on the values of
-    "selecting request headers"; that is, those named in the Vary
-    response header. We assume the list of header names to be constant
-    for each URI throughout the lifetime of the application, and store
-    that list in ``self.store[uri].selecting_headers``.
-
-    The items contained in ``self.store[uri]`` have keys which are tuples of
-    request header values (in the same order as the names in its
-    selecting_headers), and values which are the actual responses.
-    """
-
-    maxobjects = 1000
-    """The maximum number of cached objects; defaults to 1000."""
-
-    maxobj_size = 100000
-    """The maximum size of each cached object in bytes; defaults to 100 KB."""
-
-    maxsize = 10000000
-    """The maximum size of the entire cache in bytes; defaults to 10 MB."""
-
-    delay = 600
-    """Seconds until the cached content expires; defaults to 600 (10 minutes).
-    """
-
-    antistampede_timeout = 5
-    """Seconds to wait for other threads to release a cache lock."""
-
-    expire_freq = 0.1
-    """Seconds to sleep between cache expiration sweeps."""
-
-    debug = False
-
-    def __init__(self):
-        self.clear()
-
-        # Run self.expire_cache in a separate daemon thread.
-        t = threading.Thread(target=self.expire_cache, name='expire_cache')
-        self.expiration_thread = t
-        t.daemon = True
-        t.start()
-
-    def clear(self):
-        """Reset the cache to its initial, empty state."""
-        self.store = {}
-        self.expirations = {}
-        self.tot_puts = 0
-        self.tot_gets = 0
-        self.tot_hist = 0
-        self.tot_expires = 0
-        self.tot_non_modified = 0
-        self.cursize = 0
-
-    def expire_cache(self):
-        """Continuously examine cached objects, expiring stale ones.
-
-        This function is designed to be run in its own daemon thread,
-        referenced at ``self.expiration_thread``.
-        """
-        # It's possible that "time" will be set to None
-        # arbitrarily, so we check "while time" to avoid exceptions.
-        # See tickets #99 and #180 for more information.
-        while time:
-            now = time.time()
-            # Must make a copy of expirations so it doesn't change size
-            # during iteration
-            items = list(six.iteritems(self.expirations))
-            for expiration_time, objects in items:
-                if expiration_time <= now:
-                    for obj_size, uri, sel_header_values in objects:
-                        try:
-                            del self.store[uri][tuple(sel_header_values)]
-                            self.tot_expires += 1
-                            self.cursize -= obj_size
-                        except KeyError:
-                            # the key may have been deleted elsewhere
-                            pass
-                    del self.expirations[expiration_time]
-            time.sleep(self.expire_freq)
-
-    def get(self):
-        """Return the current variant if in the cache, else None."""
-        request = cherrypy.serving.request
-        self.tot_gets += 1
-
-        uri = cherrypy.url(qs=request.query_string)
-        uricache = self.store.get(uri)
-        if uricache is None:
-            return None
-
-        header_values = [request.headers.get(h, '')
-                         for h in uricache.selecting_headers]
-        variant = uricache.wait(key=tuple(sorted(header_values)),
-                                timeout=self.antistampede_timeout,
-                                debug=self.debug)
-        if variant is not None:
-            self.tot_hist += 1
-        return variant
-
-    def put(self, variant, size):
-        """Store the current variant in the cache."""
-        request = cherrypy.serving.request
-        response = cherrypy.serving.response
-
-        uri = cherrypy.url(qs=request.query_string)
-        uricache = self.store.get(uri)
-        if uricache is None:
-            uricache = AntiStampedeCache()
-            uricache.selecting_headers = [
-                e.value for e in response.headers.elements('Vary')]
-            self.store[uri] = uricache
-
-        if len(self.store) < self.maxobjects:
-            total_size = self.cursize + size
-
-            # checks if there's space for the object
-            if (size < self.maxobj_size and total_size < self.maxsize):
-                # add to the expirations list
-                expiration_time = response.time + self.delay
-                bucket = self.expirations.setdefault(expiration_time, [])
-                bucket.append((size, uri, uricache.selecting_headers))
-
-                # add to the cache
-                header_values = [request.headers.get(h, '')
-                                 for h in uricache.selecting_headers]
-                uricache[tuple(sorted(header_values))] = variant
-                self.tot_puts += 1
-                self.cursize = total_size
-
-    def delete(self):
-        """Remove ALL cached variants of the current resource."""
-        uri = cherrypy.url(qs=cherrypy.serving.request.query_string)
-        self.store.pop(uri, None)
-
-
-def get(invalid_methods=('POST', 'PUT', 'DELETE'), debug=False, **kwargs):
-    """Try to obtain cached output. If fresh enough, raise HTTPError(304).
-
-    If POST, PUT, or DELETE:
-        * invalidates (deletes) any cached response for this resource
-        * sets request.cached = False
-        * sets request.cacheable = False
-
-    else if a cached copy exists:
-        * sets request.cached = True
-        * sets request.cacheable = False
-        * sets response.headers to the cached values
-        * checks the cached Last-Modified response header against the
-          current If-(Un)Modified-Since request headers; raises 304
-          if necessary.
-        * sets response.status and response.body to the cached values
-        * returns True
-
-    otherwise:
-        * sets request.cached = False
-        * sets request.cacheable = True
-        * returns False
-    """
-    request = cherrypy.serving.request
-    response = cherrypy.serving.response
-
-    if not hasattr(cherrypy, '_cache'):
-        # Make a process-wide Cache object.
-        cherrypy._cache = kwargs.pop('cache_class', MemoryCache)()
-
-        # Take all remaining kwargs and set them on the Cache object.
-        for k, v in kwargs.items():
-            setattr(cherrypy._cache, k, v)
-        cherrypy._cache.debug = debug
-
-    # POST, PUT, DELETE should invalidate (delete) the cached copy.
-    # See http://www.w3.org/Protocols/rfc2616/rfc2616-sec13.html#sec13.10.
-    if request.method in invalid_methods:
-        if debug:
-            cherrypy.log('request.method %r in invalid_methods %r' %
-                         (request.method, invalid_methods), 'TOOLS.CACHING')
-        cherrypy._cache.delete()
-        request.cached = False
-        request.cacheable = False
-        return False
-
-    if 'no-cache' in [e.value for e in request.headers.elements('Pragma')]:
-        request.cached = False
-        request.cacheable = True
-        return False
-
-    cache_data = cherrypy._cache.get()
-    request.cached = bool(cache_data)
-    request.cacheable = not request.cached
-    if request.cached:
-        # Serve the cached copy.
-        max_age = cherrypy._cache.delay
-        for v in [e.value for e in request.headers.elements('Cache-Control')]:
-            atoms = v.split('=', 1)
-            directive = atoms.pop(0)
-            if directive == 'max-age':
-                if len(atoms) != 1 or not atoms[0].isdigit():
-                    raise cherrypy.HTTPError(
-                        400, 'Invalid Cache-Control header')
-                max_age = int(atoms[0])
-                break
-            elif directive == 'no-cache':
-                if debug:
-                    cherrypy.log(
-                        'Ignoring cache due to Cache-Control: no-cache',
-                        'TOOLS.CACHING')
-                request.cached = False
-                request.cacheable = True
-                return False
-
-        if debug:
-            cherrypy.log('Reading response from cache', 'TOOLS.CACHING')
-        s, h, b, create_time = cache_data
-        age = int(response.time - create_time)
-        if (age > max_age):
-            if debug:
-                cherrypy.log('Ignoring cache due to age > %d' % max_age,
-                             'TOOLS.CACHING')
-            request.cached = False
-            request.cacheable = True
-            return False
-
-        # Copy the response headers. See
-        # https://github.com/cherrypy/cherrypy/issues/721.
-        response.headers = rh = httputil.HeaderMap()
-        for k in h:
-            dict.__setitem__(rh, k, dict.__getitem__(h, k))
-
-        # Add the required Age header
-        response.headers['Age'] = str(age)
-
-        try:
-            # Note that validate_since depends on a Last-Modified header;
-            # this was put into the cached copy, and should have been
-            # resurrected just above (response.headers = cache_data[1]).
-            cptools.validate_since()
-        except cherrypy.HTTPRedirect:
-            x = sys.exc_info()[1]
-            if x.status == 304:
-                cherrypy._cache.tot_non_modified += 1
-            raise
-
-        # serve it & get out from the request
-        response.status = s
-        response.body = b
-    else:
-        if debug:
-            cherrypy.log('request is not cached', 'TOOLS.CACHING')
-    return request.cached
-
-
-def tee_output():
-    """Tee response output to cache storage. Internal."""
-    # Used by CachingTool by attaching to request.hooks
-
-    request = cherrypy.serving.request
-    if 'no-store' in request.headers.values('Cache-Control'):
-        return
-
-    def tee(body):
-        """Tee response.body into a list."""
-        if ('no-cache' in response.headers.values('Pragma') or
-                'no-store' in response.headers.values('Cache-Control')):
-            for chunk in body:
-                yield chunk
-            return
-
-        output = []
-        for chunk in body:
-            output.append(chunk)
-            yield chunk
-
-        # Save the cache data, but only if the body isn't empty.
-        # e.g. a 304 Not Modified on a static file response will
-        # have an empty body.
-        # If the body is empty, delete the cache because it
-        # contains a stale Threading._Event object that will
-        # stall all consecutive requests until the _Event times
-        # out
-        body = b''.join(output)
-        if not body:
-            cherrypy._cache.delete()
-        else:
-            cherrypy._cache.put((response.status, response.headers or {},
-                                 body, response.time), len(body))
-
-    response = cherrypy.serving.response
-    response.body = tee(response.body)
-
-
-def expires(secs=0, force=False, debug=False):
-    """Tool for influencing cache mechanisms using the 'Expires' header.
-
-    secs
-        Must be either an int or a datetime.timedelta, and indicates the
-        number of seconds between response.time and when the response should
-        expire. The 'Expires' header will be set to response.time + secs.
-        If secs is zero, the 'Expires' header is set one year in the past, and
-        the following "cache prevention" headers are also set:
-
-            * Pragma: no-cache
-            * Cache-Control': no-cache, must-revalidate
-
-    force
-        If False, the following headers are checked:
-
-            * Etag
-            * Last-Modified
-            * Age
-            * Expires
-
-        If any are already present, none of the above response headers are set.
-
-    """
-
-    response = cherrypy.serving.response
-    headers = response.headers
-
-    cacheable = False
-    if not force:
-        # some header names that indicate that the response can be cached
-        for indicator in ('Etag', 'Last-Modified', 'Age', 'Expires'):
-            if indicator in headers:
-                cacheable = True
-                break
-
-    if not cacheable and not force:
-        if debug:
-            cherrypy.log('request is not cacheable', 'TOOLS.EXPIRES')
-    else:
-        if debug:
-            cherrypy.log('request is cacheable', 'TOOLS.EXPIRES')
-        if isinstance(secs, datetime.timedelta):
-            secs = (86400 * secs.days) + secs.seconds
-
-        if secs == 0:
-            if force or ('Pragma' not in headers):
-                headers['Pragma'] = 'no-cache'
-            if cherrypy.serving.request.protocol >= (1, 1):
-                if force or 'Cache-Control' not in headers:
-                    headers['Cache-Control'] = 'no-cache, must-revalidate'
-            # Set an explicit Expires date in the past.
-            expiry = httputil.HTTPDate(1169942400.0)
-        else:
-            expiry = httputil.HTTPDate(response.time + secs)
-        if force or 'Expires' not in headers:
-            headers['Expires'] = expiry
diff --git a/libraries/cherrypy/lib/covercp.py b/libraries/cherrypy/lib/covercp.py
deleted file mode 100644
index 0bafca13..00000000
--- a/libraries/cherrypy/lib/covercp.py
+++ /dev/null
@@ -1,391 +0,0 @@
-"""Code-coverage tools for CherryPy.
-
-To use this module, or the coverage tools in the test suite,
-you need to download 'coverage.py', either Gareth Rees' `original
-implementation <http://www.garethrees.org/2001/12/04/python-coverage/>`_
-or Ned Batchelder's `enhanced version:
-<http://www.nedbatchelder.com/code/modules/coverage.html>`_
-
-To turn on coverage tracing, use the following code::
-
-    cherrypy.engine.subscribe('start', covercp.start)
-
-DO NOT subscribe anything on the 'start_thread' channel, as previously
-recommended. Calling start once in the main thread should be sufficient
-to start coverage on all threads. Calling start again in each thread
-effectively clears any coverage data gathered up to that point.
-
-Run your code, then use the ``covercp.serve()`` function to browse the
-results in a web browser. If you run this module from the command line,
-it will call ``serve()`` for you.
-"""
-
-import re
-import sys
-import cgi
-import os
-import os.path
-
-from six.moves import urllib
-
-import cherrypy
-
-
-localFile = os.path.join(os.path.dirname(__file__), 'coverage.cache')
-
-the_coverage = None
-try:
-    from coverage import coverage
-    the_coverage = coverage(data_file=localFile)
-
-    def start():
-        the_coverage.start()
-except ImportError:
-    # Setting the_coverage to None will raise errors
-    # that need to be trapped downstream.
-    the_coverage = None
-
-    import warnings
-    warnings.warn(
-        'No code coverage will be performed; '
-        'coverage.py could not be imported.')
-
-    def start():
-        pass
-start.priority = 20
-
-TEMPLATE_MENU = """<html>
-<head>
-    <title>CherryPy Coverage Menu</title>
-    <style>
-        body {font: 9pt Arial, serif;}
-        #tree {
-            font-size: 8pt;
-            font-family: Andale Mono, monospace;
-            white-space: pre;
-            }
-        #tree a:active, a:focus {
-            background-color: black;
-            padding: 1px;
-            color: white;
-            border: 0px solid #9999FF;
-            -moz-outline-style: none;
-            }
-        .fail { color: red;}
-        .pass { color: #888;}
-        #pct { text-align: right;}
-        h3 {
-            font-size: small;
-            font-weight: bold;
-            font-style: italic;
-            margin-top: 5px;
-            }
-        input { border: 1px solid #ccc; padding: 2px; }
-        .directory {
-            color: #933;
-            font-style: italic;
-            font-weight: bold;
-            font-size: 10pt;
-            }
-        .file {
-            color: #400;
-            }
-        a { text-decoration: none; }
-        #crumbs {
-            color: white;
-            font-size: 8pt;
-            font-family: Andale Mono, monospace;
-            width: 100%;
-            background-color: black;
-            }
-        #crumbs a {
-            color: #f88;
-            }
-        #options {
-            line-height: 2.3em;
-            border: 1px solid black;
-            background-color: #eee;
-            padding: 4px;
-            }
-        #exclude {
-            width: 100%;
-            margin-bottom: 3px;
-            border: 1px solid #999;
-            }
-        #submit {
-            background-color: black;
-            color: white;
-            border: 0;
-            margin-bottom: -9px;
-            }
-    </style>
-</head>
-<body>
-<h2>CherryPy Coverage</h2>"""
-
-TEMPLATE_FORM = """
-<div id="options">
-<form action='menu' method=GET>
-    <input type='hidden' name='base' value='%(base)s' />
-    Show percentages
-    <input type='checkbox' %(showpct)s name='showpct' value='checked' /><br />
-    Hide files over
-    <input type='text' id='pct' name='pct' value='%(pct)s' size='3' />%%<br />
-    Exclude files matching<br />
-    <input type='text' id='exclude' name='exclude'
-     value='%(exclude)s' size='20' />
-    <br />
-
-    <input type='submit' value='Change view' id="submit"/>
-</form>
-</div>"""
-
-TEMPLATE_FRAMESET = """<html>
-<head><title>CherryPy coverage data</title></head>
-<frameset cols='250, 1*'>
-    <frame src='menu?base=%s' />
-    <frame name='main' src='' />
-</frameset>
-</html>
-"""
-
-TEMPLATE_COVERAGE = """<html>
-<head>
-    <title>Coverage for %(name)s</title>
-    <style>
-        h2 { margin-bottom: .25em; }
-        p { margin: .25em; }
-        .covered { color: #000; background-color: #fff; }
-        .notcovered { color: #fee; background-color: #500; }
-        .excluded { color: #00f; background-color: #fff; }
-         table .covered, table .notcovered, table .excluded
-             { font-family: Andale Mono, monospace;
-               font-size: 10pt; white-space: pre; }
-
-         .lineno { background-color: #eee;}
-         .notcovered .lineno { background-color: #000;}
-         table { border-collapse: collapse;
-    </style>
-</head>
-<body>
-<h2>%(name)s</h2>
-<p>%(fullpath)s</p>
-<p>Coverage: %(pc)s%%</p>"""
-
-TEMPLATE_LOC_COVERED = """<tr class="covered">
-    <td class="lineno">%s&nbsp;</td>
-    <td>%s</td>
-</tr>\n"""
-TEMPLATE_LOC_NOT_COVERED = """<tr class="notcovered">
-    <td class="lineno">%s&nbsp;</td>
-    <td>%s</td>
-</tr>\n"""
-TEMPLATE_LOC_EXCLUDED = """<tr class="excluded">
-    <td class="lineno">%s&nbsp;</td>
-    <td>%s</td>
-</tr>\n"""
-
-TEMPLATE_ITEM = (
-    "%s%s<a class='file' href='report?name=%s' target='main'>%s</a>\n"
-)
-
-
-def _percent(statements, missing):
-    s = len(statements)
-    e = s - len(missing)
-    if s > 0:
-        return int(round(100.0 * e / s))
-    return 0
-
-
-def _show_branch(root, base, path, pct=0, showpct=False, exclude='',
-                 coverage=the_coverage):
-
-    # Show the directory name and any of our children
-    dirs = [k for k, v in root.items() if v]
-    dirs.sort()
-    for name in dirs:
-        newpath = os.path.join(path, name)
-
-        if newpath.lower().startswith(base):
-            relpath = newpath[len(base):]
-            yield '| ' * relpath.count(os.sep)
-            yield (
-                "<a class='directory' "
-                "href='menu?base=%s&exclude=%s'>%s</a>\n" %
-                (newpath, urllib.parse.quote_plus(exclude), name)
-            )
-
-        for chunk in _show_branch(
-            root[name], base, newpath, pct, showpct,
-            exclude, coverage=coverage
-        ):
-            yield chunk
-
-    # Now list the files
-    if path.lower().startswith(base):
-        relpath = path[len(base):]
-        files = [k for k, v in root.items() if not v]
-        files.sort()
-        for name in files:
-            newpath = os.path.join(path, name)
-
-            pc_str = ''
-            if showpct:
-                try:
-                    _, statements, _, missing, _ = coverage.analysis2(newpath)
-                except Exception:
-                    # Yes, we really want to pass on all errors.
-                    pass
-                else:
-                    pc = _percent(statements, missing)
-                    pc_str = ('%3d%% ' % pc).replace(' ', '&nbsp;')
-                    if pc < float(pct) or pc == -1:
-                        pc_str = "<span class='fail'>%s</span>" % pc_str
-                    else:
-                        pc_str = "<span class='pass'>%s</span>" % pc_str
-
-            yield TEMPLATE_ITEM % ('| ' * (relpath.count(os.sep) + 1),
-                                   pc_str, newpath, name)
-
-
-def _skip_file(path, exclude):
-    if exclude:
-        return bool(re.search(exclude, path))
-
-
-def _graft(path, tree):
-    d = tree
-
-    p = path
-    atoms = []
-    while True:
-        p, tail = os.path.split(p)
-        if not tail:
-            break
-        atoms.append(tail)
-    atoms.append(p)
-    if p != '/':
-        atoms.append('/')
-
-    atoms.reverse()
-    for node in atoms:
-        if node:
-            d = d.setdefault(node, {})
-
-
-def get_tree(base, exclude, coverage=the_coverage):
-    """Return covered module names as a nested dict."""
-    tree = {}
-    runs = coverage.data.executed_files()
-    for path in runs:
-        if not _skip_file(path, exclude) and not os.path.isdir(path):
-            _graft(path, tree)
-    return tree
-
-
-class CoverStats(object):
-
-    def __init__(self, coverage, root=None):
-        self.coverage = coverage
-        if root is None:
-            # Guess initial depth. Files outside this path will not be
-            # reachable from the web interface.
-            root = os.path.dirname(cherrypy.__file__)
-        self.root = root
-
-    @cherrypy.expose
-    def index(self):
-        return TEMPLATE_FRAMESET % self.root.lower()
-
-    @cherrypy.expose
-    def menu(self, base='/', pct='50', showpct='',
-             exclude=r'python\d\.\d|test|tut\d|tutorial'):
-
-        # The coverage module uses all-lower-case names.
-        base = base.lower().rstrip(os.sep)
-
-        yield TEMPLATE_MENU
-        yield TEMPLATE_FORM % locals()
-
-        # Start by showing links for parent paths
-        yield "<div id='crumbs'>"
-        path = ''
-        atoms = base.split(os.sep)
-        atoms.pop()
-        for atom in atoms:
-            path += atom + os.sep
-            yield ("<a href='menu?base=%s&exclude=%s'>%s</a> %s"
-                   % (path, urllib.parse.quote_plus(exclude), atom, os.sep))
-        yield '</div>'
-
-        yield "<div id='tree'>"
-
-        # Then display the tree
-        tree = get_tree(base, exclude, self.coverage)
-        if not tree:
-            yield '<p>No modules covered.</p>'
-        else:
-            for chunk in _show_branch(tree, base, '/', pct,
-                                      showpct == 'checked', exclude,
-                                      coverage=self.coverage):
-                yield chunk
-
-        yield '</div>'
-        yield '</body></html>'
-
-    def annotated_file(self, filename, statements, excluded, missing):
-        source = open(filename, 'r')
-        buffer = []
-        for lineno, line in enumerate(source.readlines()):
-            lineno += 1
-            line = line.strip('\n\r')
-            empty_the_buffer = True
-            if lineno in excluded:
-                template = TEMPLATE_LOC_EXCLUDED
-            elif lineno in missing:
-                template = TEMPLATE_LOC_NOT_COVERED
-            elif lineno in statements:
-                template = TEMPLATE_LOC_COVERED
-            else:
-                empty_the_buffer = False
-                buffer.append((lineno, line))
-            if empty_the_buffer:
-                for lno, pastline in buffer:
-                    yield template % (lno, cgi.escape(pastline))
-                buffer = []
-                yield template % (lineno, cgi.escape(line))
-
-    @cherrypy.expose
-    def report(self, name):
-        filename, statements, excluded, missing, _ = self.coverage.analysis2(
-            name)
-        pc = _percent(statements, missing)
-        yield TEMPLATE_COVERAGE % dict(name=os.path.basename(name),
-                                       fullpath=name,
-                                       pc=pc)
-        yield '<table>\n'
-        for line in self.annotated_file(filename, statements, excluded,
-                                        missing):
-            yield line
-        yield '</table>'
-        yield '</body>'
-        yield '</html>'
-
-
-def serve(path=localFile, port=8080, root=None):
-    if coverage is None:
-        raise ImportError('The coverage module could not be imported.')
-    from coverage import coverage
-    cov = coverage(data_file=path)
-    cov.load()
-
-    cherrypy.config.update({'server.socket_port': int(port),
-                            'server.thread_pool': 10,
-                            'environment': 'production',
-                            })
-    cherrypy.quickstart(CoverStats(cov, root))
-
-
-if __name__ == '__main__':
-    serve(*tuple(sys.argv[1:]))
diff --git a/libraries/cherrypy/lib/cpstats.py b/libraries/cherrypy/lib/cpstats.py
deleted file mode 100644
index ae9f7475..00000000
--- a/libraries/cherrypy/lib/cpstats.py
+++ /dev/null
@@ -1,696 +0,0 @@
-"""CPStats, a package for collecting and reporting on program statistics.
-
-Overview
-========
-
-Statistics about program operation are an invaluable monitoring and debugging
-tool. Unfortunately, the gathering and reporting of these critical values is
-usually ad-hoc. This package aims to add a centralized place for gathering
-statistical performance data, a structure for recording that data which
-provides for extrapolation of that data into more useful information,
-and a method of serving that data to both human investigators and
-monitoring software. Let's examine each of those in more detail.
-
-Data Gathering
---------------
-
-Just as Python's `logging` module provides a common importable for gathering
-and sending messages, performance statistics would benefit from a similar
-common mechanism, and one that does *not* require each package which wishes
-to collect stats to import a third-party module. Therefore, we choose to
-re-use the `logging` module by adding a `statistics` object to it.
-
-That `logging.statistics` object is a nested dict. It is not a custom class,
-because that would:
-
- 1. require libraries and applications to import a third-party module in
-    order to participate
- 2. inhibit innovation in extrapolation approaches and in reporting tools, and
- 3. be slow.
-
-There are, however, some specifications regarding the structure of the dict.::
-
-   {
-     +----"SQLAlchemy": {
-     |        "Inserts": 4389745,
-     |        "Inserts per Second":
-     |            lambda s: s["Inserts"] / (time() - s["Start"]),
-     |  C +---"Table Statistics": {
-     |  o |        "widgets": {-----------+
-   N |  l |            "Rows": 1.3M,      | Record
-   a |  l |            "Inserts": 400,    |
-   m |  e |        },---------------------+
-   e |  c |        "froobles": {
-   s |  t |            "Rows": 7845,
-   p |  i |            "Inserts": 0,
-   a |  o |        },
-   c |  n +---},
-   e |        "Slow Queries":
-     |            [{"Query": "SELECT * FROM widgets;",
-     |              "Processing Time": 47.840923343,
-     |              },
-     |             ],
-     +----},
-   }
-
-The `logging.statistics` dict has four levels. The topmost level is nothing
-more than a set of names to introduce modularity, usually along the lines of
-package names. If the SQLAlchemy project wanted to participate, for example,
-it might populate the item `logging.statistics['SQLAlchemy']`, whose value
-would be a second-layer dict we call a "namespace". Namespaces help multiple
-packages to avoid collisions over key names, and make reports easier to read,
-to boot. The maintainers of SQLAlchemy should feel free to use more than one
-namespace if needed (such as 'SQLAlchemy ORM'). Note that there are no case
-or other syntax constraints on the namespace names; they should be chosen
-to be maximally readable by humans (neither too short nor too long).
-
-Each namespace, then, is a dict of named statistical values, such as
-'Requests/sec' or 'Uptime'. You should choose names which will look
-good on a report: spaces and capitalization are just fine.
-
-In addition to scalars, values in a namespace MAY be a (third-layer)
-dict, or a list, called a "collection". For example, the CherryPy
-:class:`StatsTool` keeps track of what each request is doing (or has most
-recently done) in a 'Requests' collection, where each key is a thread ID; each
-value in the subdict MUST be a fourth dict (whew!) of statistical data about
-each thread. We call each subdict in the collection a "record". Similarly,
-the :class:`StatsTool` also keeps a list of slow queries, where each record
-contains data about each slow query, in order.
-
-Values in a namespace or record may also be functions, which brings us to:
-
-Extrapolation
--------------
-
-The collection of statistical data needs to be fast, as close to unnoticeable
-as possible to the host program. That requires us to minimize I/O, for example,
-but in Python it also means we need to minimize function calls. So when you
-are designing your namespace and record values, try to insert the most basic
-scalar values you already have on hand.
-
-When it comes time to report on the gathered data, however, we usually have
-much more freedom in what we can calculate. Therefore, whenever reporting
-tools (like the provided :class:`StatsPage` CherryPy class) fetch the contents
-of `logging.statistics` for reporting, they first call
-`extrapolate_statistics` (passing the whole `statistics` dict as the only
-argument). This makes a deep copy of the statistics dict so that the
-reporting tool can both iterate over it and even change it without harming
-the original. But it also expands any functions in the dict by calling them.
-For example, you might have a 'Current Time' entry in the namespace with the
-value "lambda scope: time.time()". The "scope" parameter is the current
-namespace dict (or record, if we're currently expanding one of those
-instead), allowing you access to existing static entries. If you're truly
-evil, you can even modify more than one entry at a time.
-
-However, don't try to calculate an entry and then use its value in further
-extrapolations; the order in which the functions are called is not guaranteed.
-This can lead to a certain amount of duplicated work (or a redesign of your
-schema), but that's better than complicating the spec.
-
-After the whole thing has been extrapolated, it's time for:
-
-Reporting
----------
-
-The :class:`StatsPage` class grabs the `logging.statistics` dict, extrapolates
-it all, and then transforms it to HTML for easy viewing. Each namespace gets
-its own header and attribute table, plus an extra table for each collection.
-This is NOT part of the statistics specification; other tools can format how
-they like.
-
-You can control which columns are output and how they are formatted by updating
-StatsPage.formatting, which is a dict that mirrors the keys and nesting of
-`logging.statistics`. The difference is that, instead of data values, it has
-formatting values. Use None for a given key to indicate to the StatsPage that a
-given column should not be output. Use a string with formatting
-(such as '%.3f') to interpolate the value(s), or use a callable (such as
-lambda v: v.isoformat()) for more advanced formatting. Any entry which is not
-mentioned in the formatting dict is output unchanged.
-
-Monitoring
-----------
-
-Although the HTML output takes pains to assign unique id's to each <td> with
-statistical data, you're probably better off fetching /cpstats/data, which
-outputs the whole (extrapolated) `logging.statistics` dict in JSON format.
-That is probably easier to parse, and doesn't have any formatting controls,
-so you get the "original" data in a consistently-serialized format.
-Note: there's no treatment yet for datetime objects. Try time.time() instead
-for now if you can. Nagios will probably thank you.
-
-Turning Collection Off
-----------------------
-
-It is recommended each namespace have an "Enabled" item which, if False,
-stops collection (but not reporting) of statistical data. Applications
-SHOULD provide controls to pause and resume collection by setting these
-entries to False or True, if present.
-
-
-Usage
-=====
-
-To collect statistics on CherryPy applications::
-
-    from cherrypy.lib import cpstats
-    appconfig['/']['tools.cpstats.on'] = True
-
-To collect statistics on your own code::
-
-    import logging
-    # Initialize the repository
-    if not hasattr(logging, 'statistics'): logging.statistics = {}
-    # Initialize my namespace
-    mystats = logging.statistics.setdefault('My Stuff', {})
-    # Initialize my namespace's scalars and collections
-    mystats.update({
-        'Enabled': True,
-        'Start Time': time.time(),
-        'Important Events': 0,
-        'Events/Second': lambda s: (
-            (s['Important Events'] / (time.time() - s['Start Time']))),
-        })
-    ...
-    for event in events:
-        ...
-        # Collect stats
-        if mystats.get('Enabled', False):
-            mystats['Important Events'] += 1
-
-To report statistics::
-
-    root.cpstats = cpstats.StatsPage()
-
-To format statistics reports::
-
-    See 'Reporting', above.
-
-"""
-
-import logging
-import os
-import sys
-import threading
-import time
-
-import six
-
-import cherrypy
-from cherrypy._cpcompat import json
-
-# ------------------------------- Statistics -------------------------------- #
-
-if not hasattr(logging, 'statistics'):
-    logging.statistics = {}
-
-
-def extrapolate_statistics(scope):
-    """Return an extrapolated copy of the given scope."""
-    c = {}
-    for k, v in list(scope.items()):
-        if isinstance(v, dict):
-            v = extrapolate_statistics(v)
-        elif isinstance(v, (list, tuple)):
-            v = [extrapolate_statistics(record) for record in v]
-        elif hasattr(v, '__call__'):
-            v = v(scope)
-        c[k] = v
-    return c
-
-
-# -------------------- CherryPy Applications Statistics --------------------- #
-
-appstats = logging.statistics.setdefault('CherryPy Applications', {})
-appstats.update({
-    'Enabled': True,
-    'Bytes Read/Request': lambda s: (
-        s['Total Requests'] and
-        (s['Total Bytes Read'] / float(s['Total Requests'])) or
-        0.0
-    ),
-    'Bytes Read/Second': lambda s: s['Total Bytes Read'] / s['Uptime'](s),
-    'Bytes Written/Request': lambda s: (
-        s['Total Requests'] and
-        (s['Total Bytes Written'] / float(s['Total Requests'])) or
-        0.0
-    ),
-    'Bytes Written/Second': lambda s: (
-        s['Total Bytes Written'] / s['Uptime'](s)
-    ),
-    'Current Time': lambda s: time.time(),
-    'Current Requests': 0,
-    'Requests/Second': lambda s: float(s['Total Requests']) / s['Uptime'](s),
-    'Server Version': cherrypy.__version__,
-    'Start Time': time.time(),
-    'Total Bytes Read': 0,
-    'Total Bytes Written': 0,
-    'Total Requests': 0,
-    'Total Time': 0,
-    'Uptime': lambda s: time.time() - s['Start Time'],
-    'Requests': {},
-})
-
-
-def proc_time(s):
-    return time.time() - s['Start Time']
-
-
-class ByteCountWrapper(object):
-
-    """Wraps a file-like object, counting the number of bytes read."""
-
-    def __init__(self, rfile):
-        self.rfile = rfile
-        self.bytes_read = 0
-
-    def read(self, size=-1):
-        data = self.rfile.read(size)
-        self.bytes_read += len(data)
-        return data
-
-    def readline(self, size=-1):
-        data = self.rfile.readline(size)
-        self.bytes_read += len(data)
-        return data
-
-    def readlines(self, sizehint=0):
-        # Shamelessly stolen from StringIO
-        total = 0
-        lines = []
-        line = self.readline()
-        while line:
-            lines.append(line)
-            total += len(line)
-            if 0 < sizehint <= total:
-                break
-            line = self.readline()
-        return lines
-
-    def close(self):
-        self.rfile.close()
-
-    def __iter__(self):
-        return self
-
-    def next(self):
-        data = self.rfile.next()
-        self.bytes_read += len(data)
-        return data
-
-
-def average_uriset_time(s):
-    return s['Count'] and (s['Sum'] / s['Count']) or 0
-
-
-def _get_threading_ident():
-    if sys.version_info >= (3, 3):
-        return threading.get_ident()
-    return threading._get_ident()
-
-
-class StatsTool(cherrypy.Tool):
-
-    """Record various information about the current request."""
-
-    def __init__(self):
-        cherrypy.Tool.__init__(self, 'on_end_request', self.record_stop)
-
-    def _setup(self):
-        """Hook this tool into cherrypy.request.
-
-        The standard CherryPy request object will automatically call this
-        method when the tool is "turned on" in config.
-        """
-        if appstats.get('Enabled', False):
-            cherrypy.Tool._setup(self)
-            self.record_start()
-
-    def record_start(self):
-        """Record the beginning of a request."""
-        request = cherrypy.serving.request
-        if not hasattr(request.rfile, 'bytes_read'):
-            request.rfile = ByteCountWrapper(request.rfile)
-            request.body.fp = request.rfile
-
-        r = request.remote
-
-        appstats['Current Requests'] += 1
-        appstats['Total Requests'] += 1
-        appstats['Requests'][_get_threading_ident()] = {
-            'Bytes Read': None,
-            'Bytes Written': None,
-            # Use a lambda so the ip gets updated by tools.proxy later
-            'Client': lambda s: '%s:%s' % (r.ip, r.port),
-            'End Time': None,
-            'Processing Time': proc_time,
-            'Request-Line': request.request_line,
-            'Response Status': None,
-            'Start Time': time.time(),
-        }
-
-    def record_stop(
-            self, uriset=None, slow_queries=1.0, slow_queries_count=100,
-            debug=False, **kwargs):
-        """Record the end of a request."""
-        resp = cherrypy.serving.response
-        w = appstats['Requests'][_get_threading_ident()]
-
-        r = cherrypy.request.rfile.bytes_read
-        w['Bytes Read'] = r
-        appstats['Total Bytes Read'] += r
-
-        if resp.stream:
-            w['Bytes Written'] = 'chunked'
-        else:
-            cl = int(resp.headers.get('Content-Length', 0))
-            w['Bytes Written'] = cl
-            appstats['Total Bytes Written'] += cl
-
-        w['Response Status'] = getattr(
-            resp, 'output_status', None) or resp.status
-
-        w['End Time'] = time.time()
-        p = w['End Time'] - w['Start Time']
-        w['Processing Time'] = p
-        appstats['Total Time'] += p
-
-        appstats['Current Requests'] -= 1
-
-        if debug:
-            cherrypy.log('Stats recorded: %s' % repr(w), 'TOOLS.CPSTATS')
-
-        if uriset:
-            rs = appstats.setdefault('URI Set Tracking', {})
-            r = rs.setdefault(uriset, {
-                'Min': None, 'Max': None, 'Count': 0, 'Sum': 0,
-                'Avg': average_uriset_time})
-            if r['Min'] is None or p < r['Min']:
-                r['Min'] = p
-            if r['Max'] is None or p > r['Max']:
-                r['Max'] = p
-            r['Count'] += 1
-            r['Sum'] += p
-
-        if slow_queries and p > slow_queries:
-            sq = appstats.setdefault('Slow Queries', [])
-            sq.append(w.copy())
-            if len(sq) > slow_queries_count:
-                sq.pop(0)
-
-
-cherrypy.tools.cpstats = StatsTool()
-
-
-# ---------------------- CherryPy Statistics Reporting ---------------------- #
-
-thisdir = os.path.abspath(os.path.dirname(__file__))
-
-missing = object()
-
-
-def locale_date(v):
-    return time.strftime('%c', time.gmtime(v))
-
-
-def iso_format(v):
-    return time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(v))
-
-
-def pause_resume(ns):
-    def _pause_resume(enabled):
-        pause_disabled = ''
-        resume_disabled = ''
-        if enabled:
-            resume_disabled = 'disabled="disabled" '
-        else:
-            pause_disabled = 'disabled="disabled" '
-        return """
-            <form action="pause" method="POST" style="display:inline">
-            <input type="hidden" name="namespace" value="%s" />
-            <input type="submit" value="Pause" %s/>
-            </form>
-            <form action="resume" method="POST" style="display:inline">
-            <input type="hidden" name="namespace" value="%s" />
-            <input type="submit" value="Resume" %s/>
-            </form>
-            """ % (ns, pause_disabled, ns, resume_disabled)
-    return _pause_resume
-
-
-class StatsPage(object):
-
-    formatting = {
-        'CherryPy Applications': {
-            'Enabled': pause_resume('CherryPy Applications'),
-            'Bytes Read/Request': '%.3f',
-            'Bytes Read/Second': '%.3f',
-            'Bytes Written/Request': '%.3f',
-            'Bytes Written/Second': '%.3f',
-            'Current Time': iso_format,
-            'Requests/Second': '%.3f',
-            'Start Time': iso_format,
-            'Total Time': '%.3f',
-            'Uptime': '%.3f',
-            'Slow Queries': {
-                'End Time': None,
-                'Processing Time': '%.3f',
-                'Start Time': iso_format,
-            },
-            'URI Set Tracking': {
-                'Avg': '%.3f',
-                'Max': '%.3f',
-                'Min': '%.3f',
-                'Sum': '%.3f',
-            },
-            'Requests': {
-                'Bytes Read': '%s',
-                'Bytes Written': '%s',
-                'End Time': None,
-                'Processing Time': '%.3f',
-                'Start Time': None,
-            },
-        },
-        'CherryPy WSGIServer': {
-            'Enabled': pause_resume('CherryPy WSGIServer'),
-            'Connections/second': '%.3f',
-            'Start time': iso_format,
-        },
-    }
-
-    @cherrypy.expose
-    def index(self):
-        # Transform the raw data into pretty output for HTML
-        yield """
-<html>
-<head>
-    <title>Statistics</title>
-<style>
-
-th, td {
-    padding: 0.25em 0.5em;
-    border: 1px solid #666699;
-}
-
-table {
-    border-collapse: collapse;
-}
-
-table.stats1 {
-    width: 100%;
-}
-
-table.stats1 th {
-    font-weight: bold;
-    text-align: right;
-    background-color: #CCD5DD;
-}
-
-table.stats2, h2 {
-    margin-left: 50px;
-}
-
-table.stats2 th {
-    font-weight: bold;
-    text-align: center;
-    background-color: #CCD5DD;
-}
-
-</style>
-</head>
-<body>
-"""
-        for title, scalars, collections in self.get_namespaces():
-            yield """
-<h1>%s</h1>
-
-<table class='stats1'>
-    <tbody>
-""" % title
-            for i, (key, value) in enumerate(scalars):
-                colnum = i % 3
-                if colnum == 0:
-                    yield """
-        <tr>"""
-                yield (
-                    """
-            <th>%(key)s</th><td id='%(title)s-%(key)s'>%(value)s</td>""" %
-                    vars()
-                )
-                if colnum == 2:
-                    yield """
-        </tr>"""
-
-            if colnum == 0:
-                yield """
-            <th></th><td></td>
-            <th></th><td></td>
-        </tr>"""
-            elif colnum == 1:
-                yield """
-            <th></th><td></td>
-        </tr>"""
-            yield """
-    </tbody>
-</table>"""
-
-            for subtitle, headers, subrows in collections:
-                yield """
-<h2>%s</h2>
-<table class='stats2'>
-    <thead>
-        <tr>""" % subtitle
-                for key in headers:
-                    yield """
-            <th>%s</th>""" % key
-                yield """
-        </tr>
-    </thead>
-    <tbody>"""
-                for subrow in subrows:
-                    yield """
-        <tr>"""
-                    for value in subrow:
-                        yield """
-            <td>%s</td>""" % value
-                    yield """
-        </tr>"""
-                yield """
-    </tbody>
-</table>"""
-        yield """
-</body>
-</html>
-"""
-
-    def get_namespaces(self):
-        """Yield (title, scalars, collections) for each namespace."""
-        s = extrapolate_statistics(logging.statistics)
-        for title, ns in sorted(s.items()):
-            scalars = []
-            collections = []
-            ns_fmt = self.formatting.get(title, {})
-            for k, v in sorted(ns.items()):
-                fmt = ns_fmt.get(k, {})
-                if isinstance(v, dict):
-                    headers, subrows = self.get_dict_collection(v, fmt)
-                    collections.append((k, ['ID'] + headers, subrows))
-                elif isinstance(v, (list, tuple)):
-                    headers, subrows = self.get_list_collection(v, fmt)
-                    collections.append((k, headers, subrows))
-                else:
-                    format = ns_fmt.get(k, missing)
-                    if format is None:
-                        # Don't output this column.
-                        continue
-                    if hasattr(format, '__call__'):
-                        v = format(v)
-                    elif format is not missing:
-                        v = format % v
-                    scalars.append((k, v))
-            yield title, scalars, collections
-
-    def get_dict_collection(self, v, formatting):
-        """Return ([headers], [rows]) for the given collection."""
-        # E.g., the 'Requests' dict.
-        headers = []
-        vals = six.itervalues(v)
-        for record in vals:
-            for k3 in record:
-                format = formatting.get(k3, missing)
-                if format is None:
-                    # Don't output this column.
-                    continue
-                if k3 not in headers:
-                    headers.append(k3)
-        headers.sort()
-
-        subrows = []
-        for k2, record in sorted(v.items()):
-            subrow = [k2]
-            for k3 in headers:
-                v3 = record.get(k3, '')
-                format = formatting.get(k3, missing)
-                if format is None:
-                    # Don't output this column.
-                    continue
-                if hasattr(format, '__call__'):
-                    v3 = format(v3)
-                elif format is not missing:
-                    v3 = format % v3
-                subrow.append(v3)
-            subrows.append(subrow)
-
-        return headers, subrows
-
-    def get_list_collection(self, v, formatting):
-        """Return ([headers], [subrows]) for the given collection."""
-        # E.g., the 'Slow Queries' list.
-        headers = []
-        for record in v:
-            for k3 in record:
-                format = formatting.get(k3, missing)
-                if format is None:
-                    # Don't output this column.
-                    continue
-                if k3 not in headers:
-                    headers.append(k3)
-        headers.sort()
-
-        subrows = []
-        for record in v:
-            subrow = []
-            for k3 in headers:
-                v3 = record.get(k3, '')
-                format = formatting.get(k3, missing)
-                if format is None:
-                    # Don't output this column.
-                    continue
-                if hasattr(format, '__call__'):
-                    v3 = format(v3)
-                elif format is not missing:
-                    v3 = format % v3
-                subrow.append(v3)
-            subrows.append(subrow)
-
-        return headers, subrows
-
-    if json is not None:
-        @cherrypy.expose
-        def data(self):
-            s = extrapolate_statistics(logging.statistics)
-            cherrypy.response.headers['Content-Type'] = 'application/json'
-            return json.dumps(s, sort_keys=True, indent=4)
-
-    @cherrypy.expose
-    def pause(self, namespace):
-        logging.statistics.get(namespace, {})['Enabled'] = False
-        raise cherrypy.HTTPRedirect('./')
-    pause.cp_config = {'tools.allow.on': True,
-                       'tools.allow.methods': ['POST']}
-
-    @cherrypy.expose
-    def resume(self, namespace):
-        logging.statistics.get(namespace, {})['Enabled'] = True
-        raise cherrypy.HTTPRedirect('./')
-    resume.cp_config = {'tools.allow.on': True,
-                        'tools.allow.methods': ['POST']}
diff --git a/libraries/cherrypy/lib/cptools.py b/libraries/cherrypy/lib/cptools.py
deleted file mode 100644
index 1c079634..00000000
--- a/libraries/cherrypy/lib/cptools.py
+++ /dev/null
@@ -1,640 +0,0 @@
-"""Functions for builtin CherryPy tools."""
-
-import logging
-import re
-from hashlib import md5
-
-import six
-from six.moves import urllib
-
-import cherrypy
-from cherrypy._cpcompat import text_or_bytes
-from cherrypy.lib import httputil as _httputil
-from cherrypy.lib import is_iterator
-
-
-#                     Conditional HTTP request support                     #
-
-def validate_etags(autotags=False, debug=False):
-    """Validate the current ETag against If-Match, If-None-Match headers.
-
-    If autotags is True, an ETag response-header value will be provided
-    from an MD5 hash of the response body (unless some other code has
-    already provided an ETag header). If False (the default), the ETag
-    will not be automatic.
-
-    WARNING: the autotags feature is not designed for URL's which allow
-    methods other than GET. For example, if a POST to the same URL returns
-    no content, the automatic ETag will be incorrect, breaking a fundamental
-    use for entity tags in a possibly destructive fashion. Likewise, if you
-    raise 304 Not Modified, the response body will be empty, the ETag hash
-    will be incorrect, and your application will break.
-    See :rfc:`2616` Section 14.24.
-    """
-    response = cherrypy.serving.response
-
-    # Guard against being run twice.
-    if hasattr(response, 'ETag'):
-        return
-
-    status, reason, msg = _httputil.valid_status(response.status)
-
-    etag = response.headers.get('ETag')
-
-    # Automatic ETag generation. See warning in docstring.
-    if etag:
-        if debug:
-            cherrypy.log('ETag already set: %s' % etag, 'TOOLS.ETAGS')
-    elif not autotags:
-        if debug:
-            cherrypy.log('Autotags off', 'TOOLS.ETAGS')
-    elif status != 200:
-        if debug:
-            cherrypy.log('Status not 200', 'TOOLS.ETAGS')
-    else:
-        etag = response.collapse_body()
-        etag = '"%s"' % md5(etag).hexdigest()
-        if debug:
-            cherrypy.log('Setting ETag: %s' % etag, 'TOOLS.ETAGS')
-        response.headers['ETag'] = etag
-
-    response.ETag = etag
-
-    # "If the request would, without the If-Match header field, result in
-    # anything other than a 2xx or 412 status, then the If-Match header
-    # MUST be ignored."
-    if debug:
-        cherrypy.log('Status: %s' % status, 'TOOLS.ETAGS')
-    if status >= 200 and status <= 299:
-        request = cherrypy.serving.request
-
-        conditions = request.headers.elements('If-Match') or []
-        conditions = [str(x) for x in conditions]
-        if debug:
-            cherrypy.log('If-Match conditions: %s' % repr(conditions),
-                         'TOOLS.ETAGS')
-        if conditions and not (conditions == ['*'] or etag in conditions):
-            raise cherrypy.HTTPError(412, 'If-Match failed: ETag %r did '
-                                     'not match %r' % (etag, conditions))
-
-        conditions = request.headers.elements('If-None-Match') or []
-        conditions = [str(x) for x in conditions]
-        if debug:
-            cherrypy.log('If-None-Match conditions: %s' % repr(conditions),
-                         'TOOLS.ETAGS')
-        if conditions == ['*'] or etag in conditions:
-            if debug:
-                cherrypy.log('request.method: %s' %
-                             request.method, 'TOOLS.ETAGS')
-            if request.method in ('GET', 'HEAD'):
-                raise cherrypy.HTTPRedirect([], 304)
-            else:
-                raise cherrypy.HTTPError(412, 'If-None-Match failed: ETag %r '
-                                         'matched %r' % (etag, conditions))
-
-
-def validate_since():
-    """Validate the current Last-Modified against If-Modified-Since headers.
-
-    If no code has set the Last-Modified response header, then no validation
-    will be performed.
-    """
-    response = cherrypy.serving.response
-    lastmod = response.headers.get('Last-Modified')
-    if lastmod:
-        status, reason, msg = _httputil.valid_status(response.status)
-
-        request = cherrypy.serving.request
-
-        since = request.headers.get('If-Unmodified-Since')
-        if since and since != lastmod:
-            if (status >= 200 and status <= 299) or status == 412:
-                raise cherrypy.HTTPError(412)
-
-        since = request.headers.get('If-Modified-Since')
-        if since and since == lastmod:
-            if (status >= 200 and status <= 299) or status == 304:
-                if request.method in ('GET', 'HEAD'):
-                    raise cherrypy.HTTPRedirect([], 304)
-                else:
-                    raise cherrypy.HTTPError(412)
-
-
-#                                Tool code                                #
-
-def allow(methods=None, debug=False):
-    """Raise 405 if request.method not in methods (default ['GET', 'HEAD']).
-
-    The given methods are case-insensitive, and may be in any order.
-    If only one method is allowed, you may supply a single string;
-    if more than one, supply a list of strings.
-
-    Regardless of whether the current method is allowed or not, this
-    also emits an 'Allow' response header, containing the given methods.
-    """
-    if not isinstance(methods, (tuple, list)):
-        methods = [methods]
-    methods = [m.upper() for m in methods if m]
-    if not methods:
-        methods = ['GET', 'HEAD']
-    elif 'GET' in methods and 'HEAD' not in methods:
-        methods.append('HEAD')
-
-    cherrypy.response.headers['Allow'] = ', '.join(methods)
-    if cherrypy.request.method not in methods:
-        if debug:
-            cherrypy.log('request.method %r not in methods %r' %
-                         (cherrypy.request.method, methods), 'TOOLS.ALLOW')
-        raise cherrypy.HTTPError(405)
-    else:
-        if debug:
-            cherrypy.log('request.method %r in methods %r' %
-                         (cherrypy.request.method, methods), 'TOOLS.ALLOW')
-
-
-def proxy(base=None, local='X-Forwarded-Host', remote='X-Forwarded-For',
-          scheme='X-Forwarded-Proto', debug=False):
-    """Change the base URL (scheme://host[:port][/path]).
-
-    For running a CP server behind Apache, lighttpd, or other HTTP server.
-
-    For Apache and lighttpd, you should leave the 'local' argument at the
-    default value of 'X-Forwarded-Host'. For Squid, you probably want to set
-    tools.proxy.local = 'Origin'.
-
-    If you want the new request.base to include path info (not just the host),
-    you must explicitly set base to the full base path, and ALSO set 'local'
-    to '', so that the X-Forwarded-Host request header (which never includes
-    path info) does not override it. Regardless, the value for 'base' MUST
-    NOT end in a slash.
-
-    cherrypy.request.remote.ip (the IP address of the client) will be
-    rewritten if the header specified by the 'remote' arg is valid.
-    By default, 'remote' is set to 'X-Forwarded-For'. If you do not
-    want to rewrite remote.ip, set the 'remote' arg to an empty string.
-    """
-
-    request = cherrypy.serving.request
-
-    if scheme:
-        s = request.headers.get(scheme, None)
-        if debug:
-            cherrypy.log('Testing scheme %r:%r' % (scheme, s), 'TOOLS.PROXY')
-        if s == 'on' and 'ssl' in scheme.lower():
-            # This handles e.g. webfaction's 'X-Forwarded-Ssl: on' header
-            scheme = 'https'
-        else:
-            # This is for lighttpd/pound/Mongrel's 'X-Forwarded-Proto: https'
-            scheme = s
-    if not scheme:
-        scheme = request.base[:request.base.find('://')]
-
-    if local:
-        lbase = request.headers.get(local, None)
-        if debug:
-            cherrypy.log('Testing local %r:%r' % (local, lbase), 'TOOLS.PROXY')
-        if lbase is not None:
-            base = lbase.split(',')[0]
-    if not base:
-        default = urllib.parse.urlparse(request.base).netloc
-        base = request.headers.get('Host', default)
-
-    if base.find('://') == -1:
-        # add http:// or https:// if needed
-        base = scheme + '://' + base
-
-    request.base = base
-
-    if remote:
-        xff = request.headers.get(remote)
-        if debug:
-            cherrypy.log('Testing remote %r:%r' % (remote, xff), 'TOOLS.PROXY')
-        if xff:
-            if remote == 'X-Forwarded-For':
-                # Grab the first IP in a comma-separated list. Ref #1268.
-                xff = next(ip.strip() for ip in xff.split(','))
-            request.remote.ip = xff
-
-
-def ignore_headers(headers=('Range',), debug=False):
-    """Delete request headers whose field names are included in 'headers'.
-
-    This is a useful tool for working behind certain HTTP servers;
-    for example, Apache duplicates the work that CP does for 'Range'
-    headers, and will doubly-truncate the response.
-    """
-    request = cherrypy.serving.request
-    for name in headers:
-        if name in request.headers:
-            if debug:
-                cherrypy.log('Ignoring request header %r' % name,
-                             'TOOLS.IGNORE_HEADERS')
-            del request.headers[name]
-
-
-def response_headers(headers=None, debug=False):
-    """Set headers on the response."""
-    if debug:
-        cherrypy.log('Setting response headers: %s' % repr(headers),
-                     'TOOLS.RESPONSE_HEADERS')
-    for name, value in (headers or []):
-        cherrypy.serving.response.headers[name] = value
-
-
-response_headers.failsafe = True
-
-
-def referer(pattern, accept=True, accept_missing=False, error=403,
-            message='Forbidden Referer header.', debug=False):
-    """Raise HTTPError if Referer header does/does not match the given pattern.
-
-    pattern
-        A regular expression pattern to test against the Referer.
-
-    accept
-        If True, the Referer must match the pattern; if False,
-        the Referer must NOT match the pattern.
-
-    accept_missing
-        If True, permit requests with no Referer header.
-
-    error
-        The HTTP error code to return to the client on failure.
-
-    message
-        A string to include in the response body on failure.
-
-    """
-    try:
-        ref = cherrypy.serving.request.headers['Referer']
-        match = bool(re.match(pattern, ref))
-        if debug:
-            cherrypy.log('Referer %r matches %r' % (ref, pattern),
-                         'TOOLS.REFERER')
-        if accept == match:
-            return
-    except KeyError:
-        if debug:
-            cherrypy.log('No Referer header', 'TOOLS.REFERER')
-        if accept_missing:
-            return
-
-    raise cherrypy.HTTPError(error, message)
-
-
-class SessionAuth(object):
-
-    """Assert that the user is logged in."""
-
-    session_key = 'username'
-    debug = False
-
-    def check_username_and_password(self, username, password):
-        pass
-
-    def anonymous(self):
-        """Provide a temporary user name for anonymous users."""
-        pass
-
-    def on_login(self, username):
-        pass
-
-    def on_logout(self, username):
-        pass
-
-    def on_check(self, username):
-        pass
-
-    def login_screen(self, from_page='..', username='', error_msg='',
-                     **kwargs):
-        return (six.text_type("""<html><body>
-Message: %(error_msg)s
-<form method="post" action="do_login">
-    Login: <input type="text" name="username" value="%(username)s" size="10" />
-    <br />
-    Password: <input type="password" name="password" size="10" />
-    <br />
-    <input type="hidden" name="from_page" value="%(from_page)s" />
-    <br />
-    <input type="submit" />
-</form>
-</body></html>""") % vars()).encode('utf-8')
-
-    def do_login(self, username, password, from_page='..', **kwargs):
-        """Login. May raise redirect, or return True if request handled."""
-        response = cherrypy.serving.response
-        error_msg = self.check_username_and_password(username, password)
-        if error_msg:
-            body = self.login_screen(from_page, username, error_msg)
-            response.body = body
-            if 'Content-Length' in response.headers:
-                # Delete Content-Length header so finalize() recalcs it.
-                del response.headers['Content-Length']
-            return True
-        else:
-            cherrypy.serving.request.login = username
-            cherrypy.session[self.session_key] = username
-            self.on_login(username)
-            raise cherrypy.HTTPRedirect(from_page or '/')
-
-    def do_logout(self, from_page='..', **kwargs):
-        """Logout. May raise redirect, or return True if request handled."""
-        sess = cherrypy.session
-        username = sess.get(self.session_key)
-        sess[self.session_key] = None
-        if username:
-            cherrypy.serving.request.login = None
-            self.on_logout(username)
-        raise cherrypy.HTTPRedirect(from_page)
-
-    def do_check(self):
-        """Assert username. Raise redirect, or return True if request handled.
-        """
-        sess = cherrypy.session
-        request = cherrypy.serving.request
-        response = cherrypy.serving.response
-
-        username = sess.get(self.session_key)
-        if not username:
-            sess[self.session_key] = username = self.anonymous()
-            self._debug_message('No session[username], trying anonymous')
-        if not username:
-            url = cherrypy.url(qs=request.query_string)
-            self._debug_message(
-                'No username, routing to login_screen with from_page %(url)r',
-                locals(),
-            )
-            response.body = self.login_screen(url)
-            if 'Content-Length' in response.headers:
-                # Delete Content-Length header so finalize() recalcs it.
-                del response.headers['Content-Length']
-            return True
-        self._debug_message('Setting request.login to %(username)r', locals())
-        request.login = username
-        self.on_check(username)
-
-    def _debug_message(self, template, context={}):
-        if not self.debug:
-            return
-        cherrypy.log(template % context, 'TOOLS.SESSAUTH')
-
-    def run(self):
-        request = cherrypy.serving.request
-        response = cherrypy.serving.response
-
-        path = request.path_info
-        if path.endswith('login_screen'):
-            self._debug_message('routing %(path)r to login_screen', locals())
-            response.body = self.login_screen()
-            return True
-        elif path.endswith('do_login'):
-            if request.method != 'POST':
-                response.headers['Allow'] = 'POST'
-                self._debug_message('do_login requires POST')
-                raise cherrypy.HTTPError(405)
-            self._debug_message('routing %(path)r to do_login', locals())
-            return self.do_login(**request.params)
-        elif path.endswith('do_logout'):
-            if request.method != 'POST':
-                response.headers['Allow'] = 'POST'
-                raise cherrypy.HTTPError(405)
-            self._debug_message('routing %(path)r to do_logout', locals())
-            return self.do_logout(**request.params)
-        else:
-            self._debug_message('No special path, running do_check')
-            return self.do_check()
-
-
-def session_auth(**kwargs):
-    sa = SessionAuth()
-    for k, v in kwargs.items():
-        setattr(sa, k, v)
-    return sa.run()
-
-
-session_auth.__doc__ = (
-    """Session authentication hook.
-
-    Any attribute of the SessionAuth class may be overridden via a keyword arg
-    to this function:
-
-    """ + '\n'.join(['%s: %s' % (k, type(getattr(SessionAuth, k)).__name__)
-                     for k in dir(SessionAuth) if not k.startswith('__')])
-)
-
-
-def log_traceback(severity=logging.ERROR, debug=False):
-    """Write the last error's traceback to the cherrypy error log."""
-    cherrypy.log('', 'HTTP', severity=severity, traceback=True)
-
-
-def log_request_headers(debug=False):
-    """Write request headers to the cherrypy error log."""
-    h = ['  %s: %s' % (k, v) for k, v in cherrypy.serving.request.header_list]
-    cherrypy.log('\nRequest Headers:\n' + '\n'.join(h), 'HTTP')
-
-
-def log_hooks(debug=False):
-    """Write request.hooks to the cherrypy error log."""
-    request = cherrypy.serving.request
-
-    msg = []
-    # Sort by the standard points if possible.
-    from cherrypy import _cprequest
-    points = _cprequest.hookpoints
-    for k in request.hooks.keys():
-        if k not in points:
-            points.append(k)
-
-    for k in points:
-        msg.append('    %s:' % k)
-        v = request.hooks.get(k, [])
-        v.sort()
-        for h in v:
-            msg.append('        %r' % h)
-    cherrypy.log('\nRequest Hooks for ' + cherrypy.url() +
-                 ':\n' + '\n'.join(msg), 'HTTP')
-
-
-def redirect(url='', internal=True, debug=False):
-    """Raise InternalRedirect or HTTPRedirect to the given url."""
-    if debug:
-        cherrypy.log('Redirecting %sto: %s' %
-                     ({True: 'internal ', False: ''}[internal], url),
-                     'TOOLS.REDIRECT')
-    if internal:
-        raise cherrypy.InternalRedirect(url)
-    else:
-        raise cherrypy.HTTPRedirect(url)
-
-
-def trailing_slash(missing=True, extra=False, status=None, debug=False):
-    """Redirect if path_info has (missing|extra) trailing slash."""
-    request = cherrypy.serving.request
-    pi = request.path_info
-
-    if debug:
-        cherrypy.log('is_index: %r, missing: %r, extra: %r, path_info: %r' %
-                     (request.is_index, missing, extra, pi),
-                     'TOOLS.TRAILING_SLASH')
-    if request.is_index is True:
-        if missing:
-            if not pi.endswith('/'):
-                new_url = cherrypy.url(pi + '/', request.query_string)
-                raise cherrypy.HTTPRedirect(new_url, status=status or 301)
-    elif request.is_index is False:
-        if extra:
-            # If pi == '/', don't redirect to ''!
-            if pi.endswith('/') and pi != '/':
-                new_url = cherrypy.url(pi[:-1], request.query_string)
-                raise cherrypy.HTTPRedirect(new_url, status=status or 301)
-
-
-def flatten(debug=False):
-    """Wrap response.body in a generator that recursively iterates over body.
-
-    This allows cherrypy.response.body to consist of 'nested generators';
-    that is, a set of generators that yield generators.
-    """
-    def flattener(input):
-        numchunks = 0
-        for x in input:
-            if not is_iterator(x):
-                numchunks += 1
-                yield x
-            else:
-                for y in flattener(x):
-                    numchunks += 1
-                    yield y
-        if debug:
-            cherrypy.log('Flattened %d chunks' % numchunks, 'TOOLS.FLATTEN')
-    response = cherrypy.serving.response
-    response.body = flattener(response.body)
-
-
-def accept(media=None, debug=False):
-    """Return the client's preferred media-type (from the given Content-Types).
-
-    If 'media' is None (the default), no test will be performed.
-
-    If 'media' is provided, it should be the Content-Type value (as a string)
-    or values (as a list or tuple of strings) which the current resource
-    can emit. The client's acceptable media ranges (as declared in the
-    Accept request header) will be matched in order to these Content-Type
-    values; the first such string is returned. That is, the return value
-    will always be one of the strings provided in the 'media' arg (or None
-    if 'media' is None).
-
-    If no match is found, then HTTPError 406 (Not Acceptable) is raised.
-    Note that most web browsers send */* as a (low-quality) acceptable
-    media range, which should match any Content-Type. In addition, "...if
-    no Accept header field is present, then it is assumed that the client
-    accepts all media types."
-
-    Matching types are checked in order of client preference first,
-    and then in the order of the given 'media' values.
-
-    Note that this function does not honor accept-params (other than "q").
-    """
-    if not media:
-        return
-    if isinstance(media, text_or_bytes):
-        media = [media]
-    request = cherrypy.serving.request
-
-    # Parse the Accept request header, and try to match one
-    # of the requested media-ranges (in order of preference).
-    ranges = request.headers.elements('Accept')
-    if not ranges:
-        # Any media type is acceptable.
-        if debug:
-            cherrypy.log('No Accept header elements', 'TOOLS.ACCEPT')
-        return media[0]
-    else:
-        # Note that 'ranges' is sorted in order of preference
-        for element in ranges:
-            if element.qvalue > 0:
-                if element.value == '*/*':
-                    # Matches any type or subtype
-                    if debug:
-                        cherrypy.log('Match due to */*', 'TOOLS.ACCEPT')
-                    return media[0]
-                elif element.value.endswith('/*'):
-                    # Matches any subtype
-                    mtype = element.value[:-1]  # Keep the slash
-                    for m in media:
-                        if m.startswith(mtype):
-                            if debug:
-                                cherrypy.log('Match due to %s' % element.value,
-                                             'TOOLS.ACCEPT')
-                            return m
-                else:
-                    # Matches exact value
-                    if element.value in media:
-                        if debug:
-                            cherrypy.log('Match due to %s' % element.value,
-                                         'TOOLS.ACCEPT')
-                        return element.value
-
-    # No suitable media-range found.
-    ah = request.headers.get('Accept')
-    if ah is None:
-        msg = 'Your client did not send an Accept header.'
-    else:
-        msg = 'Your client sent this Accept header: %s.' % ah
-    msg += (' But this resource only emits these media types: %s.' %
-            ', '.join(media))
-    raise cherrypy.HTTPError(406, msg)
-
-
-class MonitoredHeaderMap(_httputil.HeaderMap):
-
-    def transform_key(self, key):
-        self.accessed_headers.add(key)
-        return super(MonitoredHeaderMap, self).transform_key(key)
-
-    def __init__(self):
-        self.accessed_headers = set()
-        super(MonitoredHeaderMap, self).__init__()
-
-
-def autovary(ignore=None, debug=False):
-    """Auto-populate the Vary response header based on request.header access.
-    """
-    request = cherrypy.serving.request
-
-    req_h = request.headers
-    request.headers = MonitoredHeaderMap()
-    request.headers.update(req_h)
-    if ignore is None:
-        ignore = set(['Content-Disposition', 'Content-Length', 'Content-Type'])
-
-    def set_response_header():
-        resp_h = cherrypy.serving.response.headers
-        v = set([e.value for e in resp_h.elements('Vary')])
-        if debug:
-            cherrypy.log(
-                'Accessed headers: %s' % request.headers.accessed_headers,
-                'TOOLS.AUTOVARY')
-        v = v.union(request.headers.accessed_headers)
-        v = v.difference(ignore)
-        v = list(v)
-        v.sort()
-        resp_h['Vary'] = ', '.join(v)
-    request.hooks.attach('before_finalize', set_response_header, 95)
-
-
-def convert_params(exception=ValueError, error=400):
-    """Convert request params based on function annotations, with error handling.
-
-    exception
-        Exception class to catch.
-
-    status
-        The HTTP error code to return to the client on failure.
-    """
-    request = cherrypy.serving.request
-    types = request.handler.callable.__annotations__
-    with cherrypy.HTTPError.handle(exception, error):
-        for key in set(types).intersection(request.params):
-            request.params[key] = types[key](request.params[key])
diff --git a/libraries/cherrypy/lib/encoding.py b/libraries/cherrypy/lib/encoding.py
deleted file mode 100644
index 3d001ca6..00000000
--- a/libraries/cherrypy/lib/encoding.py
+++ /dev/null
@@ -1,436 +0,0 @@
-import struct
-import time
-import io
-
-import six
-
-import cherrypy
-from cherrypy._cpcompat import text_or_bytes
-from cherrypy.lib import file_generator
-from cherrypy.lib import is_closable_iterator
-from cherrypy.lib import set_vary_header
-
-
-def decode(encoding=None, default_encoding='utf-8'):
-    """Replace or extend the list of charsets used to decode a request entity.
-
-    Either argument may be a single string or a list of strings.
-
-    encoding
-        If not None, restricts the set of charsets attempted while decoding
-        a request entity to the given set (even if a different charset is
-        given in the Content-Type request header).
-
-    default_encoding
-        Only in effect if the 'encoding' argument is not given.
-        If given, the set of charsets attempted while decoding a request
-        entity is *extended* with the given value(s).
-
-    """
-    body = cherrypy.request.body
-    if encoding is not None:
-        if not isinstance(encoding, list):
-            encoding = [encoding]
-        body.attempt_charsets = encoding
-    elif default_encoding:
-        if not isinstance(default_encoding, list):
-            default_encoding = [default_encoding]
-        body.attempt_charsets = body.attempt_charsets + default_encoding
-
-
-class UTF8StreamEncoder:
-    def __init__(self, iterator):
-        self._iterator = iterator
-
-    def __iter__(self):
-        return self
-
-    def next(self):
-        return self.__next__()
-
-    def __next__(self):
-        res = next(self._iterator)
-        if isinstance(res, six.text_type):
-            res = res.encode('utf-8')
-        return res
-
-    def close(self):
-        if is_closable_iterator(self._iterator):
-            self._iterator.close()
-
-    def __getattr__(self, attr):
-        if attr.startswith('__'):
-            raise AttributeError(self, attr)
-        return getattr(self._iterator, attr)
-
-
-class ResponseEncoder:
-
-    default_encoding = 'utf-8'
-    failmsg = 'Response body could not be encoded with %r.'
-    encoding = None
-    errors = 'strict'
-    text_only = True
-    add_charset = True
-    debug = False
-
-    def __init__(self, **kwargs):
-        for k, v in kwargs.items():
-            setattr(self, k, v)
-
-        self.attempted_charsets = set()
-        request = cherrypy.serving.request
-        if request.handler is not None:
-            # Replace request.handler with self
-            if self.debug:
-                cherrypy.log('Replacing request.handler', 'TOOLS.ENCODE')
-            self.oldhandler = request.handler
-            request.handler = self
-
-    def encode_stream(self, encoding):
-        """Encode a streaming response body.
-
-        Use a generator wrapper, and just pray it works as the stream is
-        being written out.
-        """
-        if encoding in self.attempted_charsets:
-            return False
-        self.attempted_charsets.add(encoding)
-
-        def encoder(body):
-            for chunk in body:
-                if isinstance(chunk, six.text_type):
-                    chunk = chunk.encode(encoding, self.errors)
-                yield chunk
-        self.body = encoder(self.body)
-        return True
-
-    def encode_string(self, encoding):
-        """Encode a buffered response body."""
-        if encoding in self.attempted_charsets:
-            return False
-        self.attempted_charsets.add(encoding)
-        body = []
-        for chunk in self.body:
-            if isinstance(chunk, six.text_type):
-                try:
-                    chunk = chunk.encode(encoding, self.errors)
-                except (LookupError, UnicodeError):
-                    return False
-            body.append(chunk)
-        self.body = body
-        return True
-
-    def find_acceptable_charset(self):
-        request = cherrypy.serving.request
-        response = cherrypy.serving.response
-
-        if self.debug:
-            cherrypy.log('response.stream %r' %
-                         response.stream, 'TOOLS.ENCODE')
-        if response.stream:
-            encoder = self.encode_stream
-        else:
-            encoder = self.encode_string
-            if 'Content-Length' in response.headers:
-                # Delete Content-Length header so finalize() recalcs it.
-                # Encoded strings may be of different lengths from their
-                # unicode equivalents, and even from each other. For example:
-                # >>> t = u"\u7007\u3040"
-                # >>> len(t)
-                # 2
-                # >>> len(t.encode("UTF-8"))
-                # 6
-                # >>> len(t.encode("utf7"))
-                # 8
-                del response.headers['Content-Length']
-
-        # Parse the Accept-Charset request header, and try to provide one
-        # of the requested charsets (in order of user preference).
-        encs = request.headers.elements('Accept-Charset')
-        charsets = [enc.value.lower() for enc in encs]
-        if self.debug:
-            cherrypy.log('charsets %s' % repr(charsets), 'TOOLS.ENCODE')
-
-        if self.encoding is not None:
-            # If specified, force this encoding to be used, or fail.
-            encoding = self.encoding.lower()
-            if self.debug:
-                cherrypy.log('Specified encoding %r' %
-                             encoding, 'TOOLS.ENCODE')
-            if (not charsets) or '*' in charsets or encoding in charsets:
-                if self.debug:
-                    cherrypy.log('Attempting encoding %r' %
-                                 encoding, 'TOOLS.ENCODE')
-                if encoder(encoding):
-                    return encoding
-        else:
-            if not encs:
-                if self.debug:
-                    cherrypy.log('Attempting default encoding %r' %
-                                 self.default_encoding, 'TOOLS.ENCODE')
-                # Any character-set is acceptable.
-                if encoder(self.default_encoding):
-                    return self.default_encoding
-                else:
-                    raise cherrypy.HTTPError(500, self.failmsg %
-                                             self.default_encoding)
-            else:
-                for element in encs:
-                    if element.qvalue > 0:
-                        if element.value == '*':
-                            # Matches any charset. Try our default.
-                            if self.debug:
-                                cherrypy.log('Attempting default encoding due '
-                                             'to %r' % element, 'TOOLS.ENCODE')
-                            if encoder(self.default_encoding):
-                                return self.default_encoding
-                        else:
-                            encoding = element.value
-                            if self.debug:
-                                cherrypy.log('Attempting encoding %s (qvalue >'
-                                             '0)' % element, 'TOOLS.ENCODE')
-                            if encoder(encoding):
-                                return encoding
-
-                if '*' not in charsets:
-                    # If no "*" is present in an Accept-Charset field, then all
-                    # character sets not explicitly mentioned get a quality
-                    # value of 0, except for ISO-8859-1, which gets a quality
-                    # value of 1 if not explicitly mentioned.
-                    iso = 'iso-8859-1'
-                    if iso not in charsets:
-                        if self.debug:
-                            cherrypy.log('Attempting ISO-8859-1 encoding',
-                                         'TOOLS.ENCODE')
-                        if encoder(iso):
-                            return iso
-
-        # No suitable encoding found.
-        ac = request.headers.get('Accept-Charset')
-        if ac is None:
-            msg = 'Your client did not send an Accept-Charset header.'
-        else:
-            msg = 'Your client sent this Accept-Charset header: %s.' % ac
-        _charsets = ', '.join(sorted(self.attempted_charsets))
-        msg += ' We tried these charsets: %s.' % (_charsets,)
-        raise cherrypy.HTTPError(406, msg)
-
-    def __call__(self, *args, **kwargs):
-        response = cherrypy.serving.response
-        self.body = self.oldhandler(*args, **kwargs)
-
-        self.body = prepare_iter(self.body)
-
-        ct = response.headers.elements('Content-Type')
-        if self.debug:
-            cherrypy.log('Content-Type: %r' % [str(h)
-                         for h in ct], 'TOOLS.ENCODE')
-        if ct and self.add_charset:
-            ct = ct[0]
-            if self.text_only:
-                if ct.value.lower().startswith('text/'):
-                    if self.debug:
-                        cherrypy.log(
-                            'Content-Type %s starts with "text/"' % ct,
-                            'TOOLS.ENCODE')
-                    do_find = True
-                else:
-                    if self.debug:
-                        cherrypy.log('Not finding because Content-Type %s '
-                                     'does not start with "text/"' % ct,
-                                     'TOOLS.ENCODE')
-                    do_find = False
-            else:
-                if self.debug:
-                    cherrypy.log('Finding because not text_only',
-                                 'TOOLS.ENCODE')
-                do_find = True
-
-            if do_find:
-                # Set "charset=..." param on response Content-Type header
-                ct.params['charset'] = self.find_acceptable_charset()
-                if self.debug:
-                    cherrypy.log('Setting Content-Type %s' % ct,
-                                 'TOOLS.ENCODE')
-                response.headers['Content-Type'] = str(ct)
-
-        return self.body
-
-
-def prepare_iter(value):
-    """
-    Ensure response body is iterable and resolves to False when empty.
-    """
-    if isinstance(value, text_or_bytes):
-        # strings get wrapped in a list because iterating over a single
-        # item list is much faster than iterating over every character
-        # in a long string.
-        if value:
-            value = [value]
-        else:
-            # [''] doesn't evaluate to False, so replace it with [].
-            value = []
-    # Don't use isinstance here; io.IOBase which has an ABC takes
-    # 1000 times as long as, say, isinstance(value, str)
-    elif hasattr(value, 'read'):
-        value = file_generator(value)
-    elif value is None:
-        value = []
-    return value
-
-
-# GZIP
-
-
-def compress(body, compress_level):
-    """Compress 'body' at the given compress_level."""
-    import zlib
-
-    # See http://www.gzip.org/zlib/rfc-gzip.html
-    yield b'\x1f\x8b'       # ID1 and ID2: gzip marker
-    yield b'\x08'           # CM: compression method
-    yield b'\x00'           # FLG: none set
-    # MTIME: 4 bytes
-    yield struct.pack('<L', int(time.time()) & int('FFFFFFFF', 16))
-    yield b'\x02'           # XFL: max compression, slowest algo
-    yield b'\xff'           # OS: unknown
-
-    crc = zlib.crc32(b'')
-    size = 0
-    zobj = zlib.compressobj(compress_level,
-                            zlib.DEFLATED, -zlib.MAX_WBITS,
-                            zlib.DEF_MEM_LEVEL, 0)
-    for line in body:
-        size += len(line)
-        crc = zlib.crc32(line, crc)
-        yield zobj.compress(line)
-    yield zobj.flush()
-
-    # CRC32: 4 bytes
-    yield struct.pack('<L', crc & int('FFFFFFFF', 16))
-    # ISIZE: 4 bytes
-    yield struct.pack('<L', size & int('FFFFFFFF', 16))
-
-
-def decompress(body):
-    import gzip
-
-    zbuf = io.BytesIO()
-    zbuf.write(body)
-    zbuf.seek(0)
-    zfile = gzip.GzipFile(mode='rb', fileobj=zbuf)
-    data = zfile.read()
-    zfile.close()
-    return data
-
-
-def gzip(compress_level=5, mime_types=['text/html', 'text/plain'],
-         debug=False):
-    """Try to gzip the response body if Content-Type in mime_types.
-
-    cherrypy.response.headers['Content-Type'] must be set to one of the
-    values in the mime_types arg before calling this function.
-
-    The provided list of mime-types must be of one of the following form:
-        * `type/subtype`
-        * `type/*`
-        * `type/*+subtype`
-
-    No compression is performed if any of the following hold:
-        * The client sends no Accept-Encoding request header
-        * No 'gzip' or 'x-gzip' is present in the Accept-Encoding header
-        * No 'gzip' or 'x-gzip' with a qvalue > 0 is present
-        * The 'identity' value is given with a qvalue > 0.
-
-    """
-    request = cherrypy.serving.request
-    response = cherrypy.serving.response
-
-    set_vary_header(response, 'Accept-Encoding')
-
-    if not response.body:
-        # Response body is empty (might be a 304 for instance)
-        if debug:
-            cherrypy.log('No response body', context='TOOLS.GZIP')
-        return
-
-    # If returning cached content (which should already have been gzipped),
-    # don't re-zip.
-    if getattr(request, 'cached', False):
-        if debug:
-            cherrypy.log('Not gzipping cached response', context='TOOLS.GZIP')
-        return
-
-    acceptable = request.headers.elements('Accept-Encoding')
-    if not acceptable:
-        # If no Accept-Encoding field is present in a request,
-        # the server MAY assume that the client will accept any
-        # content coding. In this case, if "identity" is one of
-        # the available content-codings, then the server SHOULD use
-        # the "identity" content-coding, unless it has additional
-        # information that a different content-coding is meaningful
-        # to the client.
-        if debug:
-            cherrypy.log('No Accept-Encoding', context='TOOLS.GZIP')
-        return
-
-    ct = response.headers.get('Content-Type', '').split(';')[0]
-    for coding in acceptable:
-        if coding.value == 'identity' and coding.qvalue != 0:
-            if debug:
-                cherrypy.log('Non-zero identity qvalue: %s' % coding,
-                             context='TOOLS.GZIP')
-            return
-        if coding.value in ('gzip', 'x-gzip'):
-            if coding.qvalue == 0:
-                if debug:
-                    cherrypy.log('Zero gzip qvalue: %s' % coding,
-                                 context='TOOLS.GZIP')
-                return
-
-            if ct not in mime_types:
-                # If the list of provided mime-types contains tokens
-                # such as 'text/*' or 'application/*+xml',
-                # we go through them and find the most appropriate one
-                # based on the given content-type.
-                # The pattern matching is only caring about the most
-                # common cases, as stated above, and doesn't support
-                # for extra parameters.
-                found = False
-                if '/' in ct:
-                    ct_media_type, ct_sub_type = ct.split('/')
-                    for mime_type in mime_types:
-                        if '/' in mime_type:
-                            media_type, sub_type = mime_type.split('/')
-                            if ct_media_type == media_type:
-                                if sub_type == '*':
-                                    found = True
-                                    break
-                                elif '+' in sub_type and '+' in ct_sub_type:
-                                    ct_left, ct_right = ct_sub_type.split('+')
-                                    left, right = sub_type.split('+')
-                                    if left == '*' and ct_right == right:
-                                        found = True
-                                        break
-
-                if not found:
-                    if debug:
-                        cherrypy.log('Content-Type %s not in mime_types %r' %
-                                     (ct, mime_types), context='TOOLS.GZIP')
-                    return
-
-            if debug:
-                cherrypy.log('Gzipping', context='TOOLS.GZIP')
-            # Return a generator that compresses the page
-            response.headers['Content-Encoding'] = 'gzip'
-            response.body = compress(response.body, compress_level)
-            if 'Content-Length' in response.headers:
-                # Delete Content-Length header so finalize() recalcs it.
-                del response.headers['Content-Length']
-
-            return
-
-    if debug:
-        cherrypy.log('No acceptable encoding found.', context='GZIP')
-    cherrypy.HTTPError(406, 'identity, gzip').set_response()
diff --git a/libraries/cherrypy/lib/gctools.py b/libraries/cherrypy/lib/gctools.py
deleted file mode 100644
index 26746d78..00000000
--- a/libraries/cherrypy/lib/gctools.py
+++ /dev/null
@@ -1,218 +0,0 @@
-import gc
-import inspect
-import sys
-import time
-
-try:
-    import objgraph
-except ImportError:
-    objgraph = None
-
-import cherrypy
-from cherrypy import _cprequest, _cpwsgi
-from cherrypy.process.plugins import SimplePlugin
-
-
-class ReferrerTree(object):
-
-    """An object which gathers all referrers of an object to a given depth."""
-
-    peek_length = 40
-
-    def __init__(self, ignore=None, maxdepth=2, maxparents=10):
-        self.ignore = ignore or []
-        self.ignore.append(inspect.currentframe().f_back)
-        self.maxdepth = maxdepth
-        self.maxparents = maxparents
-
-    def ascend(self, obj, depth=1):
-        """Return a nested list containing referrers of the given object."""
-        depth += 1
-        parents = []
-
-        # Gather all referrers in one step to minimize
-        # cascading references due to repr() logic.
-        refs = gc.get_referrers(obj)
-        self.ignore.append(refs)
-        if len(refs) > self.maxparents:
-            return [('[%s referrers]' % len(refs), [])]
-
-        try:
-            ascendcode = self.ascend.__code__
-        except AttributeError:
-            ascendcode = self.ascend.im_func.func_code
-        for parent in refs:
-            if inspect.isframe(parent) and parent.f_code is ascendcode:
-                continue
-            if parent in self.ignore:
-                continue
-            if depth <= self.maxdepth:
-                parents.append((parent, self.ascend(parent, depth)))
-            else:
-                parents.append((parent, []))
-
-        return parents
-
-    def peek(self, s):
-        """Return s, restricted to a sane length."""
-        if len(s) > (self.peek_length + 3):
-            half = self.peek_length // 2
-            return s[:half] + '...' + s[-half:]
-        else:
-            return s
-
-    def _format(self, obj, descend=True):
-        """Return a string representation of a single object."""
-        if inspect.isframe(obj):
-            filename, lineno, func, context, index = inspect.getframeinfo(obj)
-            return "<frame of function '%s'>" % func
-
-        if not descend:
-            return self.peek(repr(obj))
-
-        if isinstance(obj, dict):
-            return '{' + ', '.join(['%s: %s' % (self._format(k, descend=False),
-                                                self._format(v, descend=False))
-                                    for k, v in obj.items()]) + '}'
-        elif isinstance(obj, list):
-            return '[' + ', '.join([self._format(item, descend=False)
-                                    for item in obj]) + ']'
-        elif isinstance(obj, tuple):
-            return '(' + ', '.join([self._format(item, descend=False)
-                                    for item in obj]) + ')'
-
-        r = self.peek(repr(obj))
-        if isinstance(obj, (str, int, float)):
-            return r
-        return '%s: %s' % (type(obj), r)
-
-    def format(self, tree):
-        """Return a list of string reprs from a nested list of referrers."""
-        output = []
-
-        def ascend(branch, depth=1):
-            for parent, grandparents in branch:
-                output.append(('    ' * depth) + self._format(parent))
-                if grandparents:
-                    ascend(grandparents, depth + 1)
-        ascend(tree)
-        return output
-
-
-def get_instances(cls):
-    return [x for x in gc.get_objects() if isinstance(x, cls)]
-
-
-class RequestCounter(SimplePlugin):
-
-    def start(self):
-        self.count = 0
-
-    def before_request(self):
-        self.count += 1
-
-    def after_request(self):
-        self.count -= 1
-
-
-request_counter = RequestCounter(cherrypy.engine)
-request_counter.subscribe()
-
-
-def get_context(obj):
-    if isinstance(obj, _cprequest.Request):
-        return 'path=%s;stage=%s' % (obj.path_info, obj.stage)
-    elif isinstance(obj, _cprequest.Response):
-        return 'status=%s' % obj.status
-    elif isinstance(obj, _cpwsgi.AppResponse):
-        return 'PATH_INFO=%s' % obj.environ.get('PATH_INFO', '')
-    elif hasattr(obj, 'tb_lineno'):
-        return 'tb_lineno=%s' % obj.tb_lineno
-    return ''
-
-
-class GCRoot(object):
-
-    """A CherryPy page handler for testing reference leaks."""
-
-    classes = [
-        (_cprequest.Request, 2, 2,
-         'Should be 1 in this request thread and 1 in the main thread.'),
-        (_cprequest.Response, 2, 2,
-         'Should be 1 in this request thread and 1 in the main thread.'),
-        (_cpwsgi.AppResponse, 1, 1,
-         'Should be 1 in this request thread only.'),
-    ]
-
-    @cherrypy.expose
-    def index(self):
-        return 'Hello, world!'
-
-    @cherrypy.expose
-    def stats(self):
-        output = ['Statistics:']
-
-        for trial in range(10):
-            if request_counter.count > 0:
-                break
-            time.sleep(0.5)
-        else:
-            output.append('\nNot all requests closed properly.')
-
-        # gc_collect isn't perfectly synchronous, because it may
-        # break reference cycles that then take time to fully
-        # finalize. Call it thrice and hope for the best.
-        gc.collect()
-        gc.collect()
-        unreachable = gc.collect()
-        if unreachable:
-            if objgraph is not None:
-                final = objgraph.by_type('Nondestructible')
-                if final:
-                    objgraph.show_backrefs(final, filename='finalizers.png')
-
-            trash = {}
-            for x in gc.garbage:
-                trash[type(x)] = trash.get(type(x), 0) + 1
-            if trash:
-                output.insert(0, '\n%s unreachable objects:' % unreachable)
-                trash = [(v, k) for k, v in trash.items()]
-                trash.sort()
-                for pair in trash:
-                    output.append('    ' + repr(pair))
-
-        # Check declared classes to verify uncollected instances.
-        # These don't have to be part of a cycle; they can be
-        # any objects that have unanticipated referrers that keep
-        # them from being collected.
-        allobjs = {}
-        for cls, minobj, maxobj, msg in self.classes:
-            allobjs[cls] = get_instances(cls)
-
-        for cls, minobj, maxobj, msg in self.classes:
-            objs = allobjs[cls]
-            lenobj = len(objs)
-            if lenobj < minobj or lenobj > maxobj:
-                if minobj == maxobj:
-                    output.append(
-                        '\nExpected %s %r references, got %s.' %
-                        (minobj, cls, lenobj))
-                else:
-                    output.append(
-                        '\nExpected %s to %s %r references, got %s.' %
-                        (minobj, maxobj, cls, lenobj))
-
-                for obj in objs:
-                    if objgraph is not None:
-                        ig = [id(objs), id(inspect.currentframe())]
-                        fname = 'graph_%s_%s.png' % (cls.__name__, id(obj))
-                        objgraph.show_backrefs(
-                            obj, extra_ignore=ig, max_depth=4, too_many=20,
-                            filename=fname, extra_info=get_context)
-                    output.append('\nReferrers for %s (refcount=%s):' %
-                                  (repr(obj), sys.getrefcount(obj)))
-                    t = ReferrerTree(ignore=[objs], maxdepth=3)
-                    tree = t.ascend(obj)
-                    output.extend(t.format(tree))
-
-        return '\n'.join(output)
diff --git a/libraries/cherrypy/lib/httputil.py b/libraries/cherrypy/lib/httputil.py
deleted file mode 100644
index b68d8dd5..00000000
--- a/libraries/cherrypy/lib/httputil.py
+++ /dev/null
@@ -1,581 +0,0 @@
-"""HTTP library functions.
-
-This module contains functions for building an HTTP application
-framework: any one, not just one whose name starts with "Ch". ;) If you
-reference any modules from some popular framework inside *this* module,
-FuManChu will personally hang you up by your thumbs and submit you
-to a public caning.
-"""
-
-import functools
-import email.utils
-import re
-from binascii import b2a_base64
-from cgi import parse_header
-from email.header import decode_header
-
-import six
-from six.moves import range, builtins, map
-from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
-
-import cherrypy
-from cherrypy._cpcompat import ntob, ntou
-from cherrypy._cpcompat import unquote_plus
-
-response_codes = BaseHTTPRequestHandler.responses.copy()
-
-# From https://github.com/cherrypy/cherrypy/issues/361
-response_codes[500] = ('Internal Server Error',
-                       'The server encountered an unexpected condition '
-                       'which prevented it from fulfilling the request.')
-response_codes[503] = ('Service Unavailable',
-                       'The server is currently unable to handle the '
-                       'request due to a temporary overloading or '
-                       'maintenance of the server.')
-
-
-HTTPDate = functools.partial(email.utils.formatdate, usegmt=True)
-
-
-def urljoin(*atoms):
-    r"""Return the given path \*atoms, joined into a single URL.
-
-    This will correctly join a SCRIPT_NAME and PATH_INFO into the
-    original URL, even if either atom is blank.
-    """
-    url = '/'.join([x for x in atoms if x])
-    while '//' in url:
-        url = url.replace('//', '/')
-    # Special-case the final url of "", and return "/" instead.
-    return url or '/'
-
-
-def urljoin_bytes(*atoms):
-    """Return the given path `*atoms`, joined into a single URL.
-
-    This will correctly join a SCRIPT_NAME and PATH_INFO into the
-    original URL, even if either atom is blank.
-    """
-    url = b'/'.join([x for x in atoms if x])
-    while b'//' in url:
-        url = url.replace(b'//', b'/')
-    # Special-case the final url of "", and return "/" instead.
-    return url or b'/'
-
-
-def protocol_from_http(protocol_str):
-    """Return a protocol tuple from the given 'HTTP/x.y' string."""
-    return int(protocol_str[5]), int(protocol_str[7])
-
-
-def get_ranges(headervalue, content_length):
-    """Return a list of (start, stop) indices from a Range header, or None.
-
-    Each (start, stop) tuple will be composed of two ints, which are suitable
-    for use in a slicing operation. That is, the header "Range: bytes=3-6",
-    if applied against a Python string, is requesting resource[3:7]. This
-    function will return the list [(3, 7)].
-
-    If this function returns an empty list, you should return HTTP 416.
-    """
-
-    if not headervalue:
-        return None
-
-    result = []
-    bytesunit, byteranges = headervalue.split('=', 1)
-    for brange in byteranges.split(','):
-        start, stop = [x.strip() for x in brange.split('-', 1)]
-        if start:
-            if not stop:
-                stop = content_length - 1
-            start, stop = int(start), int(stop)
-            if start >= content_length:
-                # From rfc 2616 sec 14.16:
-                # "If the server receives a request (other than one
-                # including an If-Range request-header field) with an
-                # unsatisfiable Range request-header field (that is,
-                # all of whose byte-range-spec values have a first-byte-pos
-                # value greater than the current length of the selected
-                # resource), it SHOULD return a response code of 416
-                # (Requested range not satisfiable)."
-                continue
-            if stop < start:
-                # From rfc 2616 sec 14.16:
-                # "If the server ignores a byte-range-spec because it
-                # is syntactically invalid, the server SHOULD treat
-                # the request as if the invalid Range header field
-                # did not exist. (Normally, this means return a 200
-                # response containing the full entity)."
-                return None
-            result.append((start, stop + 1))
-        else:
-            if not stop:
-                # See rfc quote above.
-                return None
-            # Negative subscript (last N bytes)
-            #
-            # RFC 2616 Section 14.35.1:
-            #   If the entity is shorter than the specified suffix-length,
-            #   the entire entity-body is used.
-            if int(stop) > content_length:
-                result.append((0, content_length))
-            else:
-                result.append((content_length - int(stop), content_length))
-
-    return result
-
-
-class HeaderElement(object):
-
-    """An element (with parameters) from an HTTP header's element list."""
-
-    def __init__(self, value, params=None):
-        self.value = value
-        if params is None:
-            params = {}
-        self.params = params
-
-    def __cmp__(self, other):
-        return builtins.cmp(self.value, other.value)
-
-    def __lt__(self, other):
-        return self.value < other.value
-
-    def __str__(self):
-        p = [';%s=%s' % (k, v) for k, v in six.iteritems(self.params)]
-        return str('%s%s' % (self.value, ''.join(p)))
-
-    def __bytes__(self):
-        return ntob(self.__str__())
-
-    def __unicode__(self):
-        return ntou(self.__str__())
-
-    @staticmethod
-    def parse(elementstr):
-        """Transform 'token;key=val' to ('token', {'key': 'val'})."""
-        initial_value, params = parse_header(elementstr)
-        return initial_value, params
-
-    @classmethod
-    def from_str(cls, elementstr):
-        """Construct an instance from a string of the form 'token;key=val'."""
-        ival, params = cls.parse(elementstr)
-        return cls(ival, params)
-
-
-q_separator = re.compile(r'; *q *=')
-
-
-class AcceptElement(HeaderElement):
-
-    """An element (with parameters) from an Accept* header's element list.
-
-    AcceptElement objects are comparable; the more-preferred object will be
-    "less than" the less-preferred object. They are also therefore sortable;
-    if you sort a list of AcceptElement objects, they will be listed in
-    priority order; the most preferred value will be first. Yes, it should
-    have been the other way around, but it's too late to fix now.
-    """
-
-    @classmethod
-    def from_str(cls, elementstr):
-        qvalue = None
-        # The first "q" parameter (if any) separates the initial
-        # media-range parameter(s) (if any) from the accept-params.
-        atoms = q_separator.split(elementstr, 1)
-        media_range = atoms.pop(0).strip()
-        if atoms:
-            # The qvalue for an Accept header can have extensions. The other
-            # headers cannot, but it's easier to parse them as if they did.
-            qvalue = HeaderElement.from_str(atoms[0].strip())
-
-        media_type, params = cls.parse(media_range)
-        if qvalue is not None:
-            params['q'] = qvalue
-        return cls(media_type, params)
-
-    @property
-    def qvalue(self):
-        'The qvalue, or priority, of this value.'
-        val = self.params.get('q', '1')
-        if isinstance(val, HeaderElement):
-            val = val.value
-        try:
-            return float(val)
-        except ValueError as val_err:
-            """Fail client requests with invalid quality value.
-
-            Ref: https://github.com/cherrypy/cherrypy/issues/1370
-            """
-            six.raise_from(
-                cherrypy.HTTPError(
-                    400,
-                    'Malformed HTTP header: `{}`'.
-                    format(str(self)),
-                ),
-                val_err,
-            )
-
-    def __cmp__(self, other):
-        diff = builtins.cmp(self.qvalue, other.qvalue)
-        if diff == 0:
-            diff = builtins.cmp(str(self), str(other))
-        return diff
-
-    def __lt__(self, other):
-        if self.qvalue == other.qvalue:
-            return str(self) < str(other)
-        else:
-            return self.qvalue < other.qvalue
-
-
-RE_HEADER_SPLIT = re.compile(',(?=(?:[^"]*"[^"]*")*[^"]*$)')
-
-
-def header_elements(fieldname, fieldvalue):
-    """Return a sorted HeaderElement list from a comma-separated header string.
-    """
-    if not fieldvalue:
-        return []
-
-    result = []
-    for element in RE_HEADER_SPLIT.split(fieldvalue):
-        if fieldname.startswith('Accept') or fieldname == 'TE':
-            hv = AcceptElement.from_str(element)
-        else:
-            hv = HeaderElement.from_str(element)
-        result.append(hv)
-
-    return list(reversed(sorted(result)))
-
-
-def decode_TEXT(value):
-    r"""
-    Decode :rfc:`2047` TEXT
-
-    >>> decode_TEXT("=?utf-8?q?f=C3=BCr?=") == b'f\xfcr'.decode('latin-1')
-    True
-    """
-    atoms = decode_header(value)
-    decodedvalue = ''
-    for atom, charset in atoms:
-        if charset is not None:
-            atom = atom.decode(charset)
-        decodedvalue += atom
-    return decodedvalue
-
-
-def decode_TEXT_maybe(value):
-    """
-    Decode the text but only if '=?' appears in it.
-    """
-    return decode_TEXT(value) if '=?' in value else value
-
-
-def valid_status(status):
-    """Return legal HTTP status Code, Reason-phrase and Message.
-
-    The status arg must be an int, a str that begins with an int
-    or the constant from ``http.client`` stdlib module.
-
-    If status has no reason-phrase is supplied, a default reason-
-    phrase will be provided.
-
-    >>> from six.moves import http_client
-    >>> from six.moves.BaseHTTPServer import BaseHTTPRequestHandler
-    >>> valid_status(http_client.ACCEPTED) == (
-    ...     int(http_client.ACCEPTED),
-    ... ) + BaseHTTPRequestHandler.responses[http_client.ACCEPTED]
-    True
-    """
-
-    if not status:
-        status = 200
-
-    code, reason = status, None
-    if isinstance(status, six.string_types):
-        code, _, reason = status.partition(' ')
-        reason = reason.strip() or None
-
-    try:
-        code = int(code)
-    except (TypeError, ValueError):
-        raise ValueError('Illegal response status from server '
-                         '(%s is non-numeric).' % repr(code))
-
-    if code < 100 or code > 599:
-        raise ValueError('Illegal response status from server '
-                         '(%s is out of range).' % repr(code))
-
-    if code not in response_codes:
-        # code is unknown but not illegal
-        default_reason, message = '', ''
-    else:
-        default_reason, message = response_codes[code]
-
-    if reason is None:
-        reason = default_reason
-
-    return code, reason, message
-
-
-# NOTE: the parse_qs functions that follow are modified version of those
-# in the python3.0 source - we need to pass through an encoding to the unquote
-# method, but the default parse_qs function doesn't allow us to.  These do.
-
-def _parse_qs(qs, keep_blank_values=0, strict_parsing=0, encoding='utf-8'):
-    """Parse a query given as a string argument.
-
-    Arguments:
-
-    qs: URL-encoded query string to be parsed
-
-    keep_blank_values: flag indicating whether blank values in
-        URL encoded queries should be treated as blank strings.  A
-        true value indicates that blanks should be retained as blank
-        strings.  The default false value indicates that blank values
-        are to be ignored and treated as if they were  not included.
-
-    strict_parsing: flag indicating what to do with parsing errors. If
-        false (the default), errors are silently ignored. If true,
-        errors raise a ValueError exception.
-
-    Returns a dict, as G-d intended.
-    """
-    pairs = [s2 for s1 in qs.split('&') for s2 in s1.split(';')]
-    d = {}
-    for name_value in pairs:
-        if not name_value and not strict_parsing:
-            continue
-        nv = name_value.split('=', 1)
-        if len(nv) != 2:
-            if strict_parsing:
-                raise ValueError('bad query field: %r' % (name_value,))
-            # Handle case of a control-name with no equal sign
-            if keep_blank_values:
-                nv.append('')
-            else:
-                continue
-        if len(nv[1]) or keep_blank_values:
-            name = unquote_plus(nv[0], encoding, errors='strict')
-            value = unquote_plus(nv[1], encoding, errors='strict')
-            if name in d:
-                if not isinstance(d[name], list):
-                    d[name] = [d[name]]
-                d[name].append(value)
-            else:
-                d[name] = value
-    return d
-
-
-image_map_pattern = re.compile(r'[0-9]+,[0-9]+')
-
-
-def parse_query_string(query_string, keep_blank_values=True, encoding='utf-8'):
-    """Build a params dictionary from a query_string.
-
-    Duplicate key/value pairs in the provided query_string will be
-    returned as {'key': [val1, val2, ...]}. Single key/values will
-    be returned as strings: {'key': 'value'}.
-    """
-    if image_map_pattern.match(query_string):
-        # Server-side image map. Map the coords to 'x' and 'y'
-        # (like CGI::Request does).
-        pm = query_string.split(',')
-        pm = {'x': int(pm[0]), 'y': int(pm[1])}
-    else:
-        pm = _parse_qs(query_string, keep_blank_values, encoding=encoding)
-    return pm
-
-
-####
-# Inlined from jaraco.collections 1.5.2
-# Ref #1673
-class KeyTransformingDict(dict):
-    """
-    A dict subclass that transforms the keys before they're used.
-    Subclasses may override the default transform_key to customize behavior.
-    """
-    @staticmethod
-    def transform_key(key):
-        return key
-
-    def __init__(self, *args, **kargs):
-        super(KeyTransformingDict, self).__init__()
-        # build a dictionary using the default constructs
-        d = dict(*args, **kargs)
-        # build this dictionary using transformed keys.
-        for item in d.items():
-            self.__setitem__(*item)
-
-    def __setitem__(self, key, val):
-        key = self.transform_key(key)
-        super(KeyTransformingDict, self).__setitem__(key, val)
-
-    def __getitem__(self, key):
-        key = self.transform_key(key)
-        return super(KeyTransformingDict, self).__getitem__(key)
-
-    def __contains__(self, key):
-        key = self.transform_key(key)
-        return super(KeyTransformingDict, self).__contains__(key)
-
-    def __delitem__(self, key):
-        key = self.transform_key(key)
-        return super(KeyTransformingDict, self).__delitem__(key)
-
-    def get(self, key, *args, **kwargs):
-        key = self.transform_key(key)
-        return super(KeyTransformingDict, self).get(key, *args, **kwargs)
-
-    def setdefault(self, key, *args, **kwargs):
-        key = self.transform_key(key)
-        return super(KeyTransformingDict, self).setdefault(
-            key, *args, **kwargs)
-
-    def pop(self, key, *args, **kwargs):
-        key = self.transform_key(key)
-        return super(KeyTransformingDict, self).pop(key, *args, **kwargs)
-
-    def matching_key_for(self, key):
-        """
-        Given a key, return the actual key stored in self that matches.
-        Raise KeyError if the key isn't found.
-        """
-        try:
-            return next(e_key for e_key in self.keys() if e_key == key)
-        except StopIteration:
-            raise KeyError(key)
-####
-
-
-class CaseInsensitiveDict(KeyTransformingDict):
-
-    """A case-insensitive dict subclass.
-
-    Each key is changed on entry to str(key).title().
-    """
-
-    @staticmethod
-    def transform_key(key):
-        return str(key).title()
-
-
-#   TEXT = <any OCTET except CTLs, but including LWS>
-#
-# A CRLF is allowed in the definition of TEXT only as part of a header
-# field continuation. It is expected that the folding LWS will be
-# replaced with a single SP before interpretation of the TEXT value."
-if str == bytes:
-    header_translate_table = ''.join([chr(i) for i in range(256)])
-    header_translate_deletechars = ''.join(
-        [chr(i) for i in range(32)]) + chr(127)
-else:
-    header_translate_table = None
-    header_translate_deletechars = bytes(range(32)) + bytes([127])
-
-
-class HeaderMap(CaseInsensitiveDict):
-
-    """A dict subclass for HTTP request and response headers.
-
-    Each key is changed on entry to str(key).title(). This allows headers
-    to be case-insensitive and avoid duplicates.
-
-    Values are header values (decoded according to :rfc:`2047` if necessary).
-    """
-
-    protocol = (1, 1)
-    encodings = ['ISO-8859-1']
-
-    # Someday, when http-bis is done, this will probably get dropped
-    # since few servers, clients, or intermediaries do it. But until then,
-    # we're going to obey the spec as is.
-    # "Words of *TEXT MAY contain characters from character sets other than
-    # ISO-8859-1 only when encoded according to the rules of RFC 2047."
-    use_rfc_2047 = True
-
-    def elements(self, key):
-        """Return a sorted list of HeaderElements for the given header."""
-        key = str(key).title()
-        value = self.get(key)
-        return header_elements(key, value)
-
-    def values(self, key):
-        """Return a sorted list of HeaderElement.value for the given header."""
-        return [e.value for e in self.elements(key)]
-
-    def output(self):
-        """Transform self into a list of (name, value) tuples."""
-        return list(self.encode_header_items(self.items()))
-
-    @classmethod
-    def encode_header_items(cls, header_items):
-        """
-        Prepare the sequence of name, value tuples into a form suitable for
-        transmitting on the wire for HTTP.
-        """
-        for k, v in header_items:
-            if not isinstance(v, six.string_types):
-                v = six.text_type(v)
-
-            yield tuple(map(cls.encode_header_item, (k, v)))
-
-    @classmethod
-    def encode_header_item(cls, item):
-        if isinstance(item, six.text_type):
-            item = cls.encode(item)
-
-        # See header_translate_* constants above.
-        # Replace only if you really know what you're doing.
-        return item.translate(
-            header_translate_table, header_translate_deletechars)
-
-    @classmethod
-    def encode(cls, v):
-        """Return the given header name or value, encoded for HTTP output."""
-        for enc in cls.encodings:
-            try:
-                return v.encode(enc)
-            except UnicodeEncodeError:
-                continue
-
-        if cls.protocol == (1, 1) and cls.use_rfc_2047:
-            # Encode RFC-2047 TEXT
-            # (e.g. u"\u8200" -> "=?utf-8?b?6IiA?=").
-            # We do our own here instead of using the email module
-            # because we never want to fold lines--folding has
-            # been deprecated by the HTTP working group.
-            v = b2a_base64(v.encode('utf-8'))
-            return (b'=?utf-8?b?' + v.strip(b'\n') + b'?=')
-
-        raise ValueError('Could not encode header part %r using '
-                         'any of the encodings %r.' %
-                         (v, cls.encodings))
-
-
-class Host(object):
-
-    """An internet address.
-
-    name
-        Should be the client's host name. If not available (because no DNS
-        lookup is performed), the IP address should be used instead.
-
-    """
-
-    ip = '0.0.0.0'
-    port = 80
-    name = 'unknown.tld'
-
-    def __init__(self, ip, port, name=None):
-        self.ip = ip
-        self.port = port
-        if name is None:
-            name = ip
-        self.name = name
-
-    def __repr__(self):
-        return 'httputil.Host(%r, %r, %r)' % (self.ip, self.port, self.name)
diff --git a/libraries/cherrypy/lib/jsontools.py b/libraries/cherrypy/lib/jsontools.py
deleted file mode 100644
index 48683097..00000000
--- a/libraries/cherrypy/lib/jsontools.py
+++ /dev/null
@@ -1,88 +0,0 @@
-import cherrypy
-from cherrypy._cpcompat import text_or_bytes, ntou, json_encode, json_decode
-
-
-def json_processor(entity):
-    """Read application/json data into request.json."""
-    if not entity.headers.get(ntou('Content-Length'), ntou('')):
-        raise cherrypy.HTTPError(411)
-
-    body = entity.fp.read()
-    with cherrypy.HTTPError.handle(ValueError, 400, 'Invalid JSON document'):
-        cherrypy.serving.request.json = json_decode(body.decode('utf-8'))
-
-
-def json_in(content_type=[ntou('application/json'), ntou('text/javascript')],
-            force=True, debug=False, processor=json_processor):
-    """Add a processor to parse JSON request entities:
-    The default processor places the parsed data into request.json.
-
-    Incoming request entities which match the given content_type(s) will
-    be deserialized from JSON to the Python equivalent, and the result
-    stored at cherrypy.request.json. The 'content_type' argument may
-    be a Content-Type string or a list of allowable Content-Type strings.
-
-    If the 'force' argument is True (the default), then entities of other
-    content types will not be allowed; "415 Unsupported Media Type" is
-    raised instead.
-
-    Supply your own processor to use a custom decoder, or to handle the parsed
-    data differently.  The processor can be configured via
-    tools.json_in.processor or via the decorator method.
-
-    Note that the deserializer requires the client send a Content-Length
-    request header, or it will raise "411 Length Required". If for any
-    other reason the request entity cannot be deserialized from JSON,
-    it will raise "400 Bad Request: Invalid JSON document".
-    """
-    request = cherrypy.serving.request
-    if isinstance(content_type, text_or_bytes):
-        content_type = [content_type]
-
-    if force:
-        if debug:
-            cherrypy.log('Removing body processors %s' %
-                         repr(request.body.processors.keys()), 'TOOLS.JSON_IN')
-        request.body.processors.clear()
-        request.body.default_proc = cherrypy.HTTPError(
-            415, 'Expected an entity of content type %s' %
-            ', '.join(content_type))
-
-    for ct in content_type:
-        if debug:
-            cherrypy.log('Adding body processor for %s' % ct, 'TOOLS.JSON_IN')
-        request.body.processors[ct] = processor
-
-
-def json_handler(*args, **kwargs):
-    value = cherrypy.serving.request._json_inner_handler(*args, **kwargs)
-    return json_encode(value)
-
-
-def json_out(content_type='application/json', debug=False,
-             handler=json_handler):
-    """Wrap request.handler to serialize its output to JSON. Sets Content-Type.
-
-    If the given content_type is None, the Content-Type response header
-    is not set.
-
-    Provide your own handler to use a custom encoder.  For example
-    cherrypy.config['tools.json_out.handler'] = <function>, or
-    @json_out(handler=function).
-    """
-    request = cherrypy.serving.request
-    # request.handler may be set to None by e.g. the caching tool
-    # to signal to all components that a response body has already
-    # been attached, in which case we don't need to wrap anything.
-    if request.handler is None:
-        return
-    if debug:
-        cherrypy.log('Replacing %s with JSON handler' % request.handler,
-                     'TOOLS.JSON_OUT')
-    request._json_inner_handler = request.handler
-    request.handler = handler
-    if content_type is not None:
-        if debug:
-            cherrypy.log('Setting Content-Type to %s' %
-                         content_type, 'TOOLS.JSON_OUT')
-        cherrypy.serving.response.headers['Content-Type'] = content_type
diff --git a/libraries/cherrypy/lib/locking.py b/libraries/cherrypy/lib/locking.py
deleted file mode 100644
index 317fb58c..00000000
--- a/libraries/cherrypy/lib/locking.py
+++ /dev/null
@@ -1,47 +0,0 @@
-import datetime
-
-
-class NeverExpires(object):
-    def expired(self):
-        return False
-
-
-class Timer(object):
-    """
-    A simple timer that will indicate when an expiration time has passed.
-    """
-    def __init__(self, expiration):
-        'Create a timer that expires at `expiration` (UTC datetime)'
-        self.expiration = expiration
-
-    @classmethod
-    def after(cls, elapsed):
-        """
-        Return a timer that will expire after `elapsed` passes.
-        """
-        return cls(datetime.datetime.utcnow() + elapsed)
-
-    def expired(self):
-        return datetime.datetime.utcnow() >= self.expiration
-
-
-class LockTimeout(Exception):
-    'An exception when a lock could not be acquired before a timeout period'
-
-
-class LockChecker(object):
-    """
-    Keep track of the time and detect if a timeout has expired
-    """
-    def __init__(self, session_id, timeout):
-        self.session_id = session_id
-        if timeout:
-            self.timer = Timer.after(timeout)
-        else:
-            self.timer = NeverExpires()
-
-    def expired(self):
-        if self.timer.expired():
-            raise LockTimeout(
-                'Timeout acquiring lock for %(session_id)s' % vars(self))
-        return False
diff --git a/libraries/cherrypy/lib/profiler.py b/libraries/cherrypy/lib/profiler.py
deleted file mode 100644
index fccf2eb8..00000000
--- a/libraries/cherrypy/lib/profiler.py
+++ /dev/null
@@ -1,221 +0,0 @@
-"""Profiler tools for CherryPy.
-
-CherryPy users
-==============
-
-You can profile any of your pages as follows::
-
-    from cherrypy.lib import profiler
-
-    class Root:
-        p = profiler.Profiler("/path/to/profile/dir")
-
-        @cherrypy.expose
-        def index(self):
-            self.p.run(self._index)
-
-        def _index(self):
-            return "Hello, world!"
-
-    cherrypy.tree.mount(Root())
-
-You can also turn on profiling for all requests
-using the ``make_app`` function as WSGI middleware.
-
-CherryPy developers
-===================
-
-This module can be used whenever you make changes to CherryPy,
-to get a quick sanity-check on overall CP performance. Use the
-``--profile`` flag when running the test suite. Then, use the ``serve()``
-function to browse the results in a web browser. If you run this
-module from the command line, it will call ``serve()`` for you.
-
-"""
-
-import io
-import os
-import os.path
-import sys
-import warnings
-
-import cherrypy
-
-
-try:
-    import profile
-    import pstats
-
-    def new_func_strip_path(func_name):
-        """Make profiler output more readable by adding `__init__` modules' parents
-        """
-        filename, line, name = func_name
-        if filename.endswith('__init__.py'):
-            return (
-                os.path.basename(filename[:-12]) + filename[-12:],
-                line,
-                name,
-            )
-        return os.path.basename(filename), line, name
-
-    pstats.func_strip_path = new_func_strip_path
-except ImportError:
-    profile = None
-    pstats = None
-
-
-_count = 0
-
-
-class Profiler(object):
-
-    def __init__(self, path=None):
-        if not path:
-            path = os.path.join(os.path.dirname(__file__), 'profile')
-        self.path = path
-        if not os.path.exists(path):
-            os.makedirs(path)
-
-    def run(self, func, *args, **params):
-        """Dump profile data into self.path."""
-        global _count
-        c = _count = _count + 1
-        path = os.path.join(self.path, 'cp_%04d.prof' % c)
-        prof = profile.Profile()
-        result = prof.runcall(func, *args, **params)
-        prof.dump_stats(path)
-        return result
-
-    def statfiles(self):
-        """:rtype: list of available profiles.
-        """
-        return [f for f in os.listdir(self.path)
-                if f.startswith('cp_') and f.endswith('.prof')]
-
-    def stats(self, filename, sortby='cumulative'):
-        """:rtype stats(index): output of print_stats() for the given profile.
-        """
-        sio = io.StringIO()
-        if sys.version_info >= (2, 5):
-            s = pstats.Stats(os.path.join(self.path, filename), stream=sio)
-            s.strip_dirs()
-            s.sort_stats(sortby)
-            s.print_stats()
-        else:
-            # pstats.Stats before Python 2.5 didn't take a 'stream' arg,
-            # but just printed to stdout. So re-route stdout.
-            s = pstats.Stats(os.path.join(self.path, filename))
-            s.strip_dirs()
-            s.sort_stats(sortby)
-            oldout = sys.stdout
-            try:
-                sys.stdout = sio
-                s.print_stats()
-            finally:
-                sys.stdout = oldout
-        response = sio.getvalue()
-        sio.close()
-        return response
-
-    @cherrypy.expose
-    def index(self):
-        return """<html>
-        <head><title>CherryPy profile data</title></head>
-        <frameset cols='200, 1*'>
-            <frame src='menu' />
-            <frame name='main' src='' />
-        </frameset>
-        </html>
-        """
-
-    @cherrypy.expose
-    def menu(self):
-        yield '<h2>Profiling runs</h2>'
-        yield '<p>Click on one of the runs below to see profiling data.</p>'
-        runs = self.statfiles()
-        runs.sort()
-        for i in runs:
-            yield "<a href='report?filename=%s' target='main'>%s</a><br />" % (
-                i, i)
-
-    @cherrypy.expose
-    def report(self, filename):
-        cherrypy.response.headers['Content-Type'] = 'text/plain'
-        return self.stats(filename)
-
-
-class ProfileAggregator(Profiler):
-
-    def __init__(self, path=None):
-        Profiler.__init__(self, path)
-        global _count
-        self.count = _count = _count + 1
-        self.profiler = profile.Profile()
-
-    def run(self, func, *args, **params):
-        path = os.path.join(self.path, 'cp_%04d.prof' % self.count)
-        result = self.profiler.runcall(func, *args, **params)
-        self.profiler.dump_stats(path)
-        return result
-
-
-class make_app:
-
-    def __init__(self, nextapp, path=None, aggregate=False):
-        """Make a WSGI middleware app which wraps 'nextapp' with profiling.
-
-        nextapp
-            the WSGI application to wrap, usually an instance of
-            cherrypy.Application.
-
-        path
-            where to dump the profiling output.
-
-        aggregate
-            if True, profile data for all HTTP requests will go in
-            a single file. If False (the default), each HTTP request will
-            dump its profile data into a separate file.
-
-        """
-        if profile is None or pstats is None:
-            msg = ('Your installation of Python does not have a profile '
-                   "module. If you're on Debian, try "
-                   '`sudo apt-get install python-profiler`. '
-                   'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
-                   'for details.')
-            warnings.warn(msg)
-
-        self.nextapp = nextapp
-        self.aggregate = aggregate
-        if aggregate:
-            self.profiler = ProfileAggregator(path)
-        else:
-            self.profiler = Profiler(path)
-
-    def __call__(self, environ, start_response):
-        def gather():
-            result = []
-            for line in self.nextapp(environ, start_response):
-                result.append(line)
-            return result
-        return self.profiler.run(gather)
-
-
-def serve(path=None, port=8080):
-    if profile is None or pstats is None:
-        msg = ('Your installation of Python does not have a profile module. '
-               "If you're on Debian, try "
-               '`sudo apt-get install python-profiler`. '
-               'See http://www.cherrypy.org/wiki/ProfilingOnDebian '
-               'for details.')
-        warnings.warn(msg)
-
-    cherrypy.config.update({'server.socket_port': int(port),
-                            'server.thread_pool': 10,
-                            'environment': 'production',
-                            })
-    cherrypy.quickstart(Profiler(path))
-
-
-if __name__ == '__main__':
-    serve(*tuple(sys.argv[1:]))
diff --git a/libraries/cherrypy/lib/reprconf.py b/libraries/cherrypy/lib/reprconf.py
deleted file mode 100644
index 291ab663..00000000
--- a/libraries/cherrypy/lib/reprconf.py
+++ /dev/null
@@ -1,514 +0,0 @@
-"""Generic configuration system using unrepr.
-
-Configuration data may be supplied as a Python dictionary, as a filename,
-or as an open file object. When you supply a filename or file, Python's
-builtin ConfigParser is used (with some extensions).
-
-Namespaces
-----------
-
-Configuration keys are separated into namespaces by the first "." in the key.
-
-The only key that cannot exist in a namespace is the "environment" entry.
-This special entry 'imports' other config entries from a template stored in
-the Config.environments dict.
-
-You can define your own namespaces to be called when new config is merged
-by adding a named handler to Config.namespaces. The name can be any string,
-and the handler must be either a callable or a context manager.
-"""
-
-from cherrypy._cpcompat import text_or_bytes
-from six.moves import configparser
-from six.moves import builtins
-
-import operator
-import sys
-
-
-class NamespaceSet(dict):
-
-    """A dict of config namespace names and handlers.
-
-    Each config entry should begin with a namespace name; the corresponding
-    namespace handler will be called once for each config entry in that
-    namespace, and will be passed two arguments: the config key (with the
-    namespace removed) and the config value.
-
-    Namespace handlers may be any Python callable; they may also be
-    Python 2.5-style 'context managers', in which case their __enter__
-    method should return a callable to be used as the handler.
-    See cherrypy.tools (the Toolbox class) for an example.
-    """
-
-    def __call__(self, config):
-        """Iterate through config and pass it to each namespace handler.
-
-        config
-            A flat dict, where keys use dots to separate
-            namespaces, and values are arbitrary.
-
-        The first name in each config key is used to look up the corresponding
-        namespace handler. For example, a config entry of {'tools.gzip.on': v}
-        will call the 'tools' namespace handler with the args: ('gzip.on', v)
-        """
-        # Separate the given config into namespaces
-        ns_confs = {}
-        for k in config:
-            if '.' in k:
-                ns, name = k.split('.', 1)
-                bucket = ns_confs.setdefault(ns, {})
-                bucket[name] = config[k]
-
-        # I chose __enter__ and __exit__ so someday this could be
-        # rewritten using Python 2.5's 'with' statement:
-        # for ns, handler in six.iteritems(self):
-        #     with handler as callable:
-        #         for k, v in six.iteritems(ns_confs.get(ns, {})):
-        #             callable(k, v)
-        for ns, handler in self.items():
-            exit = getattr(handler, '__exit__', None)
-            if exit:
-                callable = handler.__enter__()
-                no_exc = True
-                try:
-                    try:
-                        for k, v in ns_confs.get(ns, {}).items():
-                            callable(k, v)
-                    except Exception:
-                        # The exceptional case is handled here
-                        no_exc = False
-                        if exit is None:
-                            raise
-                        if not exit(*sys.exc_info()):
-                            raise
-                        # The exception is swallowed if exit() returns true
-                finally:
-                    # The normal and non-local-goto cases are handled here
-                    if no_exc and exit:
-                        exit(None, None, None)
-            else:
-                for k, v in ns_confs.get(ns, {}).items():
-                    handler(k, v)
-
-    def __repr__(self):
-        return '%s.%s(%s)' % (self.__module__, self.__class__.__name__,
-                              dict.__repr__(self))
-
-    def __copy__(self):
-        newobj = self.__class__()
-        newobj.update(self)
-        return newobj
-    copy = __copy__
-
-
-class Config(dict):
-
-    """A dict-like set of configuration data, with defaults and namespaces.
-
-    May take a file, filename, or dict.
-    """
-
-    defaults = {}
-    environments = {}
-    namespaces = NamespaceSet()
-
-    def __init__(self, file=None, **kwargs):
-        self.reset()
-        if file is not None:
-            self.update(file)
-        if kwargs:
-            self.update(kwargs)
-
-    def reset(self):
-        """Reset self to default values."""
-        self.clear()
-        dict.update(self, self.defaults)
-
-    def update(self, config):
-        """Update self from a dict, file, or filename."""
-        self._apply(Parser.load(config))
-
-    def _apply(self, config):
-        """Update self from a dict."""
-        which_env = config.get('environment')
-        if which_env:
-            env = self.environments[which_env]
-            for k in env:
-                if k not in config:
-                    config[k] = env[k]
-
-        dict.update(self, config)
-        self.namespaces(config)
-
-    def __setitem__(self, k, v):
-        dict.__setitem__(self, k, v)
-        self.namespaces({k: v})
-
-
-class Parser(configparser.ConfigParser):
-
-    """Sub-class of ConfigParser that keeps the case of options and that
-    raises an exception if the file cannot be read.
-    """
-
-    def optionxform(self, optionstr):
-        return optionstr
-
-    def read(self, filenames):
-        if isinstance(filenames, text_or_bytes):
-            filenames = [filenames]
-        for filename in filenames:
-            # try:
-            #     fp = open(filename)
-            # except IOError:
-            #     continue
-            fp = open(filename)
-            try:
-                self._read(fp, filename)
-            finally:
-                fp.close()
-
-    def as_dict(self, raw=False, vars=None):
-        """Convert an INI file to a dictionary"""
-        # Load INI file into a dict
-        result = {}
-        for section in self.sections():
-            if section not in result:
-                result[section] = {}
-            for option in self.options(section):
-                value = self.get(section, option, raw=raw, vars=vars)
-                try:
-                    value = unrepr(value)
-                except Exception:
-                    x = sys.exc_info()[1]
-                    msg = ('Config error in section: %r, option: %r, '
-                           'value: %r. Config values must be valid Python.' %
-                           (section, option, value))
-                    raise ValueError(msg, x.__class__.__name__, x.args)
-                result[section][option] = value
-        return result
-
-    def dict_from_file(self, file):
-        if hasattr(file, 'read'):
-            self.readfp(file)
-        else:
-            self.read(file)
-        return self.as_dict()
-
-    @classmethod
-    def load(self, input):
-        """Resolve 'input' to dict from a dict, file, or filename."""
-        is_file = (
-            # Filename
-            isinstance(input, text_or_bytes)
-            # Open file object
-            or hasattr(input, 'read')
-        )
-        return Parser().dict_from_file(input) if is_file else input.copy()
-
-
-# public domain "unrepr" implementation, found on the web and then improved.
-
-
-class _Builder2:
-
-    def build(self, o):
-        m = getattr(self, 'build_' + o.__class__.__name__, None)
-        if m is None:
-            raise TypeError('unrepr does not recognize %s' %
-                            repr(o.__class__.__name__))
-        return m(o)
-
-    def astnode(self, s):
-        """Return a Python2 ast Node compiled from a string."""
-        try:
-            import compiler
-        except ImportError:
-            # Fallback to eval when compiler package is not available,
-            # e.g. IronPython 1.0.
-            return eval(s)
-
-        p = compiler.parse('__tempvalue__ = ' + s)
-        return p.getChildren()[1].getChildren()[0].getChildren()[1]
-
-    def build_Subscript(self, o):
-        expr, flags, subs = o.getChildren()
-        expr = self.build(expr)
-        subs = self.build(subs)
-        return expr[subs]
-
-    def build_CallFunc(self, o):
-        children = o.getChildren()
-        # Build callee from first child
-        callee = self.build(children[0])
-        # Build args and kwargs from remaining children
-        args = []
-        kwargs = {}
-        for child in children[1:]:
-            class_name = child.__class__.__name__
-            # None is ignored
-            if class_name == 'NoneType':
-                continue
-            # Keywords become kwargs
-            if class_name == 'Keyword':
-                kwargs.update(self.build(child))
-            # Everything else becomes args
-            else:
-                args.append(self.build(child))
-
-        return callee(*args, **kwargs)
-
-    def build_Keyword(self, o):
-        key, value_obj = o.getChildren()
-        value = self.build(value_obj)
-        kw_dict = {key: value}
-        return kw_dict
-
-    def build_List(self, o):
-        return map(self.build, o.getChildren())
-
-    def build_Const(self, o):
-        return o.value
-
-    def build_Dict(self, o):
-        d = {}
-        i = iter(map(self.build, o.getChildren()))
-        for el in i:
-            d[el] = i.next()
-        return d
-
-    def build_Tuple(self, o):
-        return tuple(self.build_List(o))
-
-    def build_Name(self, o):
-        name = o.name
-        if name == 'None':
-            return None
-        if name == 'True':
-            return True
-        if name == 'False':
-            return False
-
-        # See if the Name is a package or module. If it is, import it.
-        try:
-            return modules(name)
-        except ImportError:
-            pass
-
-        # See if the Name is in builtins.
-        try:
-            return getattr(builtins, name)
-        except AttributeError:
-            pass
-
-        raise TypeError('unrepr could not resolve the name %s' % repr(name))
-
-    def build_Add(self, o):
-        left, right = map(self.build, o.getChildren())
-        return left + right
-
-    def build_Mul(self, o):
-        left, right = map(self.build, o.getChildren())
-        return left * right
-
-    def build_Getattr(self, o):
-        parent = self.build(o.expr)
-        return getattr(parent, o.attrname)
-
-    def build_NoneType(self, o):
-        return None
-
-    def build_UnarySub(self, o):
-        return -self.build(o.getChildren()[0])
-
-    def build_UnaryAdd(self, o):
-        return self.build(o.getChildren()[0])
-
-
-class _Builder3:
-
-    def build(self, o):
-        m = getattr(self, 'build_' + o.__class__.__name__, None)
-        if m is None:
-            raise TypeError('unrepr does not recognize %s' %
-                            repr(o.__class__.__name__))
-        return m(o)
-
-    def astnode(self, s):
-        """Return a Python3 ast Node compiled from a string."""
-        try:
-            import ast
-        except ImportError:
-            # Fallback to eval when ast package is not available,
-            # e.g. IronPython 1.0.
-            return eval(s)
-
-        p = ast.parse('__tempvalue__ = ' + s)
-        return p.body[0].value
-
-    def build_Subscript(self, o):
-        return self.build(o.value)[self.build(o.slice)]
-
-    def build_Index(self, o):
-        return self.build(o.value)
-
-    def _build_call35(self, o):
-        """
-        Workaround for python 3.5 _ast.Call signature, docs found here
-        https://greentreesnakes.readthedocs.org/en/latest/nodes.html
-        """
-        import ast
-        callee = self.build(o.func)
-        args = []
-        if o.args is not None:
-            for a in o.args:
-                if isinstance(a, ast.Starred):
-                    args.append(self.build(a.value))
-                else:
-                    args.append(self.build(a))
-        kwargs = {}
-        for kw in o.keywords:
-            if kw.arg is None:  # double asterix `**`
-                rst = self.build(kw.value)
-                if not isinstance(rst, dict):
-                    raise TypeError('Invalid argument for call.'
-                                    'Must be a mapping object.')
-                # give preference to the keys set directly from arg=value
-                for k, v in rst.items():
-                    if k not in kwargs:
-                        kwargs[k] = v
-            else:  # defined on the call as: arg=value
-                kwargs[kw.arg] = self.build(kw.value)
-        return callee(*args, **kwargs)
-
-    def build_Call(self, o):
-        if sys.version_info >= (3, 5):
-            return self._build_call35(o)
-
-        callee = self.build(o.func)
-
-        if o.args is None:
-            args = ()
-        else:
-            args = tuple([self.build(a) for a in o.args])
-
-        if o.starargs is None:
-            starargs = ()
-        else:
-            starargs = tuple(self.build(o.starargs))
-
-        if o.kwargs is None:
-            kwargs = {}
-        else:
-            kwargs = self.build(o.kwargs)
-        if o.keywords is not None:  # direct a=b keywords
-            for kw in o.keywords:
-                # preference because is a direct keyword against **kwargs
-                kwargs[kw.arg] = self.build(kw.value)
-        return callee(*(args + starargs), **kwargs)
-
-    def build_List(self, o):
-        return list(map(self.build, o.elts))
-
-    def build_Str(self, o):
-        return o.s
-
-    def build_Num(self, o):
-        return o.n
-
-    def build_Dict(self, o):
-        return dict([(self.build(k), self.build(v))
-                     for k, v in zip(o.keys, o.values)])
-
-    def build_Tuple(self, o):
-        return tuple(self.build_List(o))
-
-    def build_Name(self, o):
-        name = o.id
-        if name == 'None':
-            return None
-        if name == 'True':
-            return True
-        if name == 'False':
-            return False
-
-        # See if the Name is a package or module. If it is, import it.
-        try:
-            return modules(name)
-        except ImportError:
-            pass
-
-        # See if the Name is in builtins.
-        try:
-            import builtins
-            return getattr(builtins, name)
-        except AttributeError:
-            pass
-
-        raise TypeError('unrepr could not resolve the name %s' % repr(name))
-
-    def build_NameConstant(self, o):
-        return o.value
-
-    def build_UnaryOp(self, o):
-        op, operand = map(self.build, [o.op, o.operand])
-        return op(operand)
-
-    def build_BinOp(self, o):
-        left, op, right = map(self.build, [o.left, o.op, o.right])
-        return op(left, right)
-
-    def build_Add(self, o):
-        return operator.add
-
-    def build_Mult(self, o):
-        return operator.mul
-
-    def build_USub(self, o):
-        return operator.neg
-
-    def build_Attribute(self, o):
-        parent = self.build(o.value)
-        return getattr(parent, o.attr)
-
-    def build_NoneType(self, o):
-        return None
-
-
-def unrepr(s):
-    """Return a Python object compiled from a string."""
-    if not s:
-        return s
-    if sys.version_info < (3, 0):
-        b = _Builder2()
-    else:
-        b = _Builder3()
-    obj = b.astnode(s)
-    return b.build(obj)
-
-
-def modules(modulePath):
-    """Load a module and retrieve a reference to that module."""
-    __import__(modulePath)
-    return sys.modules[modulePath]
-
-
-def attributes(full_attribute_name):
-    """Load a module and retrieve an attribute of that module."""
-
-    # Parse out the path, module, and attribute
-    last_dot = full_attribute_name.rfind('.')
-    attr_name = full_attribute_name[last_dot + 1:]
-    mod_path = full_attribute_name[:last_dot]
-
-    mod = modules(mod_path)
-    # Let an AttributeError propagate outward.
-    try:
-        attr = getattr(mod, attr_name)
-    except AttributeError:
-        raise AttributeError("'%s' object has no attribute '%s'"
-                             % (mod_path, attr_name))
-
-    # Return a reference to the attribute.
-    return attr
diff --git a/libraries/cherrypy/lib/sessions.py b/libraries/cherrypy/lib/sessions.py
deleted file mode 100644
index 5b49ee13..00000000
--- a/libraries/cherrypy/lib/sessions.py
+++ /dev/null
@@ -1,919 +0,0 @@
-"""Session implementation for CherryPy.
-
-You need to edit your config file to use sessions. Here's an example::
-
-    [/]
-    tools.sessions.on = True
-    tools.sessions.storage_class = cherrypy.lib.sessions.FileSession
-    tools.sessions.storage_path = "/home/site/sessions"
-    tools.sessions.timeout = 60
-
-This sets the session to be stored in files in the directory
-/home/site/sessions, and the session timeout to 60 minutes. If you omit
-``storage_class``, the sessions will be saved in RAM.
-``tools.sessions.on`` is the only required line for working sessions,
-the rest are optional.
-
-By default, the session ID is passed in a cookie, so the client's browser must
-have cookies enabled for your site.
-
-To set data for the current session, use
-``cherrypy.session['fieldname'] = 'fieldvalue'``;
-to get data use ``cherrypy.session.get('fieldname')``.
-
-================
-Locking sessions
-================
-
-By default, the ``'locking'`` mode of sessions is ``'implicit'``, which means
-the session is locked early and unlocked late. Be mindful of this default mode
-for any requests that take a long time to process (streaming responses,
-expensive calculations, database lookups, API calls, etc), as other concurrent
-requests that also utilize sessions will hang until the session is unlocked.
-
-If you want to control when the session data is locked and unlocked,
-set ``tools.sessions.locking = 'explicit'``. Then call
-``cherrypy.session.acquire_lock()`` and ``cherrypy.session.release_lock()``.
-Regardless of which mode you use, the session is guaranteed to be unlocked when
-the request is complete.
-
-=================
-Expiring Sessions
-=================
-
-You can force a session to expire with :func:`cherrypy.lib.sessions.expire`.
-Simply call that function at the point you want the session to expire, and it
-will cause the session cookie to expire client-side.
-
-===========================
-Session Fixation Protection
-===========================
-
-If CherryPy receives, via a request cookie, a session id that it does not
-recognize, it will reject that id and create a new one to return in the
-response cookie. This `helps prevent session fixation attacks
-<http://en.wikipedia.org/wiki/Session_fixation#Regenerate_SID_on_each_request>`_.
-However, CherryPy "recognizes" a session id by looking up the saved session
-data for that id. Therefore, if you never save any session data,
-**you will get a new session id for every request**.
-
-A side effect of CherryPy overwriting unrecognised session ids is that if you
-have multiple, separate CherryPy applications running on a single domain (e.g.
-on different ports), each app will overwrite the other's session id because by
-default they use the same cookie name (``"session_id"``) but do not recognise
-each others sessions. It is therefore a good idea to use a different name for
-each, for example::
-
-    [/]
-    ...
-    tools.sessions.name = "my_app_session_id"
-
-================
-Sharing Sessions
-================
-
-If you run multiple instances of CherryPy (for example via mod_python behind
-Apache prefork), you most likely cannot use the RAM session backend, since each
-instance of CherryPy will have its own memory space. Use a different backend
-instead, and verify that all instances are pointing at the same file or db
-location. Alternately, you might try a load balancer which makes sessions
-"sticky". Google is your friend, there.
-
-================
-Expiration Dates
-================
-
-The response cookie will possess an expiration date to inform the client at
-which point to stop sending the cookie back in requests. If the server time
-and client time differ, expect sessions to be unreliable. **Make sure the
-system time of your server is accurate**.
-
-CherryPy defaults to a 60-minute session timeout, which also applies to the
-cookie which is sent to the client. Unfortunately, some versions of Safari
-("4 public beta" on Windows XP at least) appear to have a bug in their parsing
-of the GMT expiration date--they appear to interpret the date as one hour in
-the past. Sixty minutes minus one hour is pretty close to zero, so you may
-experience this bug as a new session id for every request, unless the requests
-are less than one second apart. To fix, try increasing the session.timeout.
-
-On the other extreme, some users report Firefox sending cookies after their
-expiration date, although this was on a system with an inaccurate system time.
-Maybe FF doesn't trust system time.
-"""
-import sys
-import datetime
-import os
-import time
-import threading
-import binascii
-
-import six
-from six.moves import cPickle as pickle
-import contextlib2
-
-import zc.lockfile
-
-import cherrypy
-from cherrypy.lib import httputil
-from cherrypy.lib import locking
-from cherrypy.lib import is_iterator
-
-
-if six.PY2:
-    FileNotFoundError = OSError
-
-
-missing = object()
-
-
-class Session(object):
-
-    """A CherryPy dict-like Session object (one per request)."""
-
-    _id = None
-
-    id_observers = None
-    "A list of callbacks to which to pass new id's."
-
-    @property
-    def id(self):
-        """Return the current session id."""
-        return self._id
-
-    @id.setter
-    def id(self, value):
-        self._id = value
-        for o in self.id_observers:
-            o(value)
-
-    timeout = 60
-    'Number of minutes after which to delete session data.'
-
-    locked = False
-    """
-    If True, this session instance has exclusive read/write access
-    to session data."""
-
-    loaded = False
-    """
-    If True, data has been retrieved from storage. This should happen
-    automatically on the first attempt to access session data."""
-
-    clean_thread = None
-    'Class-level Monitor which calls self.clean_up.'
-
-    clean_freq = 5
-    'The poll rate for expired session cleanup in minutes.'
-
-    originalid = None
-    'The session id passed by the client. May be missing or unsafe.'
-
-    missing = False
-    'True if the session requested by the client did not exist.'
-
-    regenerated = False
-    """
-    True if the application called session.regenerate(). This is not set by
-    internal calls to regenerate the session id."""
-
-    debug = False
-    'If True, log debug information.'
-
-    # --------------------- Session management methods --------------------- #
-
-    def __init__(self, id=None, **kwargs):
-        self.id_observers = []
-        self._data = {}
-
-        for k, v in kwargs.items():
-            setattr(self, k, v)
-
-        self.originalid = id
-        self.missing = False
-        if id is None:
-            if self.debug:
-                cherrypy.log('No id given; making a new one', 'TOOLS.SESSIONS')
-            self._regenerate()
-        else:
-            self.id = id
-            if self._exists():
-                if self.debug:
-                    cherrypy.log('Set id to %s.' % id, 'TOOLS.SESSIONS')
-            else:
-                if self.debug:
-                    cherrypy.log('Expired or malicious session %r; '
-                                 'making a new one' % id, 'TOOLS.SESSIONS')
-                # Expired or malicious session. Make a new one.
-                # See https://github.com/cherrypy/cherrypy/issues/709.
-                self.id = None
-                self.missing = True
-                self._regenerate()
-
-    def now(self):
-        """Generate the session specific concept of 'now'.
-
-        Other session providers can override this to use alternative,
-        possibly timezone aware, versions of 'now'.
-        """
-        return datetime.datetime.now()
-
-    def regenerate(self):
-        """Replace the current session (with a new id)."""
-        self.regenerated = True
-        self._regenerate()
-
-    def _regenerate(self):
-        if self.id is not None:
-            if self.debug:
-                cherrypy.log(
-                    'Deleting the existing session %r before '
-                    'regeneration.' % self.id,
-                    'TOOLS.SESSIONS')
-            self.delete()
-
-        old_session_was_locked = self.locked
-        if old_session_was_locked:
-            self.release_lock()
-            if self.debug:
-                cherrypy.log('Old lock released.', 'TOOLS.SESSIONS')
-
-        self.id = None
-        while self.id is None:
-            self.id = self.generate_id()
-            # Assert that the generated id is not already stored.
-            if self._exists():
-                self.id = None
-        if self.debug:
-            cherrypy.log('Set id to generated %s.' % self.id,
-                         'TOOLS.SESSIONS')
-
-        if old_session_was_locked:
-            self.acquire_lock()
-            if self.debug:
-                cherrypy.log('Regenerated lock acquired.', 'TOOLS.SESSIONS')
-
-    def clean_up(self):
-        """Clean up expired sessions."""
-        pass
-
-    def generate_id(self):
-        """Return a new session id."""
-        return binascii.hexlify(os.urandom(20)).decode('ascii')
-
-    def save(self):
-        """Save session data."""
-        try:
-            # If session data has never been loaded then it's never been
-            #   accessed: no need to save it
-            if self.loaded:
-                t = datetime.timedelta(seconds=self.timeout * 60)
-                expiration_time = self.now() + t
-                if self.debug:
-                    cherrypy.log('Saving session %r with expiry %s' %
-                                 (self.id, expiration_time),
-                                 'TOOLS.SESSIONS')
-                self._save(expiration_time)
-            else:
-                if self.debug:
-                    cherrypy.log(
-                        'Skipping save of session %r (no session loaded).' %
-                        self.id, 'TOOLS.SESSIONS')
-        finally:
-            if self.locked:
-                # Always release the lock if the user didn't release it
-                self.release_lock()
-                if self.debug:
-                    cherrypy.log('Lock released after save.', 'TOOLS.SESSIONS')
-
-    def load(self):
-        """Copy stored session data into this session instance."""
-        data = self._load()
-        # data is either None or a tuple (session_data, expiration_time)
-        if data is None or data[1] < self.now():
-            if self.debug:
-                cherrypy.log('Expired session %r, flushing data.' % self.id,
-                             'TOOLS.SESSIONS')
-            self._data = {}
-        else:
-            if self.debug:
-                cherrypy.log('Data loaded for session %r.' % self.id,
-                             'TOOLS.SESSIONS')
-            self._data = data[0]
-        self.loaded = True
-
-        # Stick the clean_thread in the class, not the instance.
-        # The instances are created and destroyed per-request.
-        cls = self.__class__
-        if self.clean_freq and not cls.clean_thread:
-            # clean_up is an instancemethod and not a classmethod,
-            # so that tool config can be accessed inside the method.
-            t = cherrypy.process.plugins.Monitor(
-                cherrypy.engine, self.clean_up, self.clean_freq * 60,
-                name='Session cleanup')
-            t.subscribe()
-            cls.clean_thread = t
-            t.start()
-            if self.debug:
-                cherrypy.log('Started cleanup thread.', 'TOOLS.SESSIONS')
-
-    def delete(self):
-        """Delete stored session data."""
-        self._delete()
-        if self.debug:
-            cherrypy.log('Deleted session %s.' % self.id,
-                         'TOOLS.SESSIONS')
-
-    # -------------------- Application accessor methods -------------------- #
-
-    def __getitem__(self, key):
-        if not self.loaded:
-            self.load()
-        return self._data[key]
-
-    def __setitem__(self, key, value):
-        if not self.loaded:
-            self.load()
-        self._data[key] = value
-
-    def __delitem__(self, key):
-        if not self.loaded:
-            self.load()
-        del self._data[key]
-
-    def pop(self, key, default=missing):
-        """Remove the specified key and return the corresponding value.
-        If key is not found, default is returned if given,
-        otherwise KeyError is raised.
-        """
-        if not self.loaded:
-            self.load()
-        if default is missing:
-            return self._data.pop(key)
-        else:
-            return self._data.pop(key, default)
-
-    def __contains__(self, key):
-        if not self.loaded:
-            self.load()
-        return key in self._data
-
-    def get(self, key, default=None):
-        """D.get(k[,d]) -> D[k] if k in D, else d.  d defaults to None."""
-        if not self.loaded:
-            self.load()
-        return self._data.get(key, default)
-
-    def update(self, d):
-        """D.update(E) -> None.  Update D from E: for k in E: D[k] = E[k]."""
-        if not self.loaded:
-            self.load()
-        self._data.update(d)
-
-    def setdefault(self, key, default=None):
-        """D.setdefault(k[,d]) -> D.get(k,d), also set D[k]=d if k not in D."""
-        if not self.loaded:
-            self.load()
-        return self._data.setdefault(key, default)
-
-    def clear(self):
-        """D.clear() -> None.  Remove all items from D."""
-        if not self.loaded:
-            self.load()
-        self._data.clear()
-
-    def keys(self):
-        """D.keys() -> list of D's keys."""
-        if not self.loaded:
-            self.load()
-        return self._data.keys()
-
-    def items(self):
-        """D.items() -> list of D's (key, value) pairs, as 2-tuples."""
-        if not self.loaded:
-            self.load()
-        return self._data.items()
-
-    def values(self):
-        """D.values() -> list of D's values."""
-        if not self.loaded:
-            self.load()
-        return self._data.values()
-
-
-class RamSession(Session):
-
-    # Class-level objects. Don't rebind these!
-    cache = {}
-    locks = {}
-
-    def clean_up(self):
-        """Clean up expired sessions."""
-
-        now = self.now()
-        for _id, (data, expiration_time) in list(six.iteritems(self.cache)):
-            if expiration_time <= now:
-                try:
-                    del self.cache[_id]
-                except KeyError:
-                    pass
-                try:
-                    if self.locks[_id].acquire(blocking=False):
-                        lock = self.locks.pop(_id)
-                        lock.release()
-                except KeyError:
-                    pass
-
-        # added to remove obsolete lock objects
-        for _id in list(self.locks):
-            locked = (
-                _id not in self.cache
-                and self.locks[_id].acquire(blocking=False)
-            )
-            if locked:
-                lock = self.locks.pop(_id)
-                lock.release()
-
-    def _exists(self):
-        return self.id in self.cache
-
-    def _load(self):
-        return self.cache.get(self.id)
-
-    def _save(self, expiration_time):
-        self.cache[self.id] = (self._data, expiration_time)
-
-    def _delete(self):
-        self.cache.pop(self.id, None)
-
-    def acquire_lock(self):
-        """Acquire an exclusive lock on the currently-loaded session data."""
-        self.locked = True
-        self.locks.setdefault(self.id, threading.RLock()).acquire()
-
-    def release_lock(self):
-        """Release the lock on the currently-loaded session data."""
-        self.locks[self.id].release()
-        self.locked = False
-
-    def __len__(self):
-        """Return the number of active sessions."""
-        return len(self.cache)
-
-
-class FileSession(Session):
-
-    """Implementation of the File backend for sessions
-
-    storage_path
-        The folder where session data will be saved. Each session
-        will be saved as pickle.dump(data, expiration_time) in its own file;
-        the filename will be self.SESSION_PREFIX + self.id.
-
-    lock_timeout
-        A timedelta or numeric seconds indicating how long
-        to block acquiring a lock. If None (default), acquiring a lock
-        will block indefinitely.
-    """
-
-    SESSION_PREFIX = 'session-'
-    LOCK_SUFFIX = '.lock'
-    pickle_protocol = pickle.HIGHEST_PROTOCOL
-
-    def __init__(self, id=None, **kwargs):
-        # The 'storage_path' arg is required for file-based sessions.
-        kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
-        kwargs.setdefault('lock_timeout', None)
-
-        Session.__init__(self, id=id, **kwargs)
-
-        # validate self.lock_timeout
-        if isinstance(self.lock_timeout, (int, float)):
-            self.lock_timeout = datetime.timedelta(seconds=self.lock_timeout)
-        if not isinstance(self.lock_timeout, (datetime.timedelta, type(None))):
-            raise ValueError(
-                'Lock timeout must be numeric seconds or a timedelta instance.'
-            )
-
-    @classmethod
-    def setup(cls, **kwargs):
-        """Set up the storage system for file-based sessions.
-
-        This should only be called once per process; this will be done
-        automatically when using sessions.init (as the built-in Tool does).
-        """
-        # The 'storage_path' arg is required for file-based sessions.
-        kwargs['storage_path'] = os.path.abspath(kwargs['storage_path'])
-
-        for k, v in kwargs.items():
-            setattr(cls, k, v)
-
-    def _get_file_path(self):
-        f = os.path.join(self.storage_path, self.SESSION_PREFIX + self.id)
-        if not os.path.abspath(f).startswith(self.storage_path):
-            raise cherrypy.HTTPError(400, 'Invalid session id in cookie.')
-        return f
-
-    def _exists(self):
-        path = self._get_file_path()
-        return os.path.exists(path)
-
-    def _load(self, path=None):
-        assert self.locked, ('The session load without being locked.  '
-                             "Check your tools' priority levels.")
-        if path is None:
-            path = self._get_file_path()
-        try:
-            f = open(path, 'rb')
-            try:
-                return pickle.load(f)
-            finally:
-                f.close()
-        except (IOError, EOFError):
-            e = sys.exc_info()[1]
-            if self.debug:
-                cherrypy.log('Error loading the session pickle: %s' %
-                             e, 'TOOLS.SESSIONS')
-            return None
-
-    def _save(self, expiration_time):
-        assert self.locked, ('The session was saved without being locked.  '
-                             "Check your tools' priority levels.")
-        f = open(self._get_file_path(), 'wb')
-        try:
-            pickle.dump((self._data, expiration_time), f, self.pickle_protocol)
-        finally:
-            f.close()
-
-    def _delete(self):
-        assert self.locked, ('The session deletion without being locked.  '
-                             "Check your tools' priority levels.")
-        try:
-            os.unlink(self._get_file_path())
-        except OSError:
-            pass
-
-    def acquire_lock(self, path=None):
-        """Acquire an exclusive lock on the currently-loaded session data."""
-        if path is None:
-            path = self._get_file_path()
-        path += self.LOCK_SUFFIX
-        checker = locking.LockChecker(self.id, self.lock_timeout)
-        while not checker.expired():
-            try:
-                self.lock = zc.lockfile.LockFile(path)
-            except zc.lockfile.LockError:
-                time.sleep(0.1)
-            else:
-                break
-        self.locked = True
-        if self.debug:
-            cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
-
-    def release_lock(self, path=None):
-        """Release the lock on the currently-loaded session data."""
-        self.lock.close()
-        with contextlib2.suppress(FileNotFoundError):
-            os.remove(self.lock._path)
-        self.locked = False
-
-    def clean_up(self):
-        """Clean up expired sessions."""
-        now = self.now()
-        # Iterate over all session files in self.storage_path
-        for fname in os.listdir(self.storage_path):
-            have_session = (
-                fname.startswith(self.SESSION_PREFIX)
-                and not fname.endswith(self.LOCK_SUFFIX)
-            )
-            if have_session:
-                # We have a session file: lock and load it and check
-                #   if it's expired. If it fails, nevermind.
-                path = os.path.join(self.storage_path, fname)
-                self.acquire_lock(path)
-                if self.debug:
-                    # This is a bit of a hack, since we're calling clean_up
-                    # on the first instance rather than the entire class,
-                    # so depending on whether you have "debug" set on the
-                    # path of the first session called, this may not run.
-                    cherrypy.log('Cleanup lock acquired.', 'TOOLS.SESSIONS')
-
-                try:
-                    contents = self._load(path)
-                    # _load returns None on IOError
-                    if contents is not None:
-                        data, expiration_time = contents
-                        if expiration_time < now:
-                            # Session expired: deleting it
-                            os.unlink(path)
-                finally:
-                    self.release_lock(path)
-
-    def __len__(self):
-        """Return the number of active sessions."""
-        return len([fname for fname in os.listdir(self.storage_path)
-                    if (fname.startswith(self.SESSION_PREFIX) and
-                        not fname.endswith(self.LOCK_SUFFIX))])
-
-
-class MemcachedSession(Session):
-
-    # The most popular memcached client for Python isn't thread-safe.
-    # Wrap all .get and .set operations in a single lock.
-    mc_lock = threading.RLock()
-
-    # This is a separate set of locks per session id.
-    locks = {}
-
-    servers = ['127.0.0.1:11211']
-
-    @classmethod
-    def setup(cls, **kwargs):
-        """Set up the storage system for memcached-based sessions.
-
-        This should only be called once per process; this will be done
-        automatically when using sessions.init (as the built-in Tool does).
-        """
-        for k, v in kwargs.items():
-            setattr(cls, k, v)
-
-        import memcache
-        cls.cache = memcache.Client(cls.servers)
-
-    def _exists(self):
-        self.mc_lock.acquire()
-        try:
-            return bool(self.cache.get(self.id))
-        finally:
-            self.mc_lock.release()
-
-    def _load(self):
-        self.mc_lock.acquire()
-        try:
-            return self.cache.get(self.id)
-        finally:
-            self.mc_lock.release()
-
-    def _save(self, expiration_time):
-        # Send the expiration time as "Unix time" (seconds since 1/1/1970)
-        td = int(time.mktime(expiration_time.timetuple()))
-        self.mc_lock.acquire()
-        try:
-            if not self.cache.set(self.id, (self._data, expiration_time), td):
-                raise AssertionError(
-                    'Session data for id %r not set.' % self.id)
-        finally:
-            self.mc_lock.release()
-
-    def _delete(self):
-        self.cache.delete(self.id)
-
-    def acquire_lock(self):
-        """Acquire an exclusive lock on the currently-loaded session data."""
-        self.locked = True
-        self.locks.setdefault(self.id, threading.RLock()).acquire()
-        if self.debug:
-            cherrypy.log('Lock acquired.', 'TOOLS.SESSIONS')
-
-    def release_lock(self):
-        """Release the lock on the currently-loaded session data."""
-        self.locks[self.id].release()
-        self.locked = False
-
-    def __len__(self):
-        """Return the number of active sessions."""
-        raise NotImplementedError
-
-
-# Hook functions (for CherryPy tools)
-
-def save():
-    """Save any changed session data."""
-
-    if not hasattr(cherrypy.serving, 'session'):
-        return
-    request = cherrypy.serving.request
-    response = cherrypy.serving.response
-
-    # Guard against running twice
-    if hasattr(request, '_sessionsaved'):
-        return
-    request._sessionsaved = True
-
-    if response.stream:
-        # If the body is being streamed, we have to save the data
-        #   *after* the response has been written out
-        request.hooks.attach('on_end_request', cherrypy.session.save)
-    else:
-        # If the body is not being streamed, we save the data now
-        # (so we can release the lock).
-        if is_iterator(response.body):
-            response.collapse_body()
-        cherrypy.session.save()
-
-
-save.failsafe = True
-
-
-def close():
-    """Close the session object for this request."""
-    sess = getattr(cherrypy.serving, 'session', None)
-    if getattr(sess, 'locked', False):
-        # If the session is still locked we release the lock
-        sess.release_lock()
-        if sess.debug:
-            cherrypy.log('Lock released on close.', 'TOOLS.SESSIONS')
-
-
-close.failsafe = True
-close.priority = 90
-
-
-def init(storage_type=None, path=None, path_header=None, name='session_id',
-         timeout=60, domain=None, secure=False, clean_freq=5,
-         persistent=True, httponly=False, debug=False,
-         # Py27 compat
-         # *, storage_class=RamSession,
-         **kwargs):
-    """Initialize session object (using cookies).
-
-    storage_class
-        The Session subclass to use. Defaults to RamSession.
-
-    storage_type
-        (deprecated)
-        One of 'ram', 'file', memcached'. This will be
-        used to look up the corresponding class in cherrypy.lib.sessions
-        globals. For example, 'file' will use the FileSession class.
-
-    path
-        The 'path' value to stick in the response cookie metadata.
-
-    path_header
-        If 'path' is None (the default), then the response
-        cookie 'path' will be pulled from request.headers[path_header].
-
-    name
-        The name of the cookie.
-
-    timeout
-        The expiration timeout (in minutes) for the stored session data.
-        If 'persistent' is True (the default), this is also the timeout
-        for the cookie.
-
-    domain
-        The cookie domain.
-
-    secure
-        If False (the default) the cookie 'secure' value will not
-        be set. If True, the cookie 'secure' value will be set (to 1).
-
-    clean_freq (minutes)
-        The poll rate for expired session cleanup.
-
-    persistent
-        If True (the default), the 'timeout' argument will be used
-        to expire the cookie. If False, the cookie will not have an expiry,
-        and the cookie will be a "session cookie" which expires when the
-        browser is closed.
-
-    httponly
-        If False (the default) the cookie 'httponly' value will not be set.
-        If True, the cookie 'httponly' value will be set (to 1).
-
-    Any additional kwargs will be bound to the new Session instance,
-    and may be specific to the storage type. See the subclass of Session
-    you're using for more information.
-    """
-
-    # Py27 compat
-    storage_class = kwargs.pop('storage_class', RamSession)
-
-    request = cherrypy.serving.request
-
-    # Guard against running twice
-    if hasattr(request, '_session_init_flag'):
-        return
-    request._session_init_flag = True
-
-    # Check if request came with a session ID
-    id = None
-    if name in request.cookie:
-        id = request.cookie[name].value
-        if debug:
-            cherrypy.log('ID obtained from request.cookie: %r' % id,
-                         'TOOLS.SESSIONS')
-
-    first_time = not hasattr(cherrypy, 'session')
-
-    if storage_type:
-        if first_time:
-            msg = 'storage_type is deprecated. Supply storage_class instead'
-            cherrypy.log(msg)
-        storage_class = storage_type.title() + 'Session'
-        storage_class = globals()[storage_class]
-
-    # call setup first time only
-    if first_time:
-        if hasattr(storage_class, 'setup'):
-            storage_class.setup(**kwargs)
-
-    # Create and attach a new Session instance to cherrypy.serving.
-    # It will possess a reference to (and lock, and lazily load)
-    # the requested session data.
-    kwargs['timeout'] = timeout
-    kwargs['clean_freq'] = clean_freq
-    cherrypy.serving.session = sess = storage_class(id, **kwargs)
-    sess.debug = debug
-
-    def update_cookie(id):
-        """Update the cookie every time the session id changes."""
-        cherrypy.serving.response.cookie[name] = id
-    sess.id_observers.append(update_cookie)
-
-    # Create cherrypy.session which will proxy to cherrypy.serving.session
-    if not hasattr(cherrypy, 'session'):
-        cherrypy.session = cherrypy._ThreadLocalProxy('session')
-
-    if persistent:
-        cookie_timeout = timeout
-    else:
-        # See http://support.microsoft.com/kb/223799/EN-US/
-        # and http://support.mozilla.com/en-US/kb/Cookies
-        cookie_timeout = None
-    set_response_cookie(path=path, path_header=path_header, name=name,
-                        timeout=cookie_timeout, domain=domain, secure=secure,
-                        httponly=httponly)
-
-
-def set_response_cookie(path=None, path_header=None, name='session_id',
-                        timeout=60, domain=None, secure=False, httponly=False):
-    """Set a response cookie for the client.
-
-    path
-        the 'path' value to stick in the response cookie metadata.
-
-    path_header
-        if 'path' is None (the default), then the response
-        cookie 'path' will be pulled from request.headers[path_header].
-
-    name
-        the name of the cookie.
-
-    timeout
-        the expiration timeout for the cookie. If 0 or other boolean
-        False, no 'expires' param will be set, and the cookie will be a
-        "session cookie" which expires when the browser is closed.
-
-    domain
-        the cookie domain.
-
-    secure
-        if False (the default) the cookie 'secure' value will not
-        be set. If True, the cookie 'secure' value will be set (to 1).
-
-    httponly
-        If False (the default) the cookie 'httponly' value will not be set.
-        If True, the cookie 'httponly' value will be set (to 1).
-
-    """
-    # Set response cookie
-    cookie = cherrypy.serving.response.cookie
-    cookie[name] = cherrypy.serving.session.id
-    cookie[name]['path'] = (
-        path or
-        cherrypy.serving.request.headers.get(path_header) or
-        '/'
-    )
-
-    if timeout:
-        cookie[name]['max-age'] = timeout * 60
-        _add_MSIE_max_age_workaround(cookie[name], timeout)
-    if domain is not None:
-        cookie[name]['domain'] = domain
-    if secure:
-        cookie[name]['secure'] = 1
-    if httponly:
-        if not cookie[name].isReservedKey('httponly'):
-            raise ValueError('The httponly cookie token is not supported.')
-        cookie[name]['httponly'] = 1
-
-
-def _add_MSIE_max_age_workaround(cookie, timeout):
-    """
-    We'd like to use the "max-age" param as indicated in
-    http://www.faqs.org/rfcs/rfc2109.html but IE doesn't
-    save it to disk and the session is lost if people close
-    the browser. So we have to use the old "expires" ... sigh ...
-    """
-    expires = time.time() + timeout * 60
-    cookie['expires'] = httputil.HTTPDate(expires)
-
-
-def expire():
-    """Expire the current session cookie."""
-    name = cherrypy.serving.request.config.get(
-        'tools.sessions.name', 'session_id')
-    one_year = 60 * 60 * 24 * 365
-    e = time.time() - one_year
-    cherrypy.serving.response.cookie[name]['expires'] = httputil.HTTPDate(e)
-    cherrypy.serving.response.cookie[name].pop('max-age', None)
diff --git a/libraries/cherrypy/lib/static.py b/libraries/cherrypy/lib/static.py
deleted file mode 100644
index da9d9373..00000000
--- a/libraries/cherrypy/lib/static.py
+++ /dev/null
@@ -1,390 +0,0 @@
-"""Module with helpers for serving static files."""
-
-import os
-import platform
-import re
-import stat
-import mimetypes
-
-from email.generator import _make_boundary as make_boundary
-from io import UnsupportedOperation
-
-from six.moves import urllib
-
-import cherrypy
-from cherrypy._cpcompat import ntob
-from cherrypy.lib import cptools, httputil, file_generator_limited
-
-
-def _setup_mimetypes():
-    """Pre-initialize global mimetype map."""
-    if not mimetypes.inited:
-        mimetypes.init()
-    mimetypes.types_map['.dwg'] = 'image/x-dwg'
-    mimetypes.types_map['.ico'] = 'image/x-icon'
-    mimetypes.types_map['.bz2'] = 'application/x-bzip2'
-    mimetypes.types_map['.gz'] = 'application/x-gzip'
-
-
-_setup_mimetypes()
-
-
-def serve_file(path, content_type=None, disposition=None, name=None,
-               debug=False):
-    """Set status, headers, and body in order to serve the given path.
-
-    The Content-Type header will be set to the content_type arg, if provided.
-    If not provided, the Content-Type will be guessed by the file extension
-    of the 'path' argument.
-
-    If disposition is not None, the Content-Disposition header will be set
-    to "<disposition>; filename=<name>". If name is None, it will be set
-    to the basename of path. If disposition is None, no Content-Disposition
-    header will be written.
-    """
-    response = cherrypy.serving.response
-
-    # If path is relative, users should fix it by making path absolute.
-    # That is, CherryPy should not guess where the application root is.
-    # It certainly should *not* use cwd (since CP may be invoked from a
-    # variety of paths). If using tools.staticdir, you can make your relative
-    # paths become absolute by supplying a value for "tools.staticdir.root".
-    if not os.path.isabs(path):
-        msg = "'%s' is not an absolute path." % path
-        if debug:
-            cherrypy.log(msg, 'TOOLS.STATICFILE')
-        raise ValueError(msg)
-
-    try:
-        st = os.stat(path)
-    except (OSError, TypeError, ValueError):
-        # OSError when file fails to stat
-        # TypeError on Python 2 when there's a null byte
-        # ValueError on Python 3 when there's a null byte
-        if debug:
-            cherrypy.log('os.stat(%r) failed' % path, 'TOOLS.STATIC')
-        raise cherrypy.NotFound()
-
-    # Check if path is a directory.
-    if stat.S_ISDIR(st.st_mode):
-        # Let the caller deal with it as they like.
-        if debug:
-            cherrypy.log('%r is a directory' % path, 'TOOLS.STATIC')
-        raise cherrypy.NotFound()
-
-    # Set the Last-Modified response header, so that
-    # modified-since validation code can work.
-    response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
-    cptools.validate_since()
-
-    if content_type is None:
-        # Set content-type based on filename extension
-        ext = ''
-        i = path.rfind('.')
-        if i != -1:
-            ext = path[i:].lower()
-        content_type = mimetypes.types_map.get(ext, None)
-    if content_type is not None:
-        response.headers['Content-Type'] = content_type
-    if debug:
-        cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
-
-    cd = None
-    if disposition is not None:
-        if name is None:
-            name = os.path.basename(path)
-        cd = '%s; filename="%s"' % (disposition, name)
-        response.headers['Content-Disposition'] = cd
-    if debug:
-        cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
-
-    # Set Content-Length and use an iterable (file object)
-    #   this way CP won't load the whole file in memory
-    content_length = st.st_size
-    fileobj = open(path, 'rb')
-    return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
-
-
-def serve_fileobj(fileobj, content_type=None, disposition=None, name=None,
-                  debug=False):
-    """Set status, headers, and body in order to serve the given file object.
-
-    The Content-Type header will be set to the content_type arg, if provided.
-
-    If disposition is not None, the Content-Disposition header will be set
-    to "<disposition>; filename=<name>". If name is None, 'filename' will
-    not be set. If disposition is None, no Content-Disposition header will
-    be written.
-
-    CAUTION: If the request contains a 'Range' header, one or more seek()s will
-    be performed on the file object.  This may cause undesired behavior if
-    the file object is not seekable.  It could also produce undesired results
-    if the caller set the read position of the file object prior to calling
-    serve_fileobj(), expecting that the data would be served starting from that
-    position.
-    """
-    response = cherrypy.serving.response
-
-    try:
-        st = os.fstat(fileobj.fileno())
-    except AttributeError:
-        if debug:
-            cherrypy.log('os has no fstat attribute', 'TOOLS.STATIC')
-        content_length = None
-    except UnsupportedOperation:
-        content_length = None
-    else:
-        # Set the Last-Modified response header, so that
-        # modified-since validation code can work.
-        response.headers['Last-Modified'] = httputil.HTTPDate(st.st_mtime)
-        cptools.validate_since()
-        content_length = st.st_size
-
-    if content_type is not None:
-        response.headers['Content-Type'] = content_type
-    if debug:
-        cherrypy.log('Content-Type: %r' % content_type, 'TOOLS.STATIC')
-
-    cd = None
-    if disposition is not None:
-        if name is None:
-            cd = disposition
-        else:
-            cd = '%s; filename="%s"' % (disposition, name)
-        response.headers['Content-Disposition'] = cd
-    if debug:
-        cherrypy.log('Content-Disposition: %r' % cd, 'TOOLS.STATIC')
-
-    return _serve_fileobj(fileobj, content_type, content_length, debug=debug)
-
-
-def _serve_fileobj(fileobj, content_type, content_length, debug=False):
-    """Internal. Set response.body to the given file object, perhaps ranged."""
-    response = cherrypy.serving.response
-
-    # HTTP/1.0 didn't have Range/Accept-Ranges headers, or the 206 code
-    request = cherrypy.serving.request
-    if request.protocol >= (1, 1):
-        response.headers['Accept-Ranges'] = 'bytes'
-        r = httputil.get_ranges(request.headers.get('Range'), content_length)
-        if r == []:
-            response.headers['Content-Range'] = 'bytes */%s' % content_length
-            message = ('Invalid Range (first-byte-pos greater than '
-                       'Content-Length)')
-            if debug:
-                cherrypy.log(message, 'TOOLS.STATIC')
-            raise cherrypy.HTTPError(416, message)
-
-        if r:
-            if len(r) == 1:
-                # Return a single-part response.
-                start, stop = r[0]
-                if stop > content_length:
-                    stop = content_length
-                r_len = stop - start
-                if debug:
-                    cherrypy.log(
-                        'Single part; start: %r, stop: %r' % (start, stop),
-                        'TOOLS.STATIC')
-                response.status = '206 Partial Content'
-                response.headers['Content-Range'] = (
-                    'bytes %s-%s/%s' % (start, stop - 1, content_length))
-                response.headers['Content-Length'] = r_len
-                fileobj.seek(start)
-                response.body = file_generator_limited(fileobj, r_len)
-            else:
-                # Return a multipart/byteranges response.
-                response.status = '206 Partial Content'
-                boundary = make_boundary()
-                ct = 'multipart/byteranges; boundary=%s' % boundary
-                response.headers['Content-Type'] = ct
-                if 'Content-Length' in response.headers:
-                    # Delete Content-Length header so finalize() recalcs it.
-                    del response.headers['Content-Length']
-
-                def file_ranges():
-                    # Apache compatibility:
-                    yield b'\r\n'
-
-                    for start, stop in r:
-                        if debug:
-                            cherrypy.log(
-                                'Multipart; start: %r, stop: %r' % (
-                                    start, stop),
-                                'TOOLS.STATIC')
-                        yield ntob('--' + boundary, 'ascii')
-                        yield ntob('\r\nContent-type: %s' % content_type,
-                                   'ascii')
-                        yield ntob(
-                            '\r\nContent-range: bytes %s-%s/%s\r\n\r\n' % (
-                                start, stop - 1, content_length),
-                            'ascii')
-                        fileobj.seek(start)
-                        gen = file_generator_limited(fileobj, stop - start)
-                        for chunk in gen:
-                            yield chunk
-                        yield b'\r\n'
-                    # Final boundary
-                    yield ntob('--' + boundary + '--', 'ascii')
-
-                    # Apache compatibility:
-                    yield b'\r\n'
-                response.body = file_ranges()
-            return response.body
-        else:
-            if debug:
-                cherrypy.log('No byteranges requested', 'TOOLS.STATIC')
-
-    # Set Content-Length and use an iterable (file object)
-    #   this way CP won't load the whole file in memory
-    response.headers['Content-Length'] = content_length
-    response.body = fileobj
-    return response.body
-
-
-def serve_download(path, name=None):
-    """Serve 'path' as an application/x-download attachment."""
-    # This is such a common idiom I felt it deserved its own wrapper.
-    return serve_file(path, 'application/x-download', 'attachment', name)
-
-
-def _attempt(filename, content_types, debug=False):
-    if debug:
-        cherrypy.log('Attempting %r (content_types %r)' %
-                     (filename, content_types), 'TOOLS.STATICDIR')
-    try:
-        # you can set the content types for a
-        # complete directory per extension
-        content_type = None
-        if content_types:
-            r, ext = os.path.splitext(filename)
-            content_type = content_types.get(ext[1:], None)
-        serve_file(filename, content_type=content_type, debug=debug)
-        return True
-    except cherrypy.NotFound:
-        # If we didn't find the static file, continue handling the
-        # request. We might find a dynamic handler instead.
-        if debug:
-            cherrypy.log('NotFound', 'TOOLS.STATICFILE')
-        return False
-
-
-def staticdir(section, dir, root='', match='', content_types=None, index='',
-              debug=False):
-    """Serve a static resource from the given (root +) dir.
-
-    match
-        If given, request.path_info will be searched for the given
-        regular expression before attempting to serve static content.
-
-    content_types
-        If given, it should be a Python dictionary of
-        {file-extension: content-type} pairs, where 'file-extension' is
-        a string (e.g. "gif") and 'content-type' is the value to write
-        out in the Content-Type response header (e.g. "image/gif").
-
-    index
-        If provided, it should be the (relative) name of a file to
-        serve for directory requests. For example, if the dir argument is
-        '/home/me', the Request-URI is 'myapp', and the index arg is
-        'index.html', the file '/home/me/myapp/index.html' will be sought.
-    """
-    request = cherrypy.serving.request
-    if request.method not in ('GET', 'HEAD'):
-        if debug:
-            cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICDIR')
-        return False
-
-    if match and not re.search(match, request.path_info):
-        if debug:
-            cherrypy.log('request.path_info %r does not match pattern %r' %
-                         (request.path_info, match), 'TOOLS.STATICDIR')
-        return False
-
-    # Allow the use of '~' to refer to a user's home directory.
-    dir = os.path.expanduser(dir)
-
-    # If dir is relative, make absolute using "root".
-    if not os.path.isabs(dir):
-        if not root:
-            msg = 'Static dir requires an absolute dir (or root).'
-            if debug:
-                cherrypy.log(msg, 'TOOLS.STATICDIR')
-            raise ValueError(msg)
-        dir = os.path.join(root, dir)
-
-    # Determine where we are in the object tree relative to 'section'
-    # (where the static tool was defined).
-    if section == 'global':
-        section = '/'
-    section = section.rstrip(r'\/')
-    branch = request.path_info[len(section) + 1:]
-    branch = urllib.parse.unquote(branch.lstrip(r'\/'))
-
-    # Requesting a file in sub-dir of the staticdir results
-    # in mixing of delimiter styles, e.g. C:\static\js/script.js.
-    # Windows accepts this form except not when the path is
-    # supplied in extended-path notation, e.g. \\?\C:\static\js/script.js.
-    # http://bit.ly/1vdioCX
-    if platform.system() == 'Windows':
-        branch = branch.replace('/', '\\')
-
-    # If branch is "", filename will end in a slash
-    filename = os.path.join(dir, branch)
-    if debug:
-        cherrypy.log('Checking file %r to fulfill %r' %
-                     (filename, request.path_info), 'TOOLS.STATICDIR')
-
-    # There's a chance that the branch pulled from the URL might
-    # have ".." or similar uplevel attacks in it. Check that the final
-    # filename is a child of dir.
-    if not os.path.normpath(filename).startswith(os.path.normpath(dir)):
-        raise cherrypy.HTTPError(403)  # Forbidden
-
-    handled = _attempt(filename, content_types)
-    if not handled:
-        # Check for an index file if a folder was requested.
-        if index:
-            handled = _attempt(os.path.join(filename, index), content_types)
-            if handled:
-                request.is_index = filename[-1] in (r'\/')
-    return handled
-
-
-def staticfile(filename, root=None, match='', content_types=None, debug=False):
-    """Serve a static resource from the given (root +) filename.
-
-    match
-        If given, request.path_info will be searched for the given
-        regular expression before attempting to serve static content.
-
-    content_types
-        If given, it should be a Python dictionary of
-        {file-extension: content-type} pairs, where 'file-extension' is
-        a string (e.g. "gif") and 'content-type' is the value to write
-        out in the Content-Type response header (e.g. "image/gif").
-
-    """
-    request = cherrypy.serving.request
-    if request.method not in ('GET', 'HEAD'):
-        if debug:
-            cherrypy.log('request.method not GET or HEAD', 'TOOLS.STATICFILE')
-        return False
-
-    if match and not re.search(match, request.path_info):
-        if debug:
-            cherrypy.log('request.path_info %r does not match pattern %r' %
-                         (request.path_info, match), 'TOOLS.STATICFILE')
-        return False
-
-    # If filename is relative, make absolute using "root".
-    if not os.path.isabs(filename):
-        if not root:
-            msg = "Static tool requires an absolute filename (got '%s')." % (
-                filename,)
-            if debug:
-                cherrypy.log(msg, 'TOOLS.STATICFILE')
-            raise ValueError(msg)
-        filename = os.path.join(root, filename)
-
-    return _attempt(filename, content_types, debug=debug)
diff --git a/libraries/cherrypy/lib/xmlrpcutil.py b/libraries/cherrypy/lib/xmlrpcutil.py
deleted file mode 100644
index ddaac86a..00000000
--- a/libraries/cherrypy/lib/xmlrpcutil.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""XML-RPC tool helpers."""
-import sys
-
-from six.moves.xmlrpc_client import (
-    loads as xmlrpc_loads, dumps as xmlrpc_dumps,
-    Fault as XMLRPCFault
-)
-
-import cherrypy
-from cherrypy._cpcompat import ntob
-
-
-def process_body():
-    """Return (params, method) from request body."""
-    try:
-        return xmlrpc_loads(cherrypy.request.body.read())
-    except Exception:
-        return ('ERROR PARAMS', ), 'ERRORMETHOD'
-
-
-def patched_path(path):
-    """Return 'path', doctored for RPC."""
-    if not path.endswith('/'):
-        path += '/'
-    if path.startswith('/RPC2/'):
-        # strip the first /rpc2
-        path = path[5:]
-    return path
-
-
-def _set_response(body):
-    """Set up HTTP status, headers and body within CherryPy."""
-    # The XML-RPC spec (http://www.xmlrpc.com/spec) says:
-    # "Unless there's a lower-level error, always return 200 OK."
-    # Since Python's xmlrpc_client interprets a non-200 response
-    # as a "Protocol Error", we'll just return 200 every time.
-    response = cherrypy.response
-    response.status = '200 OK'
-    response.body = ntob(body, 'utf-8')
-    response.headers['Content-Type'] = 'text/xml'
-    response.headers['Content-Length'] = len(body)
-
-
-def respond(body, encoding='utf-8', allow_none=0):
-    """Construct HTTP response body."""
-    if not isinstance(body, XMLRPCFault):
-        body = (body,)
-
-    _set_response(
-        xmlrpc_dumps(
-            body, methodresponse=1,
-            encoding=encoding,
-            allow_none=allow_none
-        )
-    )
-
-
-def on_error(*args, **kwargs):
-    """Construct HTTP response body for an error response."""
-    body = str(sys.exc_info()[1])
-    _set_response(xmlrpc_dumps(XMLRPCFault(1, body)))
diff --git a/libraries/cherrypy/process/__init__.py b/libraries/cherrypy/process/__init__.py
deleted file mode 100644
index f242d226..00000000
--- a/libraries/cherrypy/process/__init__.py
+++ /dev/null
@@ -1,17 +0,0 @@
-"""Site container for an HTTP server.
-
-A Web Site Process Bus object is used to connect applications, servers,
-and frameworks with site-wide services such as daemonization, process
-reload, signal handling, drop privileges, PID file management, logging
-for all of these, and many more.
-
-The 'plugins' module defines a few abstract and concrete services for
-use with the bus. Some use tool-specific channels; see the documentation
-for each class.
-"""
-
-from .wspbus import bus
-from . import plugins, servers
-
-
-__all__ = ('bus', 'plugins', 'servers')
diff --git a/libraries/cherrypy/process/plugins.py b/libraries/cherrypy/process/plugins.py
deleted file mode 100644
index 8c246c81..00000000
--- a/libraries/cherrypy/process/plugins.py
+++ /dev/null
@@ -1,752 +0,0 @@
-"""Site services for use with a Web Site Process Bus."""
-
-import os
-import re
-import signal as _signal
-import sys
-import time
-import threading
-
-from six.moves import _thread
-
-from cherrypy._cpcompat import text_or_bytes
-from cherrypy._cpcompat import ntob, Timer
-
-# _module__file__base is used by Autoreload to make
-# absolute any filenames retrieved from sys.modules which are not
-# already absolute paths.  This is to work around Python's quirk
-# of importing the startup script and using a relative filename
-# for it in sys.modules.
-#
-# Autoreload examines sys.modules afresh every time it runs. If an application
-# changes the current directory by executing os.chdir(), then the next time
-# Autoreload runs, it will not be able to find any filenames which are
-# not absolute paths, because the current directory is not the same as when the
-# module was first imported.  Autoreload will then wrongly conclude the file
-# has "changed", and initiate the shutdown/re-exec sequence.
-# See ticket #917.
-# For this workaround to have a decent probability of success, this module
-# needs to be imported as early as possible, before the app has much chance
-# to change the working directory.
-_module__file__base = os.getcwd()
-
-
-class SimplePlugin(object):
-
-    """Plugin base class which auto-subscribes methods for known channels."""
-
-    bus = None
-    """A :class:`Bus <cherrypy.process.wspbus.Bus>`, usually cherrypy.engine.
-    """
-
-    def __init__(self, bus):
-        self.bus = bus
-
-    def subscribe(self):
-        """Register this object as a (multi-channel) listener on the bus."""
-        for channel in self.bus.listeners:
-            # Subscribe self.start, self.exit, etc. if present.
-            method = getattr(self, channel, None)
-            if method is not None:
-                self.bus.subscribe(channel, method)
-
-    def unsubscribe(self):
-        """Unregister this object as a listener on the bus."""
-        for channel in self.bus.listeners:
-            # Unsubscribe self.start, self.exit, etc. if present.
-            method = getattr(self, channel, None)
-            if method is not None:
-                self.bus.unsubscribe(channel, method)
-
-
-class SignalHandler(object):
-
-    """Register bus channels (and listeners) for system signals.
-
-    You can modify what signals your application listens for, and what it does
-    when it receives signals, by modifying :attr:`SignalHandler.handlers`,
-    a dict of {signal name: callback} pairs. The default set is::
-
-        handlers = {'SIGTERM': self.bus.exit,
-                    'SIGHUP': self.handle_SIGHUP,
-                    'SIGUSR1': self.bus.graceful,
-                   }
-
-    The :func:`SignalHandler.handle_SIGHUP`` method calls
-    :func:`bus.restart()<cherrypy.process.wspbus.Bus.restart>`
-    if the process is daemonized, but
-    :func:`bus.exit()<cherrypy.process.wspbus.Bus.exit>`
-    if the process is attached to a TTY. This is because Unix window
-    managers tend to send SIGHUP to terminal windows when the user closes them.
-
-    Feel free to add signals which are not available on every platform.
-    The :class:`SignalHandler` will ignore errors raised from attempting
-    to register handlers for unknown signals.
-    """
-
-    handlers = {}
-    """A map from signal names (e.g. 'SIGTERM') to handlers (e.g. bus.exit)."""
-
-    signals = {}
-    """A map from signal numbers to names."""
-
-    for k, v in vars(_signal).items():
-        if k.startswith('SIG') and not k.startswith('SIG_'):
-            signals[v] = k
-    del k, v
-
-    def __init__(self, bus):
-        self.bus = bus
-        # Set default handlers
-        self.handlers = {'SIGTERM': self.bus.exit,
-                         'SIGHUP': self.handle_SIGHUP,
-                         'SIGUSR1': self.bus.graceful,
-                         }
-
-        if sys.platform[:4] == 'java':
-            del self.handlers['SIGUSR1']
-            self.handlers['SIGUSR2'] = self.bus.graceful
-            self.bus.log('SIGUSR1 cannot be set on the JVM platform. '
-                         'Using SIGUSR2 instead.')
-            self.handlers['SIGINT'] = self._jython_SIGINT_handler
-
-        self._previous_handlers = {}
-        # used to determine is the process is a daemon in `self._is_daemonized`
-        self._original_pid = os.getpid()
-
-    def _jython_SIGINT_handler(self, signum=None, frame=None):
-        # See http://bugs.jython.org/issue1313
-        self.bus.log('Keyboard Interrupt: shutting down bus')
-        self.bus.exit()
-
-    def _is_daemonized(self):
-        """Return boolean indicating if the current process is
-        running as a daemon.
-
-        The criteria to determine the `daemon` condition is to verify
-        if the current pid is not the same as the one that got used on
-        the initial construction of the plugin *and* the stdin is not
-        connected to a terminal.
-
-        The sole validation of the tty is not enough when the plugin
-        is executing inside other process like in a CI tool
-        (Buildbot, Jenkins).
-        """
-        return (
-            self._original_pid != os.getpid() and
-            not os.isatty(sys.stdin.fileno())
-        )
-
-    def subscribe(self):
-        """Subscribe self.handlers to signals."""
-        for sig, func in self.handlers.items():
-            try:
-                self.set_handler(sig, func)
-            except ValueError:
-                pass
-
-    def unsubscribe(self):
-        """Unsubscribe self.handlers from signals."""
-        for signum, handler in self._previous_handlers.items():
-            signame = self.signals[signum]
-
-            if handler is None:
-                self.bus.log('Restoring %s handler to SIG_DFL.' % signame)
-                handler = _signal.SIG_DFL
-            else:
-                self.bus.log('Restoring %s handler %r.' % (signame, handler))
-
-            try:
-                our_handler = _signal.signal(signum, handler)
-                if our_handler is None:
-                    self.bus.log('Restored old %s handler %r, but our '
-                                 'handler was not registered.' %
-                                 (signame, handler), level=30)
-            except ValueError:
-                self.bus.log('Unable to restore %s handler %r.' %
-                             (signame, handler), level=40, traceback=True)
-
-    def set_handler(self, signal, listener=None):
-        """Subscribe a handler for the given signal (number or name).
-
-        If the optional 'listener' argument is provided, it will be
-        subscribed as a listener for the given signal's channel.
-
-        If the given signal name or number is not available on the current
-        platform, ValueError is raised.
-        """
-        if isinstance(signal, text_or_bytes):
-            signum = getattr(_signal, signal, None)
-            if signum is None:
-                raise ValueError('No such signal: %r' % signal)
-            signame = signal
-        else:
-            try:
-                signame = self.signals[signal]
-            except KeyError:
-                raise ValueError('No such signal: %r' % signal)
-            signum = signal
-
-        prev = _signal.signal(signum, self._handle_signal)
-        self._previous_handlers[signum] = prev
-
-        if listener is not None:
-            self.bus.log('Listening for %s.' % signame)
-            self.bus.subscribe(signame, listener)
-
-    def _handle_signal(self, signum=None, frame=None):
-        """Python signal handler (self.set_handler subscribes it for you)."""
-        signame = self.signals[signum]
-        self.bus.log('Caught signal %s.' % signame)
-        self.bus.publish(signame)
-
-    def handle_SIGHUP(self):
-        """Restart if daemonized, else exit."""
-        if self._is_daemonized():
-            self.bus.log('SIGHUP caught while daemonized. Restarting.')
-            self.bus.restart()
-        else:
-            # not daemonized (may be foreground or background)
-            self.bus.log('SIGHUP caught but not daemonized. Exiting.')
-            self.bus.exit()
-
-
-try:
-    import pwd
-    import grp
-except ImportError:
-    pwd, grp = None, None
-
-
-class DropPrivileges(SimplePlugin):
-
-    """Drop privileges. uid/gid arguments not available on Windows.
-
-    Special thanks to `Gavin Baker
-    <http://antonym.org/2005/12/dropping-privileges-in-python.html>`_
-    """
-
-    def __init__(self, bus, umask=None, uid=None, gid=None):
-        SimplePlugin.__init__(self, bus)
-        self.finalized = False
-        self.uid = uid
-        self.gid = gid
-        self.umask = umask
-
-    @property
-    def uid(self):
-        """The uid under which to run. Availability: Unix."""
-        return self._uid
-
-    @uid.setter
-    def uid(self, val):
-        if val is not None:
-            if pwd is None:
-                self.bus.log('pwd module not available; ignoring uid.',
-                             level=30)
-                val = None
-            elif isinstance(val, text_or_bytes):
-                val = pwd.getpwnam(val)[2]
-        self._uid = val
-
-    @property
-    def gid(self):
-        """The gid under which to run. Availability: Unix."""
-        return self._gid
-
-    @gid.setter
-    def gid(self, val):
-        if val is not None:
-            if grp is None:
-                self.bus.log('grp module not available; ignoring gid.',
-                             level=30)
-                val = None
-            elif isinstance(val, text_or_bytes):
-                val = grp.getgrnam(val)[2]
-        self._gid = val
-
-    @property
-    def umask(self):
-        """The default permission mode for newly created files and directories.
-
-        Usually expressed in octal format, for example, ``0644``.
-        Availability: Unix, Windows.
-        """
-        return self._umask
-
-    @umask.setter
-    def umask(self, val):
-        if val is not None:
-            try:
-                os.umask
-            except AttributeError:
-                self.bus.log('umask function not available; ignoring umask.',
-                             level=30)
-                val = None
-        self._umask = val
-
-    def start(self):
-        # uid/gid
-        def current_ids():
-            """Return the current (uid, gid) if available."""
-            name, group = None, None
-            if pwd:
-                name = pwd.getpwuid(os.getuid())[0]
-            if grp:
-                group = grp.getgrgid(os.getgid())[0]
-            return name, group
-
-        if self.finalized:
-            if not (self.uid is None and self.gid is None):
-                self.bus.log('Already running as uid: %r gid: %r' %
-                             current_ids())
-        else:
-            if self.uid is None and self.gid is None:
-                if pwd or grp:
-                    self.bus.log('uid/gid not set', level=30)
-            else:
-                self.bus.log('Started as uid: %r gid: %r' % current_ids())
-                if self.gid is not None:
-                    os.setgid(self.gid)
-                    os.setgroups([])
-                if self.uid is not None:
-                    os.setuid(self.uid)
-                self.bus.log('Running as uid: %r gid: %r' % current_ids())
-
-        # umask
-        if self.finalized:
-            if self.umask is not None:
-                self.bus.log('umask already set to: %03o' % self.umask)
-        else:
-            if self.umask is None:
-                self.bus.log('umask not set', level=30)
-            else:
-                old_umask = os.umask(self.umask)
-                self.bus.log('umask old: %03o, new: %03o' %
-                             (old_umask, self.umask))
-
-        self.finalized = True
-    # This is slightly higher than the priority for server.start
-    # in order to facilitate the most common use: starting on a low
-    # port (which requires root) and then dropping to another user.
-    start.priority = 77
-
-
-class Daemonizer(SimplePlugin):
-
-    """Daemonize the running script.
-
-    Use this with a Web Site Process Bus via::
-
-        Daemonizer(bus).subscribe()
-
-    When this component finishes, the process is completely decoupled from
-    the parent environment. Please note that when this component is used,
-    the return code from the parent process will still be 0 if a startup
-    error occurs in the forked children. Errors in the initial daemonizing
-    process still return proper exit codes. Therefore, if you use this
-    plugin to daemonize, don't use the return code as an accurate indicator
-    of whether the process fully started. In fact, that return code only
-    indicates if the process successfully finished the first fork.
-    """
-
-    def __init__(self, bus, stdin='/dev/null', stdout='/dev/null',
-                 stderr='/dev/null'):
-        SimplePlugin.__init__(self, bus)
-        self.stdin = stdin
-        self.stdout = stdout
-        self.stderr = stderr
-        self.finalized = False
-
-    def start(self):
-        if self.finalized:
-            self.bus.log('Already deamonized.')
-
-        # forking has issues with threads:
-        # http://www.opengroup.org/onlinepubs/000095399/functions/fork.html
-        # "The general problem with making fork() work in a multi-threaded
-        #  world is what to do with all of the threads..."
-        # So we check for active threads:
-        if threading.activeCount() != 1:
-            self.bus.log('There are %r active threads. '
-                         'Daemonizing now may cause strange failures.' %
-                         threading.enumerate(), level=30)
-
-        self.daemonize(self.stdin, self.stdout, self.stderr, self.bus.log)
-
-        self.finalized = True
-    start.priority = 65
-
-    @staticmethod
-    def daemonize(
-            stdin='/dev/null', stdout='/dev/null', stderr='/dev/null',
-            logger=lambda msg: None):
-        # See http://www.erlenstar.demon.co.uk/unix/faq_2.html#SEC16
-        # (or http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7)
-        # and http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/66012
-
-        # Finish up with the current stdout/stderr
-        sys.stdout.flush()
-        sys.stderr.flush()
-
-        error_tmpl = (
-            '{sys.argv[0]}: fork #{n} failed: ({exc.errno}) {exc.strerror}\n'
-        )
-
-        for fork in range(2):
-            msg = ['Forking once.', 'Forking twice.'][fork]
-            try:
-                pid = os.fork()
-                if pid > 0:
-                    # This is the parent; exit.
-                    logger(msg)
-                    os._exit(0)
-            except OSError as exc:
-                # Python raises OSError rather than returning negative numbers.
-                sys.exit(error_tmpl.format(sys=sys, exc=exc, n=fork + 1))
-            if fork == 0:
-                os.setsid()
-
-        os.umask(0)
-
-        si = open(stdin, 'r')
-        so = open(stdout, 'a+')
-        se = open(stderr, 'a+')
-
-        # os.dup2(fd, fd2) will close fd2 if necessary,
-        # so we don't explicitly close stdin/out/err.
-        # See http://docs.python.org/lib/os-fd-ops.html
-        os.dup2(si.fileno(), sys.stdin.fileno())
-        os.dup2(so.fileno(), sys.stdout.fileno())
-        os.dup2(se.fileno(), sys.stderr.fileno())
-
-        logger('Daemonized to PID: %s' % os.getpid())
-
-
-class PIDFile(SimplePlugin):
-
-    """Maintain a PID file via a WSPBus."""
-
-    def __init__(self, bus, pidfile):
-        SimplePlugin.__init__(self, bus)
-        self.pidfile = pidfile
-        self.finalized = False
-
-    def start(self):
-        pid = os.getpid()
-        if self.finalized:
-            self.bus.log('PID %r already written to %r.' % (pid, self.pidfile))
-        else:
-            open(self.pidfile, 'wb').write(ntob('%s\n' % pid, 'utf8'))
-            self.bus.log('PID %r written to %r.' % (pid, self.pidfile))
-            self.finalized = True
-    start.priority = 70
-
-    def exit(self):
-        try:
-            os.remove(self.pidfile)
-            self.bus.log('PID file removed: %r.' % self.pidfile)
-        except (KeyboardInterrupt, SystemExit):
-            raise
-        except Exception:
-            pass
-
-
-class PerpetualTimer(Timer):
-
-    """A responsive subclass of threading.Timer whose run() method repeats.
-
-    Use this timer only when you really need a very interruptible timer;
-    this checks its 'finished' condition up to 20 times a second, which can
-    results in pretty high CPU usage
-    """
-
-    def __init__(self, *args, **kwargs):
-        "Override parent constructor to allow 'bus' to be provided."
-        self.bus = kwargs.pop('bus', None)
-        super(PerpetualTimer, self).__init__(*args, **kwargs)
-
-    def run(self):
-        while True:
-            self.finished.wait(self.interval)
-            if self.finished.isSet():
-                return
-            try:
-                self.function(*self.args, **self.kwargs)
-            except Exception:
-                if self.bus:
-                    self.bus.log(
-                        'Error in perpetual timer thread function %r.' %
-                        self.function, level=40, traceback=True)
-                # Quit on first error to avoid massive logs.
-                raise
-
-
-class BackgroundTask(threading.Thread):
-
-    """A subclass of threading.Thread whose run() method repeats.
-
-    Use this class for most repeating tasks. It uses time.sleep() to wait
-    for each interval, which isn't very responsive; that is, even if you call
-    self.cancel(), you'll have to wait until the sleep() call finishes before
-    the thread stops. To compensate, it defaults to being daemonic, which means
-    it won't delay stopping the whole process.
-    """
-
-    def __init__(self, interval, function, args=[], kwargs={}, bus=None):
-        super(BackgroundTask, self).__init__()
-        self.interval = interval
-        self.function = function
-        self.args = args
-        self.kwargs = kwargs
-        self.running = False
-        self.bus = bus
-
-        # default to daemonic
-        self.daemon = True
-
-    def cancel(self):
-        self.running = False
-
-    def run(self):
-        self.running = True
-        while self.running:
-            time.sleep(self.interval)
-            if not self.running:
-                return
-            try:
-                self.function(*self.args, **self.kwargs)
-            except Exception:
-                if self.bus:
-                    self.bus.log('Error in background task thread function %r.'
-                                 % self.function, level=40, traceback=True)
-                # Quit on first error to avoid massive logs.
-                raise
-
-
-class Monitor(SimplePlugin):
-
-    """WSPBus listener to periodically run a callback in its own thread."""
-
-    callback = None
-    """The function to call at intervals."""
-
-    frequency = 60
-    """The time in seconds between callback runs."""
-
-    thread = None
-    """A :class:`BackgroundTask<cherrypy.process.plugins.BackgroundTask>`
-    thread.
-    """
-
-    def __init__(self, bus, callback, frequency=60, name=None):
-        SimplePlugin.__init__(self, bus)
-        self.callback = callback
-        self.frequency = frequency
-        self.thread = None
-        self.name = name
-
-    def start(self):
-        """Start our callback in its own background thread."""
-        if self.frequency > 0:
-            threadname = self.name or self.__class__.__name__
-            if self.thread is None:
-                self.thread = BackgroundTask(self.frequency, self.callback,
-                                             bus=self.bus)
-                self.thread.setName(threadname)
-                self.thread.start()
-                self.bus.log('Started monitor thread %r.' % threadname)
-            else:
-                self.bus.log('Monitor thread %r already started.' % threadname)
-    start.priority = 70
-
-    def stop(self):
-        """Stop our callback's background task thread."""
-        if self.thread is None:
-            self.bus.log('No thread running for %s.' %
-                         self.name or self.__class__.__name__)
-        else:
-            if self.thread is not threading.currentThread():
-                name = self.thread.getName()
-                self.thread.cancel()
-                if not self.thread.daemon:
-                    self.bus.log('Joining %r' % name)
-                    self.thread.join()
-                self.bus.log('Stopped thread %r.' % name)
-            self.thread = None
-
-    def graceful(self):
-        """Stop the callback's background task thread and restart it."""
-        self.stop()
-        self.start()
-
-
-class Autoreloader(Monitor):
-
-    """Monitor which re-executes the process when files change.
-
-    This :ref:`plugin<plugins>` restarts the process (via :func:`os.execv`)
-    if any of the files it monitors change (or is deleted). By default, the
-    autoreloader monitors all imported modules; you can add to the
-    set by adding to ``autoreload.files``::
-
-        cherrypy.engine.autoreload.files.add(myFile)
-
-    If there are imported files you do *not* wish to monitor, you can
-    adjust the ``match`` attribute, a regular expression. For example,
-    to stop monitoring cherrypy itself::
-
-        cherrypy.engine.autoreload.match = r'^(?!cherrypy).+'
-
-    Like all :class:`Monitor<cherrypy.process.plugins.Monitor>` plugins,
-    the autoreload plugin takes a ``frequency`` argument. The default is
-    1 second; that is, the autoreloader will examine files once each second.
-    """
-
-    files = None
-    """The set of files to poll for modifications."""
-
-    frequency = 1
-    """The interval in seconds at which to poll for modified files."""
-
-    match = '.*'
-    """A regular expression by which to match filenames."""
-
-    def __init__(self, bus, frequency=1, match='.*'):
-        self.mtimes = {}
-        self.files = set()
-        self.match = match
-        Monitor.__init__(self, bus, self.run, frequency)
-
-    def start(self):
-        """Start our own background task thread for self.run."""
-        if self.thread is None:
-            self.mtimes = {}
-        Monitor.start(self)
-    start.priority = 70
-
-    def sysfiles(self):
-        """Return a Set of sys.modules filenames to monitor."""
-        search_mod_names = filter(re.compile(self.match).match, sys.modules)
-        mods = map(sys.modules.get, search_mod_names)
-        return set(filter(None, map(self._file_for_module, mods)))
-
-    @classmethod
-    def _file_for_module(cls, module):
-        """Return the relevant file for the module."""
-        return (
-            cls._archive_for_zip_module(module)
-            or cls._file_for_file_module(module)
-        )
-
-    @staticmethod
-    def _archive_for_zip_module(module):
-        """Return the archive filename for the module if relevant."""
-        try:
-            return module.__loader__.archive
-        except AttributeError:
-            pass
-
-    @classmethod
-    def _file_for_file_module(cls, module):
-        """Return the file for the module."""
-        try:
-            return module.__file__ and cls._make_absolute(module.__file__)
-        except AttributeError:
-            pass
-
-    @staticmethod
-    def _make_absolute(filename):
-        """Ensure filename is absolute to avoid effect of os.chdir."""
-        return filename if os.path.isabs(filename) else (
-            os.path.normpath(os.path.join(_module__file__base, filename))
-        )
-
-    def run(self):
-        """Reload the process if registered files have been modified."""
-        for filename in self.sysfiles() | self.files:
-            if filename:
-                if filename.endswith('.pyc'):
-                    filename = filename[:-1]
-
-                oldtime = self.mtimes.get(filename, 0)
-                if oldtime is None:
-                    # Module with no .py file. Skip it.
-                    continue
-
-                try:
-                    mtime = os.stat(filename).st_mtime
-                except OSError:
-                    # Either a module with no .py file, or it's been deleted.
-                    mtime = None
-
-                if filename not in self.mtimes:
-                    # If a module has no .py file, this will be None.
-                    self.mtimes[filename] = mtime
-                else:
-                    if mtime is None or mtime > oldtime:
-                        # The file has been deleted or modified.
-                        self.bus.log('Restarting because %s changed.' %
-                                     filename)
-                        self.thread.cancel()
-                        self.bus.log('Stopped thread %r.' %
-                                     self.thread.getName())
-                        self.bus.restart()
-                        return
-
-
-class ThreadManager(SimplePlugin):
-
-    """Manager for HTTP request threads.
-
-    If you have control over thread creation and destruction, publish to
-    the 'acquire_thread' and 'release_thread' channels (for each thread).
-    This will register/unregister the current thread and publish to
-    'start_thread' and 'stop_thread' listeners in the bus as needed.
-
-    If threads are created and destroyed by code you do not control
-    (e.g., Apache), then, at the beginning of every HTTP request,
-    publish to 'acquire_thread' only. You should not publish to
-    'release_thread' in this case, since you do not know whether
-    the thread will be re-used or not. The bus will call
-    'stop_thread' listeners for you when it stops.
-    """
-
-    threads = None
-    """A map of {thread ident: index number} pairs."""
-
-    def __init__(self, bus):
-        self.threads = {}
-        SimplePlugin.__init__(self, bus)
-        self.bus.listeners.setdefault('acquire_thread', set())
-        self.bus.listeners.setdefault('start_thread', set())
-        self.bus.listeners.setdefault('release_thread', set())
-        self.bus.listeners.setdefault('stop_thread', set())
-
-    def acquire_thread(self):
-        """Run 'start_thread' listeners for the current thread.
-
-        If the current thread has already been seen, any 'start_thread'
-        listeners will not be run again.
-        """
-        thread_ident = _thread.get_ident()
-        if thread_ident not in self.threads:
-            # We can't just use get_ident as the thread ID
-            # because some platforms reuse thread ID's.
-            i = len(self.threads) + 1
-            self.threads[thread_ident] = i
-            self.bus.publish('start_thread', i)
-
-    def release_thread(self):
-        """Release the current thread and run 'stop_thread' listeners."""
-        thread_ident = _thread.get_ident()
-        i = self.threads.pop(thread_ident, None)
-        if i is not None:
-            self.bus.publish('stop_thread', i)
-
-    def stop(self):
-        """Release all threads and run all 'stop_thread' listeners."""
-        for thread_ident, i in self.threads.items():
-            self.bus.publish('stop_thread', i)
-        self.threads.clear()
-    graceful = stop
diff --git a/libraries/cherrypy/process/servers.py b/libraries/cherrypy/process/servers.py
deleted file mode 100644
index dcb34de6..00000000
--- a/libraries/cherrypy/process/servers.py
+++ /dev/null
@@ -1,416 +0,0 @@
-r"""
-Starting in CherryPy 3.1, cherrypy.server is implemented as an
-:ref:`Engine Plugin<plugins>`. It's an instance of
-:class:`cherrypy._cpserver.Server`, which is a subclass of
-:class:`cherrypy.process.servers.ServerAdapter`. The ``ServerAdapter`` class
-is designed to control other servers, as well.
-
-Multiple servers/ports
-======================
-
-If you need to start more than one HTTP server (to serve on multiple ports, or
-protocols, etc.), you can manually register each one and then start them all
-with engine.start::
-
-    s1 = ServerAdapter(
-        cherrypy.engine,
-        MyWSGIServer(host='0.0.0.0', port=80)
-    )
-    s2 = ServerAdapter(
-        cherrypy.engine,
-        another.HTTPServer(host='127.0.0.1', SSL=True)
-    )
-    s1.subscribe()
-    s2.subscribe()
-    cherrypy.engine.start()
-
-.. index:: SCGI
-
-FastCGI/SCGI
-============
-
-There are also Flup\ **F**\ CGIServer and Flup\ **S**\ CGIServer classes in
-:mod:`cherrypy.process.servers`. To start an fcgi server, for example,
-wrap an instance of it in a ServerAdapter::
-
-    addr = ('0.0.0.0', 4000)
-    f = servers.FlupFCGIServer(application=cherrypy.tree, bindAddress=addr)
-    s = servers.ServerAdapter(cherrypy.engine, httpserver=f, bind_addr=addr)
-    s.subscribe()
-
-The :doc:`cherryd</deployguide/cherryd>` startup script will do the above for
-you via its `-f` flag.
-Note that you need to download and install `flup <http://trac.saddi.com/flup>`_
-yourself, whether you use ``cherryd`` or not.
-
-.. _fastcgi:
-.. index:: FastCGI
-
-FastCGI
--------
-
-A very simple setup lets your cherry run with FastCGI.
-You just need the flup library,
-plus a running Apache server (with ``mod_fastcgi``) or lighttpd server.
-
-CherryPy code
-^^^^^^^^^^^^^
-
-hello.py::
-
-    #!/usr/bin/python
-    import cherrypy
-
-    class HelloWorld:
-        '''Sample request handler class.'''
-        @cherrypy.expose
-        def index(self):
-            return "Hello world!"
-
-    cherrypy.tree.mount(HelloWorld())
-    # CherryPy autoreload must be disabled for the flup server to work
-    cherrypy.config.update({'engine.autoreload.on':False})
-
-Then run :doc:`/deployguide/cherryd` with the '-f' arg::
-
-    cherryd -c <myconfig> -d -f -i hello.py
-
-Apache
-^^^^^^
-
-At the top level in httpd.conf::
-
-    FastCgiIpcDir /tmp
-    FastCgiServer /path/to/cherry.fcgi -idle-timeout 120 -processes 4
-
-And inside the relevant VirtualHost section::
-
-    # FastCGI config
-    AddHandler fastcgi-script .fcgi
-    ScriptAliasMatch (.*$) /path/to/cherry.fcgi$1
-
-Lighttpd
-^^^^^^^^
-
-For `Lighttpd <http://www.lighttpd.net/>`_ you can follow these
-instructions. Within ``lighttpd.conf`` make sure ``mod_fastcgi`` is
-active within ``server.modules``. Then, within your ``$HTTP["host"]``
-directive, configure your fastcgi script like the following::
-
-    $HTTP["url"] =~ "" {
-      fastcgi.server = (
-        "/" => (
-          "script.fcgi" => (
-            "bin-path" => "/path/to/your/script.fcgi",
-            "socket"          => "/tmp/script.sock",
-            "check-local"     => "disable",
-            "disable-time"    => 1,
-            "min-procs"       => 1,
-            "max-procs"       => 1, # adjust as needed
-          ),
-        ),
-      )
-    } # end of $HTTP["url"] =~ "^/"
-
-Please see `Lighttpd FastCGI Docs
-<http://redmine.lighttpd.net/wiki/lighttpd/Docs:ModFastCGI>`_ for
-an explanation of the possible configuration options.
-"""
-
-import os
-import sys
-import time
-import warnings
-import contextlib
-
-import portend
-
-
-class Timeouts:
-    occupied = 5
-    free = 1
-
-
-class ServerAdapter(object):
-
-    """Adapter for an HTTP server.
-
-    If you need to start more than one HTTP server (to serve on multiple
-    ports, or protocols, etc.), you can manually register each one and then
-    start them all with bus.start::
-
-        s1 = ServerAdapter(bus, MyWSGIServer(host='0.0.0.0', port=80))
-        s2 = ServerAdapter(bus, another.HTTPServer(host='127.0.0.1', SSL=True))
-        s1.subscribe()
-        s2.subscribe()
-        bus.start()
-    """
-
-    def __init__(self, bus, httpserver=None, bind_addr=None):
-        self.bus = bus
-        self.httpserver = httpserver
-        self.bind_addr = bind_addr
-        self.interrupt = None
-        self.running = False
-
-    def subscribe(self):
-        self.bus.subscribe('start', self.start)
-        self.bus.subscribe('stop', self.stop)
-
-    def unsubscribe(self):
-        self.bus.unsubscribe('start', self.start)
-        self.bus.unsubscribe('stop', self.stop)
-
-    def start(self):
-        """Start the HTTP server."""
-        if self.running:
-            self.bus.log('Already serving on %s' % self.description)
-            return
-
-        self.interrupt = None
-        if not self.httpserver:
-            raise ValueError('No HTTP server has been created.')
-
-        if not os.environ.get('LISTEN_PID', None):
-            # Start the httpserver in a new thread.
-            if isinstance(self.bind_addr, tuple):
-                portend.free(*self.bind_addr, timeout=Timeouts.free)
-
-        import threading
-        t = threading.Thread(target=self._start_http_thread)
-        t.setName('HTTPServer ' + t.getName())
-        t.start()
-
-        self.wait()
-        self.running = True
-        self.bus.log('Serving on %s' % self.description)
-    start.priority = 75
-
-    @property
-    def description(self):
-        """
-        A description about where this server is bound.
-        """
-        if self.bind_addr is None:
-            on_what = 'unknown interface (dynamic?)'
-        elif isinstance(self.bind_addr, tuple):
-            on_what = self._get_base()
-        else:
-            on_what = 'socket file: %s' % self.bind_addr
-        return on_what
-
-    def _get_base(self):
-        if not self.httpserver:
-            return ''
-        host, port = self.bound_addr
-        if getattr(self.httpserver, 'ssl_adapter', None):
-            scheme = 'https'
-            if port != 443:
-                host += ':%s' % port
-        else:
-            scheme = 'http'
-            if port != 80:
-                host += ':%s' % port
-
-        return '%s://%s' % (scheme, host)
-
-    def _start_http_thread(self):
-        """HTTP servers MUST be running in new threads, so that the
-        main thread persists to receive KeyboardInterrupt's. If an
-        exception is raised in the httpserver's thread then it's
-        trapped here, and the bus (and therefore our httpserver)
-        are shut down.
-        """
-        try:
-            self.httpserver.start()
-        except KeyboardInterrupt:
-            self.bus.log('<Ctrl-C> hit: shutting down HTTP server')
-            self.interrupt = sys.exc_info()[1]
-            self.bus.exit()
-        except SystemExit:
-            self.bus.log('SystemExit raised: shutting down HTTP server')
-            self.interrupt = sys.exc_info()[1]
-            self.bus.exit()
-            raise
-        except Exception:
-            self.interrupt = sys.exc_info()[1]
-            self.bus.log('Error in HTTP server: shutting down',
-                         traceback=True, level=40)
-            self.bus.exit()
-            raise
-
-    def wait(self):
-        """Wait until the HTTP server is ready to receive requests."""
-        while not getattr(self.httpserver, 'ready', False):
-            if self.interrupt:
-                raise self.interrupt
-            time.sleep(.1)
-
-        # bypass check when LISTEN_PID is set
-        if os.environ.get('LISTEN_PID', None):
-            return
-
-        # bypass check when running via socket-activation
-        # (for socket-activation the port will be managed by systemd)
-        if not isinstance(self.bind_addr, tuple):
-            return
-
-        # wait for port to be occupied
-        with _safe_wait(*self.bound_addr):
-            portend.occupied(*self.bound_addr, timeout=Timeouts.occupied)
-
-    @property
-    def bound_addr(self):
-        """
-        The bind address, or if it's an ephemeral port and the
-        socket has been bound, return the actual port bound.
-        """
-        host, port = self.bind_addr
-        if port == 0 and self.httpserver.socket:
-            # Bound to ephemeral port. Get the actual port allocated.
-            port = self.httpserver.socket.getsockname()[1]
-        return host, port
-
-    def stop(self):
-        """Stop the HTTP server."""
-        if self.running:
-            # stop() MUST block until the server is *truly* stopped.
-            self.httpserver.stop()
-            # Wait for the socket to be truly freed.
-            if isinstance(self.bind_addr, tuple):
-                portend.free(*self.bound_addr, timeout=Timeouts.free)
-            self.running = False
-            self.bus.log('HTTP Server %s shut down' % self.httpserver)
-        else:
-            self.bus.log('HTTP Server %s already shut down' % self.httpserver)
-    stop.priority = 25
-
-    def restart(self):
-        """Restart the HTTP server."""
-        self.stop()
-        self.start()
-
-
-class FlupCGIServer(object):
-
-    """Adapter for a flup.server.cgi.WSGIServer."""
-
-    def __init__(self, *args, **kwargs):
-        self.args = args
-        self.kwargs = kwargs
-        self.ready = False
-
-    def start(self):
-        """Start the CGI server."""
-        # We have to instantiate the server class here because its __init__
-        # starts a threadpool. If we do it too early, daemonize won't work.
-        from flup.server.cgi import WSGIServer
-
-        self.cgiserver = WSGIServer(*self.args, **self.kwargs)
-        self.ready = True
-        self.cgiserver.run()
-
-    def stop(self):
-        """Stop the HTTP server."""
-        self.ready = False
-
-
-class FlupFCGIServer(object):
-
-    """Adapter for a flup.server.fcgi.WSGIServer."""
-
-    def __init__(self, *args, **kwargs):
-        if kwargs.get('bindAddress', None) is None:
-            import socket
-            if not hasattr(socket, 'fromfd'):
-                raise ValueError(
-                    'Dynamic FCGI server not available on this platform. '
-                    'You must use a static or external one by providing a '
-                    'legal bindAddress.')
-        self.args = args
-        self.kwargs = kwargs
-        self.ready = False
-
-    def start(self):
-        """Start the FCGI server."""
-        # We have to instantiate the server class here because its __init__
-        # starts a threadpool. If we do it too early, daemonize won't work.
-        from flup.server.fcgi import WSGIServer
-        self.fcgiserver = WSGIServer(*self.args, **self.kwargs)
-        # TODO: report this bug upstream to flup.
-        # If we don't set _oldSIGs on Windows, we get:
-        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
-        #   line 108, in run
-        #     self._restoreSignalHandlers()
-        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
-        #   line 156, in _restoreSignalHandlers
-        #     for signum,handler in self._oldSIGs:
-        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
-        self.fcgiserver._installSignalHandlers = lambda: None
-        self.fcgiserver._oldSIGs = []
-        self.ready = True
-        self.fcgiserver.run()
-
-    def stop(self):
-        """Stop the HTTP server."""
-        # Forcibly stop the fcgi server main event loop.
-        self.fcgiserver._keepGoing = False
-        # Force all worker threads to die off.
-        self.fcgiserver._threadPool.maxSpare = (
-            self.fcgiserver._threadPool._idleCount)
-        self.ready = False
-
-
-class FlupSCGIServer(object):
-
-    """Adapter for a flup.server.scgi.WSGIServer."""
-
-    def __init__(self, *args, **kwargs):
-        self.args = args
-        self.kwargs = kwargs
-        self.ready = False
-
-    def start(self):
-        """Start the SCGI server."""
-        # We have to instantiate the server class here because its __init__
-        # starts a threadpool. If we do it too early, daemonize won't work.
-        from flup.server.scgi import WSGIServer
-        self.scgiserver = WSGIServer(*self.args, **self.kwargs)
-        # TODO: report this bug upstream to flup.
-        # If we don't set _oldSIGs on Windows, we get:
-        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
-        #   line 108, in run
-        #     self._restoreSignalHandlers()
-        #   File "C:\Python24\Lib\site-packages\flup\server\threadedserver.py",
-        #   line 156, in _restoreSignalHandlers
-        #     for signum,handler in self._oldSIGs:
-        #   AttributeError: 'WSGIServer' object has no attribute '_oldSIGs'
-        self.scgiserver._installSignalHandlers = lambda: None
-        self.scgiserver._oldSIGs = []
-        self.ready = True
-        self.scgiserver.run()
-
-    def stop(self):
-        """Stop the HTTP server."""
-        self.ready = False
-        # Forcibly stop the scgi server main event loop.
-        self.scgiserver._keepGoing = False
-        # Force all worker threads to die off.
-        self.scgiserver._threadPool.maxSpare = 0
-
-
-@contextlib.contextmanager
-def _safe_wait(host, port):
-    """
-    On systems where a loopback interface is not available and the
-    server is bound to all interfaces, it's difficult to determine
-    whether the server is in fact occupying the port. In this case,
-    just issue a warning and move on. See issue #1100.
-    """
-    try:
-        yield
-    except portend.Timeout:
-        if host == portend.client_host(host):
-            raise
-        msg = 'Unable to verify that the server is bound on %r' % port
-        warnings.warn(msg)
diff --git a/libraries/cherrypy/process/win32.py b/libraries/cherrypy/process/win32.py
deleted file mode 100644
index 096b0278..00000000
--- a/libraries/cherrypy/process/win32.py
+++ /dev/null
@@ -1,183 +0,0 @@
-"""Windows service. Requires pywin32."""
-
-import os
-import win32api
-import win32con
-import win32event
-import win32service
-import win32serviceutil
-
-from cherrypy.process import wspbus, plugins
-
-
-class ConsoleCtrlHandler(plugins.SimplePlugin):
-
-    """A WSPBus plugin for handling Win32 console events (like Ctrl-C)."""
-
-    def __init__(self, bus):
-        self.is_set = False
-        plugins.SimplePlugin.__init__(self, bus)
-
-    def start(self):
-        if self.is_set:
-            self.bus.log('Handler for console events already set.', level=40)
-            return
-
-        result = win32api.SetConsoleCtrlHandler(self.handle, 1)
-        if result == 0:
-            self.bus.log('Could not SetConsoleCtrlHandler (error %r)' %
-                         win32api.GetLastError(), level=40)
-        else:
-            self.bus.log('Set handler for console events.', level=40)
-            self.is_set = True
-
-    def stop(self):
-        if not self.is_set:
-            self.bus.log('Handler for console events already off.', level=40)
-            return
-
-        try:
-            result = win32api.SetConsoleCtrlHandler(self.handle, 0)
-        except ValueError:
-            # "ValueError: The object has not been registered"
-            result = 1
-
-        if result == 0:
-            self.bus.log('Could not remove SetConsoleCtrlHandler (error %r)' %
-                         win32api.GetLastError(), level=40)
-        else:
-            self.bus.log('Removed handler for console events.', level=40)
-            self.is_set = False
-
-    def handle(self, event):
-        """Handle console control events (like Ctrl-C)."""
-        if event in (win32con.CTRL_C_EVENT, win32con.CTRL_LOGOFF_EVENT,
-                     win32con.CTRL_BREAK_EVENT, win32con.CTRL_SHUTDOWN_EVENT,
-                     win32con.CTRL_CLOSE_EVENT):
-            self.bus.log('Console event %s: shutting down bus' % event)
-
-            # Remove self immediately so repeated Ctrl-C doesn't re-call it.
-            try:
-                self.stop()
-            except ValueError:
-                pass
-
-            self.bus.exit()
-            # 'First to return True stops the calls'
-            return 1
-        return 0
-
-
-class Win32Bus(wspbus.Bus):
-
-    """A Web Site Process Bus implementation for Win32.
-
-    Instead of time.sleep, this bus blocks using native win32event objects.
-    """
-
-    def __init__(self):
-        self.events = {}
-        wspbus.Bus.__init__(self)
-
-    def _get_state_event(self, state):
-        """Return a win32event for the given state (creating it if needed)."""
-        try:
-            return self.events[state]
-        except KeyError:
-            event = win32event.CreateEvent(None, 0, 0,
-                                           'WSPBus %s Event (pid=%r)' %
-                                           (state.name, os.getpid()))
-            self.events[state] = event
-            return event
-
-    @property
-    def state(self):
-        return self._state
-
-    @state.setter
-    def state(self, value):
-        self._state = value
-        event = self._get_state_event(value)
-        win32event.PulseEvent(event)
-
-    def wait(self, state, interval=0.1, channel=None):
-        """Wait for the given state(s), KeyboardInterrupt or SystemExit.
-
-        Since this class uses native win32event objects, the interval
-        argument is ignored.
-        """
-        if isinstance(state, (tuple, list)):
-            # Don't wait for an event that beat us to the punch ;)
-            if self.state not in state:
-                events = tuple([self._get_state_event(s) for s in state])
-                win32event.WaitForMultipleObjects(
-                    events, 0, win32event.INFINITE)
-        else:
-            # Don't wait for an event that beat us to the punch ;)
-            if self.state != state:
-                event = self._get_state_event(state)
-                win32event.WaitForSingleObject(event, win32event.INFINITE)
-
-
-class _ControlCodes(dict):
-
-    """Control codes used to "signal" a service via ControlService.
-
-    User-defined control codes are in the range 128-255. We generally use
-    the standard Python value for the Linux signal and add 128. Example:
-
-        >>> signal.SIGUSR1
-        10
-        control_codes['graceful'] = 128 + 10
-    """
-
-    def key_for(self, obj):
-        """For the given value, return its corresponding key."""
-        for key, val in self.items():
-            if val is obj:
-                return key
-        raise ValueError('The given object could not be found: %r' % obj)
-
-
-control_codes = _ControlCodes({'graceful': 138})
-
-
-def signal_child(service, command):
-    if command == 'stop':
-        win32serviceutil.StopService(service)
-    elif command == 'restart':
-        win32serviceutil.RestartService(service)
-    else:
-        win32serviceutil.ControlService(service, control_codes[command])
-
-
-class PyWebService(win32serviceutil.ServiceFramework):
-
-    """Python Web Service."""
-
-    _svc_name_ = 'Python Web Service'
-    _svc_display_name_ = 'Python Web Service'
-    _svc_deps_ = None        # sequence of service names on which this depends
-    _exe_name_ = 'pywebsvc'
-    _exe_args_ = None        # Default to no arguments
-
-    # Only exists on Windows 2000 or later, ignored on windows NT
-    _svc_description_ = 'Python Web Service'
-
-    def SvcDoRun(self):
-        from cherrypy import process
-        process.bus.start()
-        process.bus.block()
-
-    def SvcStop(self):
-        from cherrypy import process
-        self.ReportServiceStatus(win32service.SERVICE_STOP_PENDING)
-        process.bus.exit()
-
-    def SvcOther(self, control):
-        from cherrypy import process
-        process.bus.publish(control_codes.key_for(control))
-
-
-if __name__ == '__main__':
-    win32serviceutil.HandleCommandLine(PyWebService)
diff --git a/libraries/cherrypy/process/wspbus.py b/libraries/cherrypy/process/wspbus.py
deleted file mode 100644
index 39ac45bf..00000000
--- a/libraries/cherrypy/process/wspbus.py
+++ /dev/null
@@ -1,590 +0,0 @@
-r"""An implementation of the Web Site Process Bus.
-
-This module is completely standalone, depending only on the stdlib.
-
-Web Site Process Bus
---------------------
-
-A Bus object is used to contain and manage site-wide behavior:
-daemonization, HTTP server start/stop, process reload, signal handling,
-drop privileges, PID file management, logging for all of these,
-and many more.
-
-In addition, a Bus object provides a place for each web framework
-to register code that runs in response to site-wide events (like
-process start and stop), or which controls or otherwise interacts with
-the site-wide components mentioned above. For example, a framework which
-uses file-based templates would add known template filenames to an
-autoreload component.
-
-Ideally, a Bus object will be flexible enough to be useful in a variety
-of invocation scenarios:
-
- 1. The deployer starts a site from the command line via a
-    framework-neutral deployment script; applications from multiple frameworks
-    are mixed in a single site. Command-line arguments and configuration
-    files are used to define site-wide components such as the HTTP server,
-    WSGI component graph, autoreload behavior, signal handling, etc.
- 2. The deployer starts a site via some other process, such as Apache;
-    applications from multiple frameworks are mixed in a single site.
-    Autoreload and signal handling (from Python at least) are disabled.
- 3. The deployer starts a site via a framework-specific mechanism;
-    for example, when running tests, exploring tutorials, or deploying
-    single applications from a single framework. The framework controls
-    which site-wide components are enabled as it sees fit.
-
-The Bus object in this package uses topic-based publish-subscribe
-messaging to accomplish all this. A few topic channels are built in
-('start', 'stop', 'exit', 'graceful', 'log', and 'main'). Frameworks and
-site containers are free to define their own. If a message is sent to a
-channel that has not been defined or has no listeners, there is no effect.
-
-In general, there should only ever be a single Bus object per process.
-Frameworks and site containers share a single Bus object by publishing
-messages and subscribing listeners.
-
-The Bus object works as a finite state machine which models the current
-state of the process. Bus methods move it from one state to another;
-those methods then publish to subscribed listeners on the channel for
-the new state.::
-
-                        O
-                        |
-                        V
-       STOPPING --> STOPPED --> EXITING -> X
-          A   A         |
-          |    \___     |
-          |        \    |
-          |         V   V
-        STARTED <-- STARTING
-
-"""
-
-import atexit
-
-try:
-    import ctypes
-except (ImportError, MemoryError):
-    """Google AppEngine is shipped without ctypes
-
-    :seealso: http://stackoverflow.com/a/6523777/70170
-    """
-    ctypes = None
-
-import operator
-import os
-import sys
-import threading
-import time
-import traceback as _traceback
-import warnings
-import subprocess
-import functools
-
-import six
-
-
-# Here I save the value of os.getcwd(), which, if I am imported early enough,
-# will be the directory from which the startup script was run.  This is needed
-# by _do_execv(), to change back to the original directory before execv()ing a
-# new process.  This is a defense against the application having changed the
-# current working directory (which could make sys.executable "not found" if
-# sys.executable is a relative-path, and/or cause other problems).
-_startup_cwd = os.getcwd()
-
-
-class ChannelFailures(Exception):
-    """Exception raised during errors on Bus.publish()."""
-
-    delimiter = '\n'
-
-    def __init__(self, *args, **kwargs):
-        """Initialize ChannelFailures errors wrapper."""
-        super(ChannelFailures, self).__init__(*args, **kwargs)
-        self._exceptions = list()
-
-    def handle_exception(self):
-        """Append the current exception to self."""
-        self._exceptions.append(sys.exc_info()[1])
-
-    def get_instances(self):
-        """Return a list of seen exception instances."""
-        return self._exceptions[:]
-
-    def __str__(self):
-        """Render the list of errors, which happened in channel."""
-        exception_strings = map(repr, self.get_instances())
-        return self.delimiter.join(exception_strings)
-
-    __repr__ = __str__
-
-    def __bool__(self):
-        """Determine whether any error happened in channel."""
-        return bool(self._exceptions)
-    __nonzero__ = __bool__
-
-# Use a flag to indicate the state of the bus.
-
-
-class _StateEnum(object):
-
-    class State(object):
-        name = None
-
-        def __repr__(self):
-            return 'states.%s' % self.name
-
-    def __setattr__(self, key, value):
-        if isinstance(value, self.State):
-            value.name = key
-        object.__setattr__(self, key, value)
-
-
-states = _StateEnum()
-states.STOPPED = states.State()
-states.STARTING = states.State()
-states.STARTED = states.State()
-states.STOPPING = states.State()
-states.EXITING = states.State()
-
-
-try:
-    import fcntl
-except ImportError:
-    max_files = 0
-else:
-    try:
-        max_files = os.sysconf('SC_OPEN_MAX')
-    except AttributeError:
-        max_files = 1024
-
-
-class Bus(object):
-    """Process state-machine and messenger for HTTP site deployment.
-
-    All listeners for a given channel are guaranteed to be called even
-    if others at the same channel fail. Each failure is logged, but
-    execution proceeds on to the next listener. The only way to stop all
-    processing from inside a listener is to raise SystemExit and stop the
-    whole server.
-    """
-
-    states = states
-    state = states.STOPPED
-    execv = False
-    max_cloexec_files = max_files
-
-    def __init__(self):
-        """Initialize pub/sub bus."""
-        self.execv = False
-        self.state = states.STOPPED
-        channels = 'start', 'stop', 'exit', 'graceful', 'log', 'main'
-        self.listeners = dict(
-            (channel, set())
-            for channel in channels
-        )
-        self._priorities = {}
-
-    def subscribe(self, channel, callback=None, priority=None):
-        """Add the given callback at the given channel (if not present).
-
-        If callback is None, return a partial suitable for decorating
-        the callback.
-        """
-        if callback is None:
-            return functools.partial(
-                self.subscribe,
-                channel,
-                priority=priority,
-            )
-
-        ch_listeners = self.listeners.setdefault(channel, set())
-        ch_listeners.add(callback)
-
-        if priority is None:
-            priority = getattr(callback, 'priority', 50)
-        self._priorities[(channel, callback)] = priority
-
-    def unsubscribe(self, channel, callback):
-        """Discard the given callback (if present)."""
-        listeners = self.listeners.get(channel)
-        if listeners and callback in listeners:
-            listeners.discard(callback)
-            del self._priorities[(channel, callback)]
-
-    def publish(self, channel, *args, **kwargs):
-        """Return output of all subscribers for the given channel."""
-        if channel not in self.listeners:
-            return []
-
-        exc = ChannelFailures()
-        output = []
-
-        raw_items = (
-            (self._priorities[(channel, listener)], listener)
-            for listener in self.listeners[channel]
-        )
-        items = sorted(raw_items, key=operator.itemgetter(0))
-        for priority, listener in items:
-            try:
-                output.append(listener(*args, **kwargs))
-            except KeyboardInterrupt:
-                raise
-            except SystemExit:
-                e = sys.exc_info()[1]
-                # If we have previous errors ensure the exit code is non-zero
-                if exc and e.code == 0:
-                    e.code = 1
-                raise
-            except Exception:
-                exc.handle_exception()
-                if channel == 'log':
-                    # Assume any further messages to 'log' will fail.
-                    pass
-                else:
-                    self.log('Error in %r listener %r' % (channel, listener),
-                             level=40, traceback=True)
-        if exc:
-            raise exc
-        return output
-
-    def _clean_exit(self):
-        """Assert that the Bus is not running in atexit handler callback."""
-        if self.state != states.EXITING:
-            warnings.warn(
-                'The main thread is exiting, but the Bus is in the %r state; '
-                'shutting it down automatically now. You must either call '
-                'bus.block() after start(), or call bus.exit() before the '
-                'main thread exits.' % self.state, RuntimeWarning)
-            self.exit()
-
-    def start(self):
-        """Start all services."""
-        atexit.register(self._clean_exit)
-
-        self.state = states.STARTING
-        self.log('Bus STARTING')
-        try:
-            self.publish('start')
-            self.state = states.STARTED
-            self.log('Bus STARTED')
-        except (KeyboardInterrupt, SystemExit):
-            raise
-        except Exception:
-            self.log('Shutting down due to error in start listener:',
-                     level=40, traceback=True)
-            e_info = sys.exc_info()[1]
-            try:
-                self.exit()
-            except Exception:
-                # Any stop/exit errors will be logged inside publish().
-                pass
-            # Re-raise the original error
-            raise e_info
-
-    def exit(self):
-        """Stop all services and prepare to exit the process."""
-        exitstate = self.state
-        EX_SOFTWARE = 70
-        try:
-            self.stop()
-
-            self.state = states.EXITING
-            self.log('Bus EXITING')
-            self.publish('exit')
-            # This isn't strictly necessary, but it's better than seeing
-            # "Waiting for child threads to terminate..." and then nothing.
-            self.log('Bus EXITED')
-        except Exception:
-            # This method is often called asynchronously (whether thread,
-            # signal handler, console handler, or atexit handler), so we
-            # can't just let exceptions propagate out unhandled.
-            # Assume it's been logged and just die.
-            os._exit(EX_SOFTWARE)
-
-        if exitstate == states.STARTING:
-            # exit() was called before start() finished, possibly due to
-            # Ctrl-C because a start listener got stuck. In this case,
-            # we could get stuck in a loop where Ctrl-C never exits the
-            # process, so we just call os.exit here.
-            os._exit(EX_SOFTWARE)
-
-    def restart(self):
-        """Restart the process (may close connections).
-
-        This method does not restart the process from the calling thread;
-        instead, it stops the bus and asks the main thread to call execv.
-        """
-        self.execv = True
-        self.exit()
-
-    def graceful(self):
-        """Advise all services to reload."""
-        self.log('Bus graceful')
-        self.publish('graceful')
-
-    def block(self, interval=0.1):
-        """Wait for the EXITING state, KeyboardInterrupt or SystemExit.
-
-        This function is intended to be called only by the main thread.
-        After waiting for the EXITING state, it also waits for all threads
-        to terminate, and then calls os.execv if self.execv is True. This
-        design allows another thread to call bus.restart, yet have the main
-        thread perform the actual execv call (required on some platforms).
-        """
-        try:
-            self.wait(states.EXITING, interval=interval, channel='main')
-        except (KeyboardInterrupt, IOError):
-            # The time.sleep call might raise
-            # "IOError: [Errno 4] Interrupted function call" on KBInt.
-            self.log('Keyboard Interrupt: shutting down bus')
-            self.exit()
-        except SystemExit:
-            self.log('SystemExit raised: shutting down bus')
-            self.exit()
-            raise
-
-        # Waiting for ALL child threads to finish is necessary on OS X.
-        # See https://github.com/cherrypy/cherrypy/issues/581.
-        # It's also good to let them all shut down before allowing
-        # the main thread to call atexit handlers.
-        # See https://github.com/cherrypy/cherrypy/issues/751.
-        self.log('Waiting for child threads to terminate...')
-        for t in threading.enumerate():
-            # Validate the we're not trying to join the MainThread
-            # that will cause a deadlock and the case exist when
-            # implemented as a windows service and in any other case
-            # that another thread executes cherrypy.engine.exit()
-            if (
-                    t != threading.currentThread() and
-                    not isinstance(t, threading._MainThread) and
-                    # Note that any dummy (external) threads are
-                    # always daemonic.
-                    not t.daemon
-            ):
-                self.log('Waiting for thread %s.' % t.getName())
-                t.join()
-
-        if self.execv:
-            self._do_execv()
-
-    def wait(self, state, interval=0.1, channel=None):
-        """Poll for the given state(s) at intervals; publish to channel."""
-        if isinstance(state, (tuple, list)):
-            states = state
-        else:
-            states = [state]
-
-        while self.state not in states:
-            time.sleep(interval)
-            self.publish(channel)
-
-    def _do_execv(self):
-        """Re-execute the current process.
-
-        This must be called from the main thread, because certain platforms
-        (OS X) don't allow execv to be called in a child thread very well.
-        """
-        try:
-            args = self._get_true_argv()
-        except NotImplementedError:
-            """It's probably win32 or GAE"""
-            args = [sys.executable] + self._get_interpreter_argv() + sys.argv
-
-        self.log('Re-spawning %s' % ' '.join(args))
-
-        self._extend_pythonpath(os.environ)
-
-        if sys.platform[:4] == 'java':
-            from _systemrestart import SystemRestart
-            raise SystemRestart
-        else:
-            if sys.platform == 'win32':
-                args = ['"%s"' % arg for arg in args]
-
-            os.chdir(_startup_cwd)
-            if self.max_cloexec_files:
-                self._set_cloexec()
-            os.execv(sys.executable, args)
-
-    @staticmethod
-    def _get_interpreter_argv():
-        """Retrieve current Python interpreter's arguments.
-
-        Returns empty tuple in case of frozen mode, uses built-in arguments
-        reproduction function otherwise.
-
-        Frozen mode is possible for the app has been packaged into a binary
-        executable using py2exe. In this case the interpreter's arguments are
-        already built-in into that executable.
-
-        :seealso: https://github.com/cherrypy/cherrypy/issues/1526
-        Ref: https://pythonhosted.org/PyInstaller/runtime-information.html
-        """
-        return ([]
-                if getattr(sys, 'frozen', False)
-                else subprocess._args_from_interpreter_flags())
-
-    @staticmethod
-    def _get_true_argv():
-        """Retrieve all real arguments of the python interpreter.
-
-        ...even those not listed in ``sys.argv``
-
-        :seealso: http://stackoverflow.com/a/28338254/595220
-        :seealso: http://stackoverflow.com/a/6683222/595220
-        :seealso: http://stackoverflow.com/a/28414807/595220
-        """
-        try:
-            char_p = ctypes.c_char_p if six.PY2 else ctypes.c_wchar_p
-
-            argv = ctypes.POINTER(char_p)()
-            argc = ctypes.c_int()
-
-            ctypes.pythonapi.Py_GetArgcArgv(
-                ctypes.byref(argc),
-                ctypes.byref(argv),
-            )
-
-            _argv = argv[:argc.value]
-
-            # The code below is trying to correctly handle special cases.
-            # `-c`'s argument interpreted by Python itself becomes `-c` as
-            # well. Same applies to `-m`. This snippet is trying to survive
-            # at least the case with `-m`
-            # Ref: https://github.com/cherrypy/cherrypy/issues/1545
-            # Ref: python/cpython@418baf9
-            argv_len, is_command, is_module = len(_argv), False, False
-
-            try:
-                m_ind = _argv.index('-m')
-                if m_ind < argv_len - 1 and _argv[m_ind + 1] in ('-c', '-m'):
-                    """
-                    In some older Python versions `-m`'s argument may be
-                    substituted with `-c`, not `-m`
-                    """
-                    is_module = True
-            except (IndexError, ValueError):
-                m_ind = None
-
-            try:
-                c_ind = _argv.index('-c')
-                if c_ind < argv_len - 1 and _argv[c_ind + 1] == '-c':
-                    is_command = True
-            except (IndexError, ValueError):
-                c_ind = None
-
-            if is_module:
-                """It's containing `-m -m` sequence of arguments"""
-                if is_command and c_ind < m_ind:
-                    """There's `-c -c` before `-m`"""
-                    raise RuntimeError(
-                        "Cannot reconstruct command from '-c'. Ref: "
-                        'https://github.com/cherrypy/cherrypy/issues/1545')
-                # Survive module argument here
-                original_module = sys.argv[0]
-                if not os.access(original_module, os.R_OK):
-                    """There's no such module exist"""
-                    raise AttributeError(
-                        "{} doesn't seem to be a module "
-                        'accessible by current user'.format(original_module))
-                del _argv[m_ind:m_ind + 2]  # remove `-m -m`
-                # ... and substitute it with the original module path:
-                _argv.insert(m_ind, original_module)
-            elif is_command:
-                """It's containing just `-c -c` sequence of arguments"""
-                raise RuntimeError(
-                    "Cannot reconstruct command from '-c'. "
-                    'Ref: https://github.com/cherrypy/cherrypy/issues/1545')
-        except AttributeError:
-            """It looks Py_GetArgcArgv is completely absent in some environments
-
-            It is known, that there's no Py_GetArgcArgv in MS Windows and
-            ``ctypes`` module is completely absent in Google AppEngine
-
-            :seealso: https://github.com/cherrypy/cherrypy/issues/1506
-            :seealso: https://github.com/cherrypy/cherrypy/issues/1512
-            :ref: http://bit.ly/2gK6bXK
-            """
-            raise NotImplementedError
-        else:
-            return _argv
-
-    @staticmethod
-    def _extend_pythonpath(env):
-        """Prepend current working dir to PATH environment variable if needed.
-
-        If sys.path[0] is an empty string, the interpreter was likely
-        invoked with -m and the effective path is about to change on
-        re-exec.  Add the current directory to $PYTHONPATH to ensure
-        that the new process sees the same path.
-
-        This issue cannot be addressed in the general case because
-        Python cannot reliably reconstruct the
-        original command line (http://bugs.python.org/issue14208).
-
-        (This idea filched from tornado.autoreload)
-        """
-        path_prefix = '.' + os.pathsep
-        existing_path = env.get('PYTHONPATH', '')
-        needs_patch = (
-            sys.path[0] == '' and
-            not existing_path.startswith(path_prefix)
-        )
-
-        if needs_patch:
-            env['PYTHONPATH'] = path_prefix + existing_path
-
-    def _set_cloexec(self):
-        """Set the CLOEXEC flag on all open files (except stdin/out/err).
-
-        If self.max_cloexec_files is an integer (the default), then on
-        platforms which support it, it represents the max open files setting
-        for the operating system. This function will be called just before
-        the process is restarted via os.execv() to prevent open files
-        from persisting into the new process.
-
-        Set self.max_cloexec_files to 0 to disable this behavior.
-        """
-        for fd in range(3, self.max_cloexec_files):  # skip stdin/out/err
-            try:
-                flags = fcntl.fcntl(fd, fcntl.F_GETFD)
-            except IOError:
-                continue
-            fcntl.fcntl(fd, fcntl.F_SETFD, flags | fcntl.FD_CLOEXEC)
-
-    def stop(self):
-        """Stop all services."""
-        self.state = states.STOPPING
-        self.log('Bus STOPPING')
-        self.publish('stop')
-        self.state = states.STOPPED
-        self.log('Bus STOPPED')
-
-    def start_with_callback(self, func, args=None, kwargs=None):
-        """Start 'func' in a new thread T, then start self (and return T)."""
-        if args is None:
-            args = ()
-        if kwargs is None:
-            kwargs = {}
-        args = (func,) + args
-
-        def _callback(func, *a, **kw):
-            self.wait(states.STARTED)
-            func(*a, **kw)
-        t = threading.Thread(target=_callback, args=args, kwargs=kwargs)
-        t.setName('Bus Callback ' + t.getName())
-        t.start()
-
-        self.start()
-
-        return t
-
-    def log(self, msg='', level=20, traceback=False):
-        """Log the given message. Append the last traceback if requested."""
-        if traceback:
-            msg += '\n' + ''.join(_traceback.format_exception(*sys.exc_info()))
-        self.publish('log', msg, level)
-
-
-bus = Bus()
diff --git a/libraries/cherrypy/scaffold/__init__.py b/libraries/cherrypy/scaffold/__init__.py
deleted file mode 100644
index bcddba2d..00000000
--- a/libraries/cherrypy/scaffold/__init__.py
+++ /dev/null
@@ -1,63 +0,0 @@
-"""<MyProject>, a CherryPy application.
-
-Use this as a base for creating new CherryPy applications. When you want
-to make a new app, copy and paste this folder to some other location
-(maybe site-packages) and rename it to the name of your project,
-then tweak as desired.
-
-Even before any tweaking, this should serve a few demonstration pages.
-Change to this directory and run:
-
-    cherryd -c site.conf
-
-"""
-
-import cherrypy
-from cherrypy import tools, url
-
-import os
-local_dir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-@cherrypy.config(**{'tools.log_tracebacks.on': True})
-class Root:
-    """Declaration of the CherryPy app URI structure."""
-
-    @cherrypy.expose
-    def index(self):
-        """Render HTML-template at the root path of the web-app."""
-        return """<html>
-<body>Try some <a href='%s?a=7'>other</a> path,
-or a <a href='%s?n=14'>default</a> path.<br />
-Or, just look at the pretty picture:<br />
-<img src='%s' />
-</body></html>""" % (url('other'), url('else'),
-                     url('files/made_with_cherrypy_small.png'))
-
-    @cherrypy.expose
-    def default(self, *args, **kwargs):
-        """Render catch-all args and kwargs."""
-        return 'args: %s kwargs: %s' % (args, kwargs)
-
-    @cherrypy.expose
-    def other(self, a=2, b='bananas', c=None):
-        """Render number of fruits based on third argument."""
-        cherrypy.response.headers['Content-Type'] = 'text/plain'
-        if c is None:
-            return 'Have %d %s.' % (int(a), b)
-        else:
-            return 'Have %d %s, %s.' % (int(a), b, c)
-
-    files = tools.staticdir.handler(
-        section='/files',
-        dir=os.path.join(local_dir, 'static'),
-        # Ignore .php files, etc.
-                match=r'\.(css|gif|html?|ico|jpe?g|js|png|swf|xml)$',
-    )
-
-
-root = Root()
-
-# Uncomment the following to use your own favicon instead of CP's default.
-# favicon_path = os.path.join(local_dir, "favicon.ico")
-# root.favicon_ico = tools.staticfile.handler(filename=favicon_path)
diff --git a/libraries/cherrypy/scaffold/apache-fcgi.conf b/libraries/cherrypy/scaffold/apache-fcgi.conf
deleted file mode 100644
index 6e4f144c..00000000
--- a/libraries/cherrypy/scaffold/apache-fcgi.conf
+++ /dev/null
@@ -1,22 +0,0 @@
-# Apache2 server conf file for using CherryPy with mod_fcgid.
-
-# This doesn't have to be "C:/", but it has to be a directory somewhere, and
-# MUST match the directory used in the FastCgiExternalServer directive, below.
-DocumentRoot "C:/"
-
-ServerName 127.0.0.1
-Listen 80
-LoadModule fastcgi_module modules/mod_fastcgi.dll
-LoadModule rewrite_module modules/mod_rewrite.so
-
-Options ExecCGI
-SetHandler fastcgi-script
-RewriteEngine On
-# Send requests for any URI to our fastcgi handler.
-RewriteRule ^(.*)$ /fastcgi.pyc [L]
-
-# The FastCgiExternalServer directive defines filename as an external FastCGI application.
-# If filename does not begin with a slash (/) then it is assumed to be relative to the ServerRoot.
-# The filename does not have to exist in the local filesystem. URIs that Apache resolves to this
-# filename will be handled by this external FastCGI application.
-FastCgiExternalServer "C:/fastcgi.pyc" -host 127.0.0.1:8088
diff --git a/libraries/cherrypy/scaffold/example.conf b/libraries/cherrypy/scaffold/example.conf
deleted file mode 100644
index 63250fe3..00000000
--- a/libraries/cherrypy/scaffold/example.conf
+++ /dev/null
@@ -1,3 +0,0 @@
-[/]
-log.error_file: "error.log"
-log.access_file: "access.log"
diff --git a/libraries/cherrypy/scaffold/site.conf b/libraries/cherrypy/scaffold/site.conf
deleted file mode 100644
index 6ed38983..00000000
--- a/libraries/cherrypy/scaffold/site.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-[global]
-# Uncomment this when you're done developing
-#environment: "production"
-
-server.socket_host: "0.0.0.0"
-server.socket_port: 8088
-
-# Uncomment the following lines to run on HTTPS at the same time
-#server.2.socket_host: "0.0.0.0"
-#server.2.socket_port: 8433
-#server.2.ssl_certificate: '../test/test.pem'
-#server.2.ssl_private_key: '../test/test.pem'
-
-tree.myapp: cherrypy.Application(scaffold.root, "/", "example.conf")
diff --git a/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png b/libraries/cherrypy/scaffold/static/made_with_cherrypy_small.png
deleted file mode 100644
index 724f9d72d9ca5aede0b788fc1216286aa967e212..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 6347
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
N002ovPDHLkV1nY?RmA`R

diff --git a/libraries/cherrypy/test/__init__.py b/libraries/cherrypy/test/__init__.py
deleted file mode 100644
index 068382be..00000000
--- a/libraries/cherrypy/test/__init__.py
+++ /dev/null
@@ -1,24 +0,0 @@
-"""
-Regression test suite for CherryPy.
-"""
-
-import os
-import sys
-
-
-def newexit():
-    os._exit(1)
-
-
-def setup():
-    # We want to monkey patch sys.exit so that we can get some
-    # information about where exit is being called.
-    newexit._old = sys.exit
-    sys.exit = newexit
-
-
-def teardown():
-    try:
-        sys.exit = sys.exit._old
-    except AttributeError:
-        sys.exit = sys._exit
diff --git a/libraries/cherrypy/test/_test_decorators.py b/libraries/cherrypy/test/_test_decorators.py
deleted file mode 100644
index 74832e40..00000000
--- a/libraries/cherrypy/test/_test_decorators.py
+++ /dev/null
@@ -1,39 +0,0 @@
-"""Test module for the @-decorator syntax, which is version-specific"""
-
-import cherrypy
-from cherrypy import expose, tools
-
-
-class ExposeExamples(object):
-
-    @expose
-    def no_call(self):
-        return 'Mr E. R. Bradshaw'
-
-    @expose()
-    def call_empty(self):
-        return 'Mrs. B.J. Smegma'
-
-    @expose('call_alias')
-    def nesbitt(self):
-        return 'Mr Nesbitt'
-
-    @expose(['alias1', 'alias2'])
-    def andrews(self):
-        return 'Mr Ken Andrews'
-
-    @expose(alias='alias3')
-    def watson(self):
-        return 'Mr. and Mrs. Watson'
-
-
-class ToolExamples(object):
-
-    @expose
-    # This is here to demonstrate that using the config decorator
-    # does not overwrite other config attributes added by the Tool
-    # decorator (in this case response_headers).
-    @cherrypy.config(**{'response.stream': True})
-    @tools.response_headers(headers=[('Content-Type', 'application/data')])
-    def blah(self):
-        yield b'blah'
diff --git a/libraries/cherrypy/test/_test_states_demo.py b/libraries/cherrypy/test/_test_states_demo.py
deleted file mode 100644
index a49407ba..00000000
--- a/libraries/cherrypy/test/_test_states_demo.py
+++ /dev/null
@@ -1,69 +0,0 @@
-import os
-import sys
-import time
-
-import cherrypy
-
-starttime = time.time()
-
-
-class Root:
-
-    @cherrypy.expose
-    def index(self):
-        return 'Hello World'
-
-    @cherrypy.expose
-    def mtimes(self):
-        return repr(cherrypy.engine.publish('Autoreloader', 'mtimes'))
-
-    @cherrypy.expose
-    def pid(self):
-        return str(os.getpid())
-
-    @cherrypy.expose
-    def start(self):
-        return repr(starttime)
-
-    @cherrypy.expose
-    def exit(self):
-        # This handler might be called before the engine is STARTED if an
-        # HTTP worker thread handles it before the HTTP server returns
-        # control to engine.start. We avoid that race condition here
-        # by waiting for the Bus to be STARTED.
-        cherrypy.engine.wait(state=cherrypy.engine.states.STARTED)
-        cherrypy.engine.exit()
-
-
-@cherrypy.engine.subscribe('start', priority=100)
-def unsub_sig():
-    cherrypy.log('unsubsig: %s' % cherrypy.config.get('unsubsig', False))
-    if cherrypy.config.get('unsubsig', False):
-        cherrypy.log('Unsubscribing the default cherrypy signal handler')
-        cherrypy.engine.signal_handler.unsubscribe()
-    try:
-        from signal import signal, SIGTERM
-    except ImportError:
-        pass
-    else:
-        def old_term_handler(signum=None, frame=None):
-            cherrypy.log('I am an old SIGTERM handler.')
-            sys.exit(0)
-        cherrypy.log('Subscribing the new one.')
-        signal(SIGTERM, old_term_handler)
-
-
-@cherrypy.engine.subscribe('start', priority=6)
-def starterror():
-    if cherrypy.config.get('starterror', False):
-        1 / 0
-
-
-@cherrypy.engine.subscribe('start', priority=6)
-def log_test_case_name():
-    if cherrypy.config.get('test_case_name', False):
-        cherrypy.log('STARTED FROM: %s' %
-                     cherrypy.config.get('test_case_name'))
-
-
-cherrypy.tree.mount(Root(), '/', {'/': {}})
diff --git a/libraries/cherrypy/test/benchmark.py b/libraries/cherrypy/test/benchmark.py
deleted file mode 100644
index 44dfeff1..00000000
--- a/libraries/cherrypy/test/benchmark.py
+++ /dev/null
@@ -1,425 +0,0 @@
-"""CherryPy Benchmark Tool
-
-    Usage:
-        benchmark.py [options]
-
-    --null:        use a null Request object (to bench the HTTP server only)
-    --notests:     start the server but do not run the tests; this allows
-                   you to check the tested pages with a browser
-    --help:        show this help message
-    --cpmodpy:     run tests via apache on 54583 (with the builtin _cpmodpy)
-    --modpython:   run tests via apache on 54583 (with modpython_gateway)
-    --ab=path:     Use the ab script/executable at 'path' (see below)
-    --apache=path: Use the apache script/exe at 'path' (see below)
-
-    To run the benchmarks, the Apache Benchmark tool "ab" must either be on
-    your system path, or specified via the --ab=path option.
-
-    To run the modpython tests, the "apache" executable or script must be
-    on your system path, or provided via the --apache=path option. On some
-    platforms, "apache" may be called "apachectl" or "apache2ctl"--create
-    a symlink to them if needed.
-"""
-
-import getopt
-import os
-import re
-import sys
-import time
-
-import cherrypy
-from cherrypy import _cperror, _cpmodpy
-from cherrypy.lib import httputil
-
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-AB_PATH = ''
-APACHE_PATH = 'apache'
-SCRIPT_NAME = '/cpbench/users/rdelon/apps/blog'
-
-__all__ = ['ABSession', 'Root', 'print_report',
-           'run_standard_benchmarks', 'safe_threads',
-           'size_report', 'thread_report',
-           ]
-
-size_cache = {}
-
-
-class Root:
-
-    @cherrypy.expose
-    def index(self):
-        return """<html>
-<head>
-    <title>CherryPy Benchmark</title>
-</head>
-<body>
-    <ul>
-        <li><a href="hello">Hello, world! (14 byte dynamic)</a></li>
-        <li><a href="static/index.html">Static file (14 bytes static)</a></li>
-        <li><form action="sizer">Response of length:
-            <input type='text' name='size' value='10' /></form>
-        </li>
-    </ul>
-</body>
-</html>"""
-
-    @cherrypy.expose
-    def hello(self):
-        return 'Hello, world\r\n'
-
-    @cherrypy.expose
-    def sizer(self, size):
-        resp = size_cache.get(size, None)
-        if resp is None:
-            size_cache[size] = resp = 'X' * int(size)
-        return resp
-
-
-def init():
-
-    cherrypy.config.update({
-        'log.error.file': '',
-        'environment': 'production',
-        'server.socket_host': '127.0.0.1',
-        'server.socket_port': 54583,
-        'server.max_request_header_size': 0,
-        'server.max_request_body_size': 0,
-    })
-
-    # Cheat mode on ;)
-    del cherrypy.config['tools.log_tracebacks.on']
-    del cherrypy.config['tools.log_headers.on']
-    del cherrypy.config['tools.trailing_slash.on']
-
-    appconf = {
-        '/static': {
-            'tools.staticdir.on': True,
-            'tools.staticdir.dir': 'static',
-            'tools.staticdir.root': curdir,
-        },
-    }
-    globals().update(
-        app=cherrypy.tree.mount(Root(), SCRIPT_NAME, appconf),
-    )
-
-
-class NullRequest:
-
-    """A null HTTP request class, returning 200 and an empty body."""
-
-    def __init__(self, local, remote, scheme='http'):
-        pass
-
-    def close(self):
-        pass
-
-    def run(self, method, path, query_string, protocol, headers, rfile):
-        cherrypy.response.status = '200 OK'
-        cherrypy.response.header_list = [('Content-Type', 'text/html'),
-                                         ('Server', 'Null CherryPy'),
-                                         ('Date', httputil.HTTPDate()),
-                                         ('Content-Length', '0'),
-                                         ]
-        cherrypy.response.body = ['']
-        return cherrypy.response
-
-
-class NullResponse:
-    pass
-
-
-class ABSession:
-
-    """A session of 'ab', the Apache HTTP server benchmarking tool.
-
-Example output from ab:
-
-This is ApacheBench, Version 2.0.40-dev <$Revision: 1.121.2.1 $> apache-2.0
-Copyright (c) 1996 Adam Twiss, Zeus Technology Ltd, http://www.zeustech.net/
-Copyright (c) 1998-2002 The Apache Software Foundation, http://www.apache.org/
-
-Benchmarking 127.0.0.1 (be patient)
-Completed 100 requests
-Completed 200 requests
-Completed 300 requests
-Completed 400 requests
-Completed 500 requests
-Completed 600 requests
-Completed 700 requests
-Completed 800 requests
-Completed 900 requests
-
-
-Server Software:        CherryPy/3.1beta
-Server Hostname:        127.0.0.1
-Server Port:            54583
-
-Document Path:          /static/index.html
-Document Length:        14 bytes
-
-Concurrency Level:      10
-Time taken for tests:   9.643867 seconds
-Complete requests:      1000
-Failed requests:        0
-Write errors:           0
-Total transferred:      189000 bytes
-HTML transferred:       14000 bytes
-Requests per second:    103.69 [#/sec] (mean)
-Time per request:       96.439 [ms] (mean)
-Time per request:       9.644 [ms] (mean, across all concurrent requests)
-Transfer rate:          19.08 [Kbytes/sec] received
-
-Connection Times (ms)
-              min  mean[+/-sd] median   max
-Connect:        0    0   2.9      0      10
-Processing:    20   94   7.3     90     130
-Waiting:        0   43  28.1     40     100
-Total:         20   95   7.3    100     130
-
-Percentage of the requests served within a certain time (ms)
-  50%    100
-  66%    100
-  75%    100
-  80%    100
-  90%    100
-  95%    100
-  98%    100
-  99%    110
- 100%    130 (longest request)
-Finished 1000 requests
-"""
-
-    parse_patterns = [
-        ('complete_requests', 'Completed',
-         br'^Complete requests:\s*(\d+)'),
-        ('failed_requests', 'Failed',
-         br'^Failed requests:\s*(\d+)'),
-        ('requests_per_second', 'req/sec',
-         br'^Requests per second:\s*([0-9.]+)'),
-        ('time_per_request_concurrent', 'msec/req',
-         br'^Time per request:\s*([0-9.]+).*concurrent requests\)$'),
-        ('transfer_rate', 'KB/sec',
-         br'^Transfer rate:\s*([0-9.]+)')
-    ]
-
-    def __init__(self, path=SCRIPT_NAME + '/hello', requests=1000,
-                 concurrency=10):
-        self.path = path
-        self.requests = requests
-        self.concurrency = concurrency
-
-    def args(self):
-        port = cherrypy.server.socket_port
-        assert self.concurrency > 0
-        assert self.requests > 0
-        # Don't use "localhost".
-        # Cf
-        # http://mail.python.org/pipermail/python-win32/2008-March/007050.html
-        return ('-k -n %s -c %s http://127.0.0.1:%s%s' %
-                (self.requests, self.concurrency, port, self.path))
-
-    def run(self):
-        # Parse output of ab, setting attributes on self
-        try:
-            self.output = _cpmodpy.read_process(AB_PATH or 'ab', self.args())
-        except Exception:
-            print(_cperror.format_exc())
-            raise
-
-        for attr, name, pattern in self.parse_patterns:
-            val = re.search(pattern, self.output, re.MULTILINE)
-            if val:
-                val = val.group(1)
-                setattr(self, attr, val)
-            else:
-                setattr(self, attr, None)
-
-
-safe_threads = (25, 50, 100, 200, 400)
-if sys.platform in ('win32',):
-    # For some reason, ab crashes with > 50 threads on my Win2k laptop.
-    safe_threads = (10, 20, 30, 40, 50)
-
-
-def thread_report(path=SCRIPT_NAME + '/hello', concurrency=safe_threads):
-    sess = ABSession(path)
-    attrs, names, patterns = list(zip(*sess.parse_patterns))
-    avg = dict.fromkeys(attrs, 0.0)
-
-    yield ('threads',) + names
-    for c in concurrency:
-        sess.concurrency = c
-        sess.run()
-        row = [c]
-        for attr in attrs:
-            val = getattr(sess, attr)
-            if val is None:
-                print(sess.output)
-                row = None
-                break
-            val = float(val)
-            avg[attr] += float(val)
-            row.append(val)
-        if row:
-            yield row
-
-    # Add a row of averages.
-    yield ['Average'] + [str(avg[attr] / len(concurrency)) for attr in attrs]
-
-
-def size_report(sizes=(10, 100, 1000, 10000, 100000, 100000000),
-                concurrency=50):
-    sess = ABSession(concurrency=concurrency)
-    attrs, names, patterns = list(zip(*sess.parse_patterns))
-    yield ('bytes',) + names
-    for sz in sizes:
-        sess.path = '%s/sizer?size=%s' % (SCRIPT_NAME, sz)
-        sess.run()
-        yield [sz] + [getattr(sess, attr) for attr in attrs]
-
-
-def print_report(rows):
-    for row in rows:
-        print('')
-        for val in row:
-            sys.stdout.write(str(val).rjust(10) + ' | ')
-    print('')
-
-
-def run_standard_benchmarks():
-    print('')
-    print('Client Thread Report (1000 requests, 14 byte response body, '
-          '%s server threads):' % cherrypy.server.thread_pool)
-    print_report(thread_report())
-
-    print('')
-    print('Client Thread Report (1000 requests, 14 bytes via staticdir, '
-          '%s server threads):' % cherrypy.server.thread_pool)
-    print_report(thread_report('%s/static/index.html' % SCRIPT_NAME))
-
-    print('')
-    print('Size Report (1000 requests, 50 client threads, '
-          '%s server threads):' % cherrypy.server.thread_pool)
-    print_report(size_report())
-
-
-#                         modpython and other WSGI                         #
-
-def startup_modpython(req=None):
-    """Start the CherryPy app server in 'serverless' mode (for modpython/WSGI).
-    """
-    if cherrypy.engine.state == cherrypy._cpengine.STOPPED:
-        if req:
-            if 'nullreq' in req.get_options():
-                cherrypy.engine.request_class = NullRequest
-                cherrypy.engine.response_class = NullResponse
-            ab_opt = req.get_options().get('ab', '')
-            if ab_opt:
-                global AB_PATH
-                AB_PATH = ab_opt
-        cherrypy.engine.start()
-    if cherrypy.engine.state == cherrypy._cpengine.STARTING:
-        cherrypy.engine.wait()
-    return 0  # apache.OK
-
-
-def run_modpython(use_wsgi=False):
-    print('Starting mod_python...')
-    pyopts = []
-
-    # Pass the null and ab=path options through Apache
-    if '--null' in opts:
-        pyopts.append(('nullreq', ''))
-
-    if '--ab' in opts:
-        pyopts.append(('ab', opts['--ab']))
-
-    s = _cpmodpy.ModPythonServer
-    if use_wsgi:
-        pyopts.append(('wsgi.application', 'cherrypy::tree'))
-        pyopts.append(
-            ('wsgi.startup', 'cherrypy.test.benchmark::startup_modpython'))
-        handler = 'modpython_gateway::handler'
-        s = s(port=54583, opts=pyopts,
-              apache_path=APACHE_PATH, handler=handler)
-    else:
-        pyopts.append(
-            ('cherrypy.setup', 'cherrypy.test.benchmark::startup_modpython'))
-        s = s(port=54583, opts=pyopts, apache_path=APACHE_PATH)
-
-    try:
-        s.start()
-        run()
-    finally:
-        s.stop()
-
-
-if __name__ == '__main__':
-    init()
-
-    longopts = ['cpmodpy', 'modpython', 'null', 'notests',
-                'help', 'ab=', 'apache=']
-    try:
-        switches, args = getopt.getopt(sys.argv[1:], '', longopts)
-        opts = dict(switches)
-    except getopt.GetoptError:
-        print(__doc__)
-        sys.exit(2)
-
-    if '--help' in opts:
-        print(__doc__)
-        sys.exit(0)
-
-    if '--ab' in opts:
-        AB_PATH = opts['--ab']
-
-    if '--notests' in opts:
-        # Return without stopping the server, so that the pages
-        # can be tested from a standard web browser.
-        def run():
-            port = cherrypy.server.socket_port
-            print('You may now open http://127.0.0.1:%s%s/' %
-                  (port, SCRIPT_NAME))
-
-            if '--null' in opts:
-                print('Using null Request object')
-    else:
-        def run():
-            end = time.time() - start
-            print('Started in %s seconds' % end)
-            if '--null' in opts:
-                print('\nUsing null Request object')
-            try:
-                try:
-                    run_standard_benchmarks()
-                except Exception:
-                    print(_cperror.format_exc())
-                    raise
-            finally:
-                cherrypy.engine.exit()
-
-    print('Starting CherryPy app server...')
-
-    class NullWriter(object):
-
-        """Suppresses the printing of socket errors."""
-
-        def write(self, data):
-            pass
-    sys.stderr = NullWriter()
-
-    start = time.time()
-
-    if '--cpmodpy' in opts:
-        run_modpython()
-    elif '--modpython' in opts:
-        run_modpython(use_wsgi=True)
-    else:
-        if '--null' in opts:
-            cherrypy.server.request_class = NullRequest
-            cherrypy.server.response_class = NullResponse
-
-        cherrypy.engine.start_with_callback(run)
-        cherrypy.engine.block()
diff --git a/libraries/cherrypy/test/checkerdemo.py b/libraries/cherrypy/test/checkerdemo.py
deleted file mode 100644
index 3438bd0c..00000000
--- a/libraries/cherrypy/test/checkerdemo.py
+++ /dev/null
@@ -1,49 +0,0 @@
-"""Demonstration app for cherrypy.checker.
-
-This application is intentionally broken and badly designed.
-To demonstrate the output of the CherryPy Checker, simply execute
-this module.
-"""
-
-import os
-import cherrypy
-thisdir = os.path.dirname(os.path.abspath(__file__))
-
-
-class Root:
-    pass
-
-
-if __name__ == '__main__':
-    conf = {'/base': {'tools.staticdir.root': thisdir,
-                      # Obsolete key.
-                      'throw_errors': True,
-                      },
-            # This entry should be OK.
-            '/base/static': {'tools.staticdir.on': True,
-                             'tools.staticdir.dir': 'static'},
-            # Warn on missing folder.
-            '/base/js': {'tools.staticdir.on': True,
-                         'tools.staticdir.dir': 'js'},
-            # Warn on dir with an abs path even though we provide root.
-            '/base/static2': {'tools.staticdir.on': True,
-                              'tools.staticdir.dir': '/static'},
-            # Warn on dir with a relative path with no root.
-            '/static3': {'tools.staticdir.on': True,
-                         'tools.staticdir.dir': 'static'},
-            # Warn on unknown namespace
-            '/unknown': {'toobles.gzip.on': True},
-            # Warn special on cherrypy.<known ns>.*
-            '/cpknown': {'cherrypy.tools.encode.on': True},
-            # Warn on mismatched types
-            '/conftype': {'request.show_tracebacks': 14},
-            # Warn on unknown tool.
-            '/web': {'tools.unknown.on': True},
-            # Warn on server.* in app config.
-            '/app1': {'server.socket_host': '0.0.0.0'},
-            # Warn on 'localhost'
-            'global': {'server.socket_host': 'localhost'},
-            # Warn on '[name]'
-            '[/extra_brackets]': {},
-            }
-    cherrypy.quickstart(Root(), config=conf)
diff --git a/libraries/cherrypy/test/fastcgi.conf b/libraries/cherrypy/test/fastcgi.conf
deleted file mode 100644
index e5c5163c..00000000
--- a/libraries/cherrypy/test/fastcgi.conf
+++ /dev/null
@@ -1,18 +0,0 @@
-
-# Apache2 server conf file for testing CherryPy with mod_fastcgi.
-# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
-ServerRoot /usr/lib/apache2
-User #1000
-ErrorLog /usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/mod_fastcgi.error.log
-
-DocumentRoot "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test"
-ServerName 127.0.0.1
-Listen 8080
-LoadModule fastcgi_module modules/mod_fastcgi.so
-LoadModule rewrite_module modules/mod_rewrite.so
-
-Options +ExecCGI
-SetHandler fastcgi-script
-RewriteEngine On
-RewriteRule ^(.*)$ /fastcgi.pyc [L]
-FastCgiExternalServer "/usr/lib/python2.5/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000
diff --git a/libraries/cherrypy/test/fcgi.conf b/libraries/cherrypy/test/fcgi.conf
deleted file mode 100644
index 3062eb35..00000000
--- a/libraries/cherrypy/test/fcgi.conf
+++ /dev/null
@@ -1,14 +0,0 @@
-
-# Apache2 server conf file for testing CherryPy with mod_fcgid.
-
-DocumentRoot "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test"
-ServerName 127.0.0.1
-Listen 8080
-LoadModule fastcgi_module modules/mod_fastcgi.dll
-LoadModule rewrite_module modules/mod_rewrite.so
-
-Options ExecCGI
-SetHandler fastcgi-script
-RewriteEngine On
-RewriteRule ^(.*)$ /fastcgi.pyc [L]
-FastCgiExternalServer "/usr/lib/python2.6/site-packages/cproot/trunk/cherrypy/test/fastcgi.pyc" -host 127.0.0.1:4000
diff --git a/libraries/cherrypy/test/helper.py b/libraries/cherrypy/test/helper.py
deleted file mode 100644
index 01c5a0c0..00000000
--- a/libraries/cherrypy/test/helper.py
+++ /dev/null
@@ -1,542 +0,0 @@
-"""A library of helper functions for the CherryPy test suite."""
-
-import datetime
-import io
-import logging
-import os
-import re
-import subprocess
-import sys
-import time
-import unittest
-import warnings
-
-import portend
-import pytest
-import six
-
-from cheroot.test import webtest
-
-import cherrypy
-from cherrypy._cpcompat import text_or_bytes, HTTPSConnection, ntob
-from cherrypy.lib import httputil
-from cherrypy.lib import gctools
-
-log = logging.getLogger(__name__)
-thisdir = os.path.abspath(os.path.dirname(__file__))
-serverpem = os.path.join(os.getcwd(), thisdir, 'test.pem')
-
-
-class Supervisor(object):
-
-    """Base class for modeling and controlling servers during testing."""
-
-    def __init__(self, **kwargs):
-        for k, v in kwargs.items():
-            if k == 'port':
-                setattr(self, k, int(v))
-            setattr(self, k, v)
-
-
-def log_to_stderr(msg, level):
-    return sys.stderr.write(msg + os.linesep)
-
-
-class LocalSupervisor(Supervisor):
-
-    """Base class for modeling/controlling servers which run in the same
-    process.
-
-    When the server side runs in a different process, start/stop can dump all
-    state between each test module easily. When the server side runs in the
-    same process as the client, however, we have to do a bit more work to
-    ensure config and mounted apps are reset between tests.
-    """
-
-    using_apache = False
-    using_wsgi = False
-
-    def __init__(self, **kwargs):
-        for k, v in kwargs.items():
-            setattr(self, k, v)
-
-        cherrypy.server.httpserver = self.httpserver_class
-
-        # This is perhaps the wrong place for this call but this is the only
-        # place that i've found so far that I KNOW is early enough to set this.
-        cherrypy.config.update({'log.screen': False})
-        engine = cherrypy.engine
-        if hasattr(engine, 'signal_handler'):
-            engine.signal_handler.subscribe()
-        if hasattr(engine, 'console_control_handler'):
-            engine.console_control_handler.subscribe()
-
-    def start(self, modulename=None):
-        """Load and start the HTTP server."""
-        if modulename:
-            # Unhook httpserver so cherrypy.server.start() creates a new
-            # one (with config from setup_server, if declared).
-            cherrypy.server.httpserver = None
-
-        cherrypy.engine.start()
-
-        self.sync_apps()
-
-    def sync_apps(self):
-        """Tell the server about any apps which the setup functions mounted."""
-        pass
-
-    def stop(self):
-        td = getattr(self, 'teardown', None)
-        if td:
-            td()
-
-        cherrypy.engine.exit()
-
-        servers_copy = list(six.iteritems(getattr(cherrypy, 'servers', {})))
-        for name, server in servers_copy:
-            server.unsubscribe()
-            del cherrypy.servers[name]
-
-
-class NativeServerSupervisor(LocalSupervisor):
-
-    """Server supervisor for the builtin HTTP server."""
-
-    httpserver_class = 'cherrypy._cpnative_server.CPHTTPServer'
-    using_apache = False
-    using_wsgi = False
-
-    def __str__(self):
-        return 'Builtin HTTP Server on %s:%s' % (self.host, self.port)
-
-
-class LocalWSGISupervisor(LocalSupervisor):
-
-    """Server supervisor for the builtin WSGI server."""
-
-    httpserver_class = 'cherrypy._cpwsgi_server.CPWSGIServer'
-    using_apache = False
-    using_wsgi = True
-
-    def __str__(self):
-        return 'Builtin WSGI Server on %s:%s' % (self.host, self.port)
-
-    def sync_apps(self):
-        """Hook a new WSGI app into the origin server."""
-        cherrypy.server.httpserver.wsgi_app = self.get_app()
-
-    def get_app(self, app=None):
-        """Obtain a new (decorated) WSGI app to hook into the origin server."""
-        if app is None:
-            app = cherrypy.tree
-
-        if self.validate:
-            try:
-                from wsgiref import validate
-            except ImportError:
-                warnings.warn(
-                    'Error importing wsgiref. The validator will not run.')
-            else:
-                # wraps the app in the validator
-                app = validate.validator(app)
-
-        return app
-
-
-def get_cpmodpy_supervisor(**options):
-    from cherrypy.test import modpy
-    sup = modpy.ModPythonSupervisor(**options)
-    sup.template = modpy.conf_cpmodpy
-    return sup
-
-
-def get_modpygw_supervisor(**options):
-    from cherrypy.test import modpy
-    sup = modpy.ModPythonSupervisor(**options)
-    sup.template = modpy.conf_modpython_gateway
-    sup.using_wsgi = True
-    return sup
-
-
-def get_modwsgi_supervisor(**options):
-    from cherrypy.test import modwsgi
-    return modwsgi.ModWSGISupervisor(**options)
-
-
-def get_modfcgid_supervisor(**options):
-    from cherrypy.test import modfcgid
-    return modfcgid.ModFCGISupervisor(**options)
-
-
-def get_modfastcgi_supervisor(**options):
-    from cherrypy.test import modfastcgi
-    return modfastcgi.ModFCGISupervisor(**options)
-
-
-def get_wsgi_u_supervisor(**options):
-    cherrypy.server.wsgi_version = ('u', 0)
-    return LocalWSGISupervisor(**options)
-
-
-class CPWebCase(webtest.WebCase):
-
-    script_name = ''
-    scheme = 'http'
-
-    available_servers = {'wsgi': LocalWSGISupervisor,
-                         'wsgi_u': get_wsgi_u_supervisor,
-                         'native': NativeServerSupervisor,
-                         'cpmodpy': get_cpmodpy_supervisor,
-                         'modpygw': get_modpygw_supervisor,
-                         'modwsgi': get_modwsgi_supervisor,
-                         'modfcgid': get_modfcgid_supervisor,
-                         'modfastcgi': get_modfastcgi_supervisor,
-                         }
-    default_server = 'wsgi'
-
-    @classmethod
-    def _setup_server(cls, supervisor, conf):
-        v = sys.version.split()[0]
-        log.info('Python version used to run this test script: %s' % v)
-        log.info('CherryPy version: %s' % cherrypy.__version__)
-        if supervisor.scheme == 'https':
-            ssl = ' (ssl)'
-        else:
-            ssl = ''
-        log.info('HTTP server version: %s%s' % (supervisor.protocol, ssl))
-        log.info('PID: %s' % os.getpid())
-
-        cherrypy.server.using_apache = supervisor.using_apache
-        cherrypy.server.using_wsgi = supervisor.using_wsgi
-
-        if sys.platform[:4] == 'java':
-            cherrypy.config.update({'server.nodelay': False})
-
-        if isinstance(conf, text_or_bytes):
-            parser = cherrypy.lib.reprconf.Parser()
-            conf = parser.dict_from_file(conf).get('global', {})
-        else:
-            conf = conf or {}
-        baseconf = conf.copy()
-        baseconf.update({'server.socket_host': supervisor.host,
-                         'server.socket_port': supervisor.port,
-                         'server.protocol_version': supervisor.protocol,
-                         'environment': 'test_suite',
-                         })
-        if supervisor.scheme == 'https':
-            # baseconf['server.ssl_module'] = 'builtin'
-            baseconf['server.ssl_certificate'] = serverpem
-            baseconf['server.ssl_private_key'] = serverpem
-
-        # helper must be imported lazily so the coverage tool
-        # can run against module-level statements within cherrypy.
-        # Also, we have to do "from cherrypy.test import helper",
-        # exactly like each test module does, because a relative import
-        # would stick a second instance of webtest in sys.modules,
-        # and we wouldn't be able to globally override the port anymore.
-        if supervisor.scheme == 'https':
-            webtest.WebCase.HTTP_CONN = HTTPSConnection
-        return baseconf
-
-    @classmethod
-    def setup_class(cls):
-        ''
-        # Creates a server
-        conf = {
-            'scheme': 'http',
-            'protocol': 'HTTP/1.1',
-            'port': 54583,
-            'host': '127.0.0.1',
-            'validate': False,
-            'server': 'wsgi',
-        }
-        supervisor_factory = cls.available_servers.get(
-            conf.get('server', 'wsgi'))
-        if supervisor_factory is None:
-            raise RuntimeError('Unknown server in config: %s' % conf['server'])
-        supervisor = supervisor_factory(**conf)
-
-        # Copied from "run_test_suite"
-        cherrypy.config.reset()
-        baseconf = cls._setup_server(supervisor, conf)
-        cherrypy.config.update(baseconf)
-        setup_client()
-
-        if hasattr(cls, 'setup_server'):
-            # Clear the cherrypy tree and clear the wsgi server so that
-            # it can be updated with the new root
-            cherrypy.tree = cherrypy._cptree.Tree()
-            cherrypy.server.httpserver = None
-            cls.setup_server()
-            # Add a resource for verifying there are no refleaks
-            # to *every* test class.
-            cherrypy.tree.mount(gctools.GCRoot(), '/gc')
-            cls.do_gc_test = True
-            supervisor.start(cls.__module__)
-
-        cls.supervisor = supervisor
-
-    @classmethod
-    def teardown_class(cls):
-        ''
-        if hasattr(cls, 'setup_server'):
-            cls.supervisor.stop()
-
-    do_gc_test = False
-
-    def test_gc(self):
-        if not self.do_gc_test:
-            return
-
-        self.getPage('/gc/stats')
-        try:
-            self.assertBody('Statistics:')
-        except Exception:
-            'Failures occur intermittently. See #1420'
-
-    def prefix(self):
-        return self.script_name.rstrip('/')
-
-    def base(self):
-        if ((self.scheme == 'http' and self.PORT == 80) or
-                (self.scheme == 'https' and self.PORT == 443)):
-            port = ''
-        else:
-            port = ':%s' % self.PORT
-
-        return '%s://%s%s%s' % (self.scheme, self.HOST, port,
-                                self.script_name.rstrip('/'))
-
-    def exit(self):
-        sys.exit()
-
-    def getPage(self, url, headers=None, method='GET', body=None,
-                protocol=None, raise_subcls=None):
-        """Open the url. Return status, headers, body.
-
-        `raise_subcls` must be a tuple with the exceptions classes
-        or a single exception class that are not going to be considered
-        a socket.error regardless that they were are subclass of a
-        socket.error and therefore not considered for a connection retry.
-        """
-        if self.script_name:
-            url = httputil.urljoin(self.script_name, url)
-        return webtest.WebCase.getPage(self, url, headers, method, body,
-                                       protocol, raise_subcls)
-
-    def skip(self, msg='skipped '):
-        pytest.skip(msg)
-
-    def assertErrorPage(self, status, message=None, pattern=''):
-        """Compare the response body with a built in error page.
-
-        The function will optionally look for the regexp pattern,
-        within the exception embedded in the error page."""
-
-        # This will never contain a traceback
-        page = cherrypy._cperror.get_error_page(status, message=message)
-
-        # First, test the response body without checking the traceback.
-        # Stick a match-all group (.*) in to grab the traceback.
-        def esc(text):
-            return re.escape(ntob(text))
-        epage = re.escape(page)
-        epage = epage.replace(
-            esc('<pre id="traceback"></pre>'),
-            esc('<pre id="traceback">') + b'(.*)' + esc('</pre>'))
-        m = re.match(epage, self.body, re.DOTALL)
-        if not m:
-            self._handlewebError(
-                'Error page does not match; expected:\n' + page)
-            return
-
-        # Now test the pattern against the traceback
-        if pattern is None:
-            # Special-case None to mean that there should be *no* traceback.
-            if m and m.group(1):
-                self._handlewebError('Error page contains traceback')
-        else:
-            if (m is None) or (
-                not re.search(ntob(re.escape(pattern), self.encoding),
-                              m.group(1))):
-                msg = 'Error page does not contain %s in traceback'
-                self._handlewebError(msg % repr(pattern))
-
-    date_tolerance = 2
-
-    def assertEqualDates(self, dt1, dt2, seconds=None):
-        """Assert abs(dt1 - dt2) is within Y seconds."""
-        if seconds is None:
-            seconds = self.date_tolerance
-
-        if dt1 > dt2:
-            diff = dt1 - dt2
-        else:
-            diff = dt2 - dt1
-        if not diff < datetime.timedelta(seconds=seconds):
-            raise AssertionError('%r and %r are not within %r seconds.' %
-                                 (dt1, dt2, seconds))
-
-
-def _test_method_sorter(_, x, y):
-    """Monkeypatch the test sorter to always run test_gc last in each suite."""
-    if x == 'test_gc':
-        return 1
-    if y == 'test_gc':
-        return -1
-    if x > y:
-        return 1
-    if x < y:
-        return -1
-    return 0
-
-
-unittest.TestLoader.sortTestMethodsUsing = _test_method_sorter
-
-
-def setup_client():
-    """Set up the WebCase classes to match the server's socket settings."""
-    webtest.WebCase.PORT = cherrypy.server.socket_port
-    webtest.WebCase.HOST = cherrypy.server.socket_host
-    if cherrypy.server.ssl_certificate:
-        CPWebCase.scheme = 'https'
-
-# --------------------------- Spawning helpers --------------------------- #
-
-
-class CPProcess(object):
-
-    pid_file = os.path.join(thisdir, 'test.pid')
-    config_file = os.path.join(thisdir, 'test.conf')
-    config_template = """[global]
-server.socket_host: '%(host)s'
-server.socket_port: %(port)s
-checker.on: False
-log.screen: False
-log.error_file: r'%(error_log)s'
-log.access_file: r'%(access_log)s'
-%(ssl)s
-%(extra)s
-"""
-    error_log = os.path.join(thisdir, 'test.error.log')
-    access_log = os.path.join(thisdir, 'test.access.log')
-
-    def __init__(self, wait=False, daemonize=False, ssl=False,
-                 socket_host=None, socket_port=None):
-        self.wait = wait
-        self.daemonize = daemonize
-        self.ssl = ssl
-        self.host = socket_host or cherrypy.server.socket_host
-        self.port = socket_port or cherrypy.server.socket_port
-
-    def write_conf(self, extra=''):
-        if self.ssl:
-            serverpem = os.path.join(thisdir, 'test.pem')
-            ssl = """
-server.ssl_certificate: r'%s'
-server.ssl_private_key: r'%s'
-""" % (serverpem, serverpem)
-        else:
-            ssl = ''
-
-        conf = self.config_template % {
-            'host': self.host,
-            'port': self.port,
-            'error_log': self.error_log,
-            'access_log': self.access_log,
-            'ssl': ssl,
-            'extra': extra,
-        }
-        with io.open(self.config_file, 'w', encoding='utf-8') as f:
-            f.write(six.text_type(conf))
-
-    def start(self, imports=None):
-        """Start cherryd in a subprocess."""
-        portend.free(self.host, self.port, timeout=1)
-
-        args = [
-            '-m',
-            'cherrypy',
-            '-c', self.config_file,
-            '-p', self.pid_file,
-        ]
-        r"""
-        Command for running cherryd server with autoreload enabled
-
-        Using
-
-        ```
-        ['-c',
-         "__requires__ = 'CherryPy'; \
-         import pkg_resources, re, sys; \
-         sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]); \
-         sys.exit(\
-            pkg_resources.load_entry_point(\
-                'CherryPy', 'console_scripts', 'cherryd')())"]
-        ```
-
-        doesn't work as it's impossible to reconstruct the `-c`'s contents.
-        Ref: https://github.com/cherrypy/cherrypy/issues/1545
-        """
-
-        if not isinstance(imports, (list, tuple)):
-            imports = [imports]
-        for i in imports:
-            if i:
-                args.append('-i')
-                args.append(i)
-
-        if self.daemonize:
-            args.append('-d')
-
-        env = os.environ.copy()
-        # Make sure we import the cherrypy package in which this module is
-        # defined.
-        grandparentdir = os.path.abspath(os.path.join(thisdir, '..', '..'))
-        if env.get('PYTHONPATH', ''):
-            env['PYTHONPATH'] = os.pathsep.join(
-                (grandparentdir, env['PYTHONPATH']))
-        else:
-            env['PYTHONPATH'] = grandparentdir
-        self._proc = subprocess.Popen([sys.executable] + args, env=env)
-        if self.wait:
-            self.exit_code = self._proc.wait()
-        else:
-            portend.occupied(self.host, self.port, timeout=5)
-
-        # Give the engine a wee bit more time to finish STARTING
-        if self.daemonize:
-            time.sleep(2)
-        else:
-            time.sleep(1)
-
-    def get_pid(self):
-        if self.daemonize:
-            return int(open(self.pid_file, 'rb').read())
-        return self._proc.pid
-
-    def join(self):
-        """Wait for the process to exit."""
-        if self.daemonize:
-            return self._join_daemon()
-        self._proc.wait()
-
-    def _join_daemon(self):
-        try:
-            try:
-                # Mac, UNIX
-                os.wait()
-            except AttributeError:
-                # Windows
-                try:
-                    pid = self.get_pid()
-                except IOError:
-                    # Assume the subprocess deleted the pidfile on shutdown.
-                    pass
-                else:
-                    os.waitpid(pid, 0)
-        except OSError:
-            x = sys.exc_info()[1]
-            if x.args != (10, 'No child processes'):
-                raise
diff --git a/libraries/cherrypy/test/logtest.py b/libraries/cherrypy/test/logtest.py
deleted file mode 100644
index ed8f1540..00000000
--- a/libraries/cherrypy/test/logtest.py
+++ /dev/null
@@ -1,228 +0,0 @@
-"""logtest, a unittest.TestCase helper for testing log output."""
-
-import sys
-import time
-from uuid import UUID
-
-import six
-
-from cherrypy._cpcompat import text_or_bytes, ntob
-
-
-try:
-    # On Windows, msvcrt.getch reads a single char without output.
-    import msvcrt
-
-    def getchar():
-        return msvcrt.getch()
-except ImportError:
-    # Unix getchr
-    import tty
-    import termios
-
-    def getchar():
-        fd = sys.stdin.fileno()
-        old_settings = termios.tcgetattr(fd)
-        try:
-            tty.setraw(sys.stdin.fileno())
-            ch = sys.stdin.read(1)
-        finally:
-            termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
-        return ch
-
-
-class LogCase(object):
-
-    """unittest.TestCase mixin for testing log messages.
-
-    logfile: a filename for the desired log. Yes, I know modes are evil,
-        but it makes the test functions so much cleaner to set this once.
-
-    lastmarker: the last marker in the log. This can be used to search for
-        messages since the last marker.
-
-    markerPrefix: a string with which to prefix log markers. This should be
-        unique enough from normal log output to use for marker identification.
-    """
-
-    logfile = None
-    lastmarker = None
-    markerPrefix = b'test suite marker: '
-
-    def _handleLogError(self, msg, data, marker, pattern):
-        print('')
-        print('    ERROR: %s' % msg)
-
-        if not self.interactive:
-            raise self.failureException(msg)
-
-        p = ('    Show: '
-             '[L]og [M]arker [P]attern; '
-             '[I]gnore, [R]aise, or sys.e[X]it >> ')
-        sys.stdout.write(p + ' ')
-        # ARGH
-        sys.stdout.flush()
-        while True:
-            i = getchar().upper()
-            if i not in 'MPLIRX':
-                continue
-            print(i.upper())  # Also prints new line
-            if i == 'L':
-                for x, line in enumerate(data):
-                    if (x + 1) % self.console_height == 0:
-                        # The \r and comma should make the next line overwrite
-                        sys.stdout.write('<-- More -->\r ')
-                        m = getchar().lower()
-                        # Erase our "More" prompt
-                        sys.stdout.write('            \r ')
-                        if m == 'q':
-                            break
-                    print(line.rstrip())
-            elif i == 'M':
-                print(repr(marker or self.lastmarker))
-            elif i == 'P':
-                print(repr(pattern))
-            elif i == 'I':
-                # return without raising the normal exception
-                return
-            elif i == 'R':
-                raise self.failureException(msg)
-            elif i == 'X':
-                self.exit()
-            sys.stdout.write(p + ' ')
-
-    def exit(self):
-        sys.exit()
-
-    def emptyLog(self):
-        """Overwrite self.logfile with 0 bytes."""
-        open(self.logfile, 'wb').write('')
-
-    def markLog(self, key=None):
-        """Insert a marker line into the log and set self.lastmarker."""
-        if key is None:
-            key = str(time.time())
-        self.lastmarker = key
-
-        open(self.logfile, 'ab+').write(
-            ntob('%s%s\n' % (self.markerPrefix, key), 'utf-8'))
-
-    def _read_marked_region(self, marker=None):
-        """Return lines from self.logfile in the marked region.
-
-        If marker is None, self.lastmarker is used. If the log hasn't
-        been marked (using self.markLog), the entire log will be returned.
-        """
-# Give the logger time to finish writing?
-# time.sleep(0.5)
-
-        logfile = self.logfile
-        marker = marker or self.lastmarker
-        if marker is None:
-            return open(logfile, 'rb').readlines()
-
-        if isinstance(marker, six.text_type):
-            marker = marker.encode('utf-8')
-        data = []
-        in_region = False
-        for line in open(logfile, 'rb'):
-            if in_region:
-                if line.startswith(self.markerPrefix) and marker not in line:
-                    break
-                else:
-                    data.append(line)
-            elif marker in line:
-                in_region = True
-        return data
-
-    def assertInLog(self, line, marker=None):
-        """Fail if the given (partial) line is not in the log.
-
-        The log will be searched from the given marker to the next marker.
-        If marker is None, self.lastmarker is used. If the log hasn't
-        been marked (using self.markLog), the entire log will be searched.
-        """
-        data = self._read_marked_region(marker)
-        for logline in data:
-            if line in logline:
-                return
-        msg = '%r not found in log' % line
-        self._handleLogError(msg, data, marker, line)
-
-    def assertNotInLog(self, line, marker=None):
-        """Fail if the given (partial) line is in the log.
-
-        The log will be searched from the given marker to the next marker.
-        If marker is None, self.lastmarker is used. If the log hasn't
-        been marked (using self.markLog), the entire log will be searched.
-        """
-        data = self._read_marked_region(marker)
-        for logline in data:
-            if line in logline:
-                msg = '%r found in log' % line
-                self._handleLogError(msg, data, marker, line)
-
-    def assertValidUUIDv4(self, marker=None):
-        """Fail if the given UUIDv4 is not valid.
-
-        The log will be searched from the given marker to the next marker.
-        If marker is None, self.lastmarker is used. If the log hasn't
-        been marked (using self.markLog), the entire log will be searched.
-        """
-        data = self._read_marked_region(marker)
-        data = [
-            chunk.decode('utf-8').rstrip('\n').rstrip('\r')
-            for chunk in data
-        ]
-        for log_chunk in data:
-            try:
-                uuid_log = data[-1]
-                uuid_obj = UUID(uuid_log, version=4)
-            except (TypeError, ValueError):
-                pass  # it might be in other chunk
-            else:
-                if str(uuid_obj) == uuid_log:
-                    return
-                msg = '%r is not a valid UUIDv4' % uuid_log
-                self._handleLogError(msg, data, marker, log_chunk)
-
-        msg = 'UUIDv4 not found in log'
-        self._handleLogError(msg, data, marker, log_chunk)
-
-    def assertLog(self, sliceargs, lines, marker=None):
-        """Fail if log.readlines()[sliceargs] is not contained in 'lines'.
-
-        The log will be searched from the given marker to the next marker.
-        If marker is None, self.lastmarker is used. If the log hasn't
-        been marked (using self.markLog), the entire log will be searched.
-        """
-        data = self._read_marked_region(marker)
-        if isinstance(sliceargs, int):
-            # Single arg. Use __getitem__ and allow lines to be str or list.
-            if isinstance(lines, (tuple, list)):
-                lines = lines[0]
-            if isinstance(lines, six.text_type):
-                lines = lines.encode('utf-8')
-            if lines not in data[sliceargs]:
-                msg = '%r not found on log line %r' % (lines, sliceargs)
-                self._handleLogError(
-                    msg,
-                    [data[sliceargs], '--EXTRA CONTEXT--'] + data[
-                        sliceargs + 1:sliceargs + 6],
-                    marker,
-                    lines)
-        else:
-            # Multiple args. Use __getslice__ and require lines to be list.
-            if isinstance(lines, tuple):
-                lines = list(lines)
-            elif isinstance(lines, text_or_bytes):
-                raise TypeError("The 'lines' arg must be a list when "
-                                "'sliceargs' is a tuple.")
-
-            start, stop = sliceargs
-            for line, logline in zip(lines, data[start:stop]):
-                if isinstance(line, six.text_type):
-                    line = line.encode('utf-8')
-                if line not in logline:
-                    msg = '%r not found in log' % line
-                    self._handleLogError(msg, data[start:stop], marker, line)
diff --git a/libraries/cherrypy/test/modfastcgi.py b/libraries/cherrypy/test/modfastcgi.py
deleted file mode 100644
index 79ec3d18..00000000
--- a/libraries/cherrypy/test/modfastcgi.py
+++ /dev/null
@@ -1,136 +0,0 @@
-"""Wrapper for mod_fastcgi, for use as a CherryPy HTTP server when testing.
-
-To autostart fastcgi, the "apache" executable or script must be
-on your system path, or you must override the global APACHE_PATH.
-On some platforms, "apache" may be called "apachectl", "apache2ctl",
-or "httpd"--create a symlink to them if needed.
-
-You'll also need the WSGIServer from flup.servers.
-See http://projects.amor.org/misc/wiki/ModPythonGateway
-
-
-KNOWN BUGS
-==========
-
-1. Apache processes Range headers automatically; CherryPy's truncated
-    output is then truncated again by Apache. See test_core.testRanges.
-    This was worked around in http://www.cherrypy.org/changeset/1319.
-2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
-    See test_core.testHTTPMethods.
-3. Max request header and body settings do not work with Apache.
-4. Apache replaces status "reason phrases" automatically. For example,
-    CherryPy may set "304 Not modified" but Apache will write out
-    "304 Not Modified" (capital "M").
-5. Apache does not allow custom error codes as per the spec.
-6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
-    Request-URI too early.
-7. mod_python will not read request bodies which use the "chunked"
-    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
-    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
-    mod_python's requestobject.c).
-8. Apache will output a "Content-Length: 0" response header even if there's
-    no response entity body. This isn't really a bug; it just differs from
-    the CherryPy default.
-"""
-
-import os
-import re
-
-import cherrypy
-from cherrypy.process import servers
-from cherrypy.test import helper
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-def read_process(cmd, args=''):
-    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
-    try:
-        firstline = pipeout.readline()
-        if (re.search(r'(not recognized|No such file|not found)', firstline,
-                      re.IGNORECASE)):
-            raise IOError('%s must be on your system path.' % cmd)
-        output = firstline + pipeout.read()
-    finally:
-        pipeout.close()
-    return output
-
-
-APACHE_PATH = 'apache2ctl'
-CONF_PATH = 'fastcgi.conf'
-
-conf_fastcgi = """
-# Apache2 server conf file for testing CherryPy with mod_fastcgi.
-# fumanchu: I had to hard-code paths due to crazy Debian layouts :(
-ServerRoot /usr/lib/apache2
-User #1000
-ErrorLog %(root)s/mod_fastcgi.error.log
-
-DocumentRoot "%(root)s"
-ServerName 127.0.0.1
-Listen %(port)s
-LoadModule fastcgi_module modules/mod_fastcgi.so
-LoadModule rewrite_module modules/mod_rewrite.so
-
-Options +ExecCGI
-SetHandler fastcgi-script
-RewriteEngine On
-RewriteRule ^(.*)$ /fastcgi.pyc [L]
-FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
-"""
-
-
-def erase_script_name(environ, start_response):
-    environ['SCRIPT_NAME'] = ''
-    return cherrypy.tree(environ, start_response)
-
-
-class ModFCGISupervisor(helper.LocalWSGISupervisor):
-
-    httpserver_class = 'cherrypy.process.servers.FlupFCGIServer'
-    using_apache = True
-    using_wsgi = True
-    template = conf_fastcgi
-
-    def __str__(self):
-        return 'FCGI Server on %s:%s' % (self.host, self.port)
-
-    def start(self, modulename):
-        cherrypy.server.httpserver = servers.FlupFCGIServer(
-            application=erase_script_name, bindAddress=('127.0.0.1', 4000))
-        cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
-        cherrypy.server.socket_port = 4000
-        # For FCGI, we both start apache...
-        self.start_apache()
-        # ...and our local server
-        cherrypy.engine.start()
-        self.sync_apps()
-
-    def start_apache(self):
-        fcgiconf = CONF_PATH
-        if not os.path.isabs(fcgiconf):
-            fcgiconf = os.path.join(curdir, fcgiconf)
-
-        # Write the Apache conf file.
-        f = open(fcgiconf, 'wb')
-        try:
-            server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
-            output = self.template % {'port': self.port, 'root': curdir,
-                                      'server': server}
-            output = output.replace('\r\n', '\n')
-            f.write(output)
-        finally:
-            f.close()
-
-        result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
-        if result:
-            print(result)
-
-    def stop(self):
-        """Gracefully shutdown a server that is serving forever."""
-        read_process(APACHE_PATH, '-k stop')
-        helper.LocalWSGISupervisor.stop(self)
-
-    def sync_apps(self):
-        cherrypy.server.httpserver.fcgiserver.application = self.get_app(
-            erase_script_name)
diff --git a/libraries/cherrypy/test/modfcgid.py b/libraries/cherrypy/test/modfcgid.py
deleted file mode 100644
index d101bd67..00000000
--- a/libraries/cherrypy/test/modfcgid.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""Wrapper for mod_fcgid, for use as a CherryPy HTTP server when testing.
-
-To autostart fcgid, the "apache" executable or script must be
-on your system path, or you must override the global APACHE_PATH.
-On some platforms, "apache" may be called "apachectl", "apache2ctl",
-or "httpd"--create a symlink to them if needed.
-
-You'll also need the WSGIServer from flup.servers.
-See http://projects.amor.org/misc/wiki/ModPythonGateway
-
-
-KNOWN BUGS
-==========
-
-1. Apache processes Range headers automatically; CherryPy's truncated
-    output is then truncated again by Apache. See test_core.testRanges.
-    This was worked around in http://www.cherrypy.org/changeset/1319.
-2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
-    See test_core.testHTTPMethods.
-3. Max request header and body settings do not work with Apache.
-4. Apache replaces status "reason phrases" automatically. For example,
-    CherryPy may set "304 Not modified" but Apache will write out
-    "304 Not Modified" (capital "M").
-5. Apache does not allow custom error codes as per the spec.
-6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
-    Request-URI too early.
-7. mod_python will not read request bodies which use the "chunked"
-    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
-    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
-    mod_python's requestobject.c).
-8. Apache will output a "Content-Length: 0" response header even if there's
-    no response entity body. This isn't really a bug; it just differs from
-    the CherryPy default.
-"""
-
-import os
-import re
-
-import cherrypy
-from cherrypy._cpcompat import ntob
-from cherrypy.process import servers
-from cherrypy.test import helper
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-def read_process(cmd, args=''):
-    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
-    try:
-        firstline = pipeout.readline()
-        if (re.search(r'(not recognized|No such file|not found)', firstline,
-                      re.IGNORECASE)):
-            raise IOError('%s must be on your system path.' % cmd)
-        output = firstline + pipeout.read()
-    finally:
-        pipeout.close()
-    return output
-
-
-APACHE_PATH = 'httpd'
-CONF_PATH = 'fcgi.conf'
-
-conf_fcgid = """
-# Apache2 server conf file for testing CherryPy with mod_fcgid.
-
-DocumentRoot "%(root)s"
-ServerName 127.0.0.1
-Listen %(port)s
-LoadModule fastcgi_module modules/mod_fastcgi.dll
-LoadModule rewrite_module modules/mod_rewrite.so
-
-Options ExecCGI
-SetHandler fastcgi-script
-RewriteEngine On
-RewriteRule ^(.*)$ /fastcgi.pyc [L]
-FastCgiExternalServer "%(server)s" -host 127.0.0.1:4000
-"""
-
-
-class ModFCGISupervisor(helper.LocalSupervisor):
-
-    using_apache = True
-    using_wsgi = True
-    template = conf_fcgid
-
-    def __str__(self):
-        return 'FCGI Server on %s:%s' % (self.host, self.port)
-
-    def start(self, modulename):
-        cherrypy.server.httpserver = servers.FlupFCGIServer(
-            application=cherrypy.tree, bindAddress=('127.0.0.1', 4000))
-        cherrypy.server.httpserver.bind_addr = ('127.0.0.1', 4000)
-        # For FCGI, we both start apache...
-        self.start_apache()
-        # ...and our local server
-        helper.LocalServer.start(self, modulename)
-
-    def start_apache(self):
-        fcgiconf = CONF_PATH
-        if not os.path.isabs(fcgiconf):
-            fcgiconf = os.path.join(curdir, fcgiconf)
-
-        # Write the Apache conf file.
-        f = open(fcgiconf, 'wb')
-        try:
-            server = repr(os.path.join(curdir, 'fastcgi.pyc'))[1:-1]
-            output = self.template % {'port': self.port, 'root': curdir,
-                                      'server': server}
-            output = ntob(output.replace('\r\n', '\n'))
-            f.write(output)
-        finally:
-            f.close()
-
-        result = read_process(APACHE_PATH, '-k start -f %s' % fcgiconf)
-        if result:
-            print(result)
-
-    def stop(self):
-        """Gracefully shutdown a server that is serving forever."""
-        read_process(APACHE_PATH, '-k stop')
-        helper.LocalServer.stop(self)
-
-    def sync_apps(self):
-        cherrypy.server.httpserver.fcgiserver.application = self.get_app()
diff --git a/libraries/cherrypy/test/modpy.py b/libraries/cherrypy/test/modpy.py
deleted file mode 100644
index 7c288d2c..00000000
--- a/libraries/cherrypy/test/modpy.py
+++ /dev/null
@@ -1,164 +0,0 @@
-"""Wrapper for mod_python, for use as a CherryPy HTTP server when testing.
-
-To autostart modpython, the "apache" executable or script must be
-on your system path, or you must override the global APACHE_PATH.
-On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
-create a symlink to them if needed.
-
-If you wish to test the WSGI interface instead of our _cpmodpy interface,
-you also need the 'modpython_gateway' module at:
-http://projects.amor.org/misc/wiki/ModPythonGateway
-
-
-KNOWN BUGS
-==========
-
-1. Apache processes Range headers automatically; CherryPy's truncated
-    output is then truncated again by Apache. See test_core.testRanges.
-    This was worked around in http://www.cherrypy.org/changeset/1319.
-2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
-    See test_core.testHTTPMethods.
-3. Max request header and body settings do not work with Apache.
-4. Apache replaces status "reason phrases" automatically. For example,
-    CherryPy may set "304 Not modified" but Apache will write out
-    "304 Not Modified" (capital "M").
-5. Apache does not allow custom error codes as per the spec.
-6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
-    Request-URI too early.
-7. mod_python will not read request bodies which use the "chunked"
-    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
-    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
-    mod_python's requestobject.c).
-8. Apache will output a "Content-Length: 0" response header even if there's
-    no response entity body. This isn't really a bug; it just differs from
-    the CherryPy default.
-"""
-
-import os
-import re
-
-import cherrypy
-from cherrypy.test import helper
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-def read_process(cmd, args=''):
-    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
-    try:
-        firstline = pipeout.readline()
-        if (re.search(r'(not recognized|No such file|not found)', firstline,
-                      re.IGNORECASE)):
-            raise IOError('%s must be on your system path.' % cmd)
-        output = firstline + pipeout.read()
-    finally:
-        pipeout.close()
-    return output
-
-
-APACHE_PATH = 'httpd'
-CONF_PATH = 'test_mp.conf'
-
-conf_modpython_gateway = """
-# Apache2 server conf file for testing CherryPy with modpython_gateway.
-
-ServerName 127.0.0.1
-DocumentRoot "/"
-Listen %(port)s
-LoadModule python_module modules/mod_python.so
-
-SetHandler python-program
-PythonFixupHandler cherrypy.test.modpy::wsgisetup
-PythonOption testmod %(modulename)s
-PythonHandler modpython_gateway::handler
-PythonOption wsgi.application cherrypy::tree
-PythonOption socket_host %(host)s
-PythonDebug On
-"""
-
-conf_cpmodpy = """
-# Apache2 server conf file for testing CherryPy with _cpmodpy.
-
-ServerName 127.0.0.1
-DocumentRoot "/"
-Listen %(port)s
-LoadModule python_module modules/mod_python.so
-
-SetHandler python-program
-PythonFixupHandler cherrypy.test.modpy::cpmodpysetup
-PythonHandler cherrypy._cpmodpy::handler
-PythonOption cherrypy.setup cherrypy.test.%(modulename)s::setup_server
-PythonOption socket_host %(host)s
-PythonDebug On
-"""
-
-
-class ModPythonSupervisor(helper.Supervisor):
-
-    using_apache = True
-    using_wsgi = False
-    template = None
-
-    def __str__(self):
-        return 'ModPython Server on %s:%s' % (self.host, self.port)
-
-    def start(self, modulename):
-        mpconf = CONF_PATH
-        if not os.path.isabs(mpconf):
-            mpconf = os.path.join(curdir, mpconf)
-
-        f = open(mpconf, 'wb')
-        try:
-            f.write(self.template %
-                    {'port': self.port, 'modulename': modulename,
-                     'host': self.host})
-        finally:
-            f.close()
-
-        result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
-        if result:
-            print(result)
-
-    def stop(self):
-        """Gracefully shutdown a server that is serving forever."""
-        read_process(APACHE_PATH, '-k stop')
-
-
-loaded = False
-
-
-def wsgisetup(req):
-    global loaded
-    if not loaded:
-        loaded = True
-        options = req.get_options()
-
-        cherrypy.config.update({
-            'log.error_file': os.path.join(curdir, 'test.log'),
-            'environment': 'test_suite',
-            'server.socket_host': options['socket_host'],
-        })
-
-        modname = options['testmod']
-        mod = __import__(modname, globals(), locals(), [''])
-        mod.setup_server()
-
-        cherrypy.server.unsubscribe()
-        cherrypy.engine.start()
-    from mod_python import apache
-    return apache.OK
-
-
-def cpmodpysetup(req):
-    global loaded
-    if not loaded:
-        loaded = True
-        options = req.get_options()
-
-        cherrypy.config.update({
-            'log.error_file': os.path.join(curdir, 'test.log'),
-            'environment': 'test_suite',
-            'server.socket_host': options['socket_host'],
-        })
-    from mod_python import apache
-    return apache.OK
diff --git a/libraries/cherrypy/test/modwsgi.py b/libraries/cherrypy/test/modwsgi.py
deleted file mode 100644
index f558e223..00000000
--- a/libraries/cherrypy/test/modwsgi.py
+++ /dev/null
@@ -1,154 +0,0 @@
-"""Wrapper for mod_wsgi, for use as a CherryPy HTTP server.
-
-To autostart modwsgi, the "apache" executable or script must be
-on your system path, or you must override the global APACHE_PATH.
-On some platforms, "apache" may be called "apachectl" or "apache2ctl"--
-create a symlink to them if needed.
-
-
-KNOWN BUGS
-==========
-
-##1. Apache processes Range headers automatically; CherryPy's truncated
-##    output is then truncated again by Apache. See test_core.testRanges.
-##    This was worked around in http://www.cherrypy.org/changeset/1319.
-2. Apache does not allow custom HTTP methods like CONNECT as per the spec.
-    See test_core.testHTTPMethods.
-3. Max request header and body settings do not work with Apache.
-##4. Apache replaces status "reason phrases" automatically. For example,
-##    CherryPy may set "304 Not modified" but Apache will write out
-##    "304 Not Modified" (capital "M").
-##5. Apache does not allow custom error codes as per the spec.
-##6. Apache (or perhaps modpython, or modpython_gateway) unquotes %xx in the
-##    Request-URI too early.
-7. mod_wsgi will not read request bodies which use the "chunked"
-    transfer-coding (it passes REQUEST_CHUNKED_ERROR to ap_setup_client_block
-    instead of REQUEST_CHUNKED_DECHUNK, see Apache2's http_protocol.c and
-    mod_python's requestobject.c).
-8. When responding with 204 No Content, mod_wsgi adds a Content-Length
-    header for you.
-9. When an error is raised, mod_wsgi has no facility for printing a
-    traceback as the response content (it's sent to the Apache log instead).
-10. Startup and shutdown of Apache when running mod_wsgi seems slow.
-"""
-
-import os
-import re
-import sys
-import time
-
-import portend
-
-from cheroot.test import webtest
-
-import cherrypy
-from cherrypy.test import helper
-
-curdir = os.path.abspath(os.path.dirname(__file__))
-
-
-def read_process(cmd, args=''):
-    pipein, pipeout = os.popen4('%s %s' % (cmd, args))
-    try:
-        firstline = pipeout.readline()
-        if (re.search(r'(not recognized|No such file|not found)', firstline,
-                      re.IGNORECASE)):
-            raise IOError('%s must be on your system path.' % cmd)
-        output = firstline + pipeout.read()
-    finally:
-        pipeout.close()
-    return output
-
-
-if sys.platform == 'win32':
-    APACHE_PATH = 'httpd'
-else:
-    APACHE_PATH = 'apache'
-
-CONF_PATH = 'test_mw.conf'
-
-conf_modwsgi = r"""
-# Apache2 server conf file for testing CherryPy with modpython_gateway.
-
-ServerName 127.0.0.1
-DocumentRoot "/"
-Listen %(port)s
-
-AllowEncodedSlashes On
-LoadModule rewrite_module modules/mod_rewrite.so
-RewriteEngine on
-RewriteMap escaping int:escape
-
-LoadModule log_config_module modules/mod_log_config.so
-LogFormat "%%h %%l %%u %%t \"%%r\" %%>s %%b \"%%{Referer}i\" \"%%{User-agent}i\"" combined
-CustomLog "%(curdir)s/apache.access.log" combined
-ErrorLog "%(curdir)s/apache.error.log"
-LogLevel debug
-
-LoadModule wsgi_module modules/mod_wsgi.so
-LoadModule env_module modules/mod_env.so
-
-WSGIScriptAlias / "%(curdir)s/modwsgi.py"
-SetEnv testmod %(testmod)s
-""" # noqa E501
-
-
-class ModWSGISupervisor(helper.Supervisor):
-
-    """Server Controller for ModWSGI and CherryPy."""
-
-    using_apache = True
-    using_wsgi = True
-    template = conf_modwsgi
-
-    def __str__(self):
-        return 'ModWSGI Server on %s:%s' % (self.host, self.port)
-
-    def start(self, modulename):
-        mpconf = CONF_PATH
-        if not os.path.isabs(mpconf):
-            mpconf = os.path.join(curdir, mpconf)
-
-        f = open(mpconf, 'wb')
-        try:
-            output = (self.template %
-                      {'port': self.port, 'testmod': modulename,
-                       'curdir': curdir})
-            f.write(output)
-        finally:
-            f.close()
-
-        result = read_process(APACHE_PATH, '-k start -f %s' % mpconf)
-        if result:
-            print(result)
-
-        # Make a request so mod_wsgi starts up our app.
-        # If we don't, concurrent initial requests will 404.
-        portend.occupied('127.0.0.1', self.port, timeout=5)
-        webtest.openURL('/ihopetheresnodefault', port=self.port)
-        time.sleep(1)
-
-    def stop(self):
-        """Gracefully shutdown a server that is serving forever."""
-        read_process(APACHE_PATH, '-k stop')
-
-
-loaded = False
-
-
-def application(environ, start_response):
-    global loaded
-    if not loaded:
-        loaded = True
-        modname = 'cherrypy.test.' + environ['testmod']
-        mod = __import__(modname, globals(), locals(), [''])
-        mod.setup_server()
-
-        cherrypy.config.update({
-            'log.error_file': os.path.join(curdir, 'test.error.log'),
-            'log.access_file': os.path.join(curdir, 'test.access.log'),
-            'environment': 'test_suite',
-            'engine.SIGHUP': None,
-            'engine.SIGTERM': None,
-        })
-    return cherrypy.tree(environ, start_response)
diff --git a/libraries/cherrypy/test/sessiondemo.py b/libraries/cherrypy/test/sessiondemo.py
deleted file mode 100644
index 8226c1b9..00000000
--- a/libraries/cherrypy/test/sessiondemo.py
+++ /dev/null
@@ -1,161 +0,0 @@
-#!/usr/bin/python
-"""A session demonstration app."""
-
-import calendar
-from datetime import datetime
-import sys
-
-import six
-
-import cherrypy
-from cherrypy.lib import sessions
-
-
-page = """
-<html>
-<head>
-<style type='text/css'>
-table { border-collapse: collapse; border: 1px solid #663333; }
-th { text-align: right; background-color: #663333; color: white; padding: 0.5em; }
-td { white-space: pre-wrap; font-family: monospace; padding: 0.5em;
-     border: 1px solid #663333; }
-.warn { font-family: serif; color: #990000; }
-</style>
-<script type="text/javascript">
-<!--
-function twodigit(d) { return d < 10 ? "0" + d : d; }
-function formattime(t) {
-    var month = t.getUTCMonth() + 1;
-    var day = t.getUTCDate();
-    var year = t.getUTCFullYear();
-    var hours = t.getUTCHours();
-    var minutes = t.getUTCMinutes();
-    return (year + "/" + twodigit(month) + "/" + twodigit(day) + " " +
-            hours + ":" + twodigit(minutes) + " UTC");
-}
-
-function interval(s) {
-    // Return the given interval (in seconds) as an English phrase
-    var seconds = s %% 60;
-    s = Math.floor(s / 60);
-    var minutes = s %% 60;
-    s = Math.floor(s / 60);
-    var hours = s %% 24;
-    var v = twodigit(hours) + ":" + twodigit(minutes) + ":" + twodigit(seconds);
-    var days = Math.floor(s / 24);
-    if (days != 0) v = days + ' days, ' + v;
-    return v;
-}
-
-var fudge_seconds = 5;
-
-function init() {
-    // Set the content of the 'btime' cell.
-    var currentTime = new Date();
-    var bunixtime = Math.floor(currentTime.getTime() / 1000);
-
-    var v = formattime(currentTime);
-    v += " (Unix time: " + bunixtime + ")";
-
-    var diff = Math.abs(%(serverunixtime)s - bunixtime);
-    if (diff > fudge_seconds) v += "<p class='warn'>Browser and Server times disagree.</p>";
-
-    document.getElementById('btime').innerHTML = v;
-
-    // Warn if response cookie expires is not close to one hour in the future.
-    // Yes, we want this to happen when wit hit the 'Expire' link, too.
-    var expires = Date.parse("%(expires)s") / 1000;
-    var onehour = (60 * 60);
-    if (Math.abs(expires - (bunixtime + onehour)) > fudge_seconds) {
-        diff = Math.floor(expires - bunixtime);
-        if (expires > (bunixtime + onehour)) {
-            var msg = "Response cookie 'expires' date is " + interval(diff) + " in the future.";
-        } else {
-            var msg = "Response cookie 'expires' date is " + interval(0 - diff) + " in the past.";
-        }
-        document.getElementById('respcookiewarn').innerHTML = msg;
-    }
-}
-//-->
-</script>
-</head>
-
-<body onload='init()'>
-<h2>Session Demo</h2>
-<p>Reload this page. The session ID should not change from one reload to the next</p>
-<p><a href='../'>Index</a> | <a href='expire'>Expire</a> | <a href='regen'>Regenerate</a></p>
-<table>
-    <tr><th>Session ID:</th><td>%(sessionid)s<p class='warn'>%(changemsg)s</p></td></tr>
-    <tr><th>Request Cookie</th><td>%(reqcookie)s</td></tr>
-    <tr><th>Response Cookie</th><td>%(respcookie)s<p id='respcookiewarn' class='warn'></p></td></tr>
-    <tr><th>Session Data</th><td>%(sessiondata)s</td></tr>
-    <tr><th>Server Time</th><td id='stime'>%(servertime)s (Unix time: %(serverunixtime)s)</td></tr>
-    <tr><th>Browser Time</th><td id='btime'>&nbsp;</td></tr>
-    <tr><th>Cherrypy Version:</th><td>%(cpversion)s</td></tr>
-    <tr><th>Python Version:</th><td>%(pyversion)s</td></tr>
-</table>
-</body></html>
-"""  # noqa E501
-
-
-class Root(object):
-
-    def page(self):
-        changemsg = []
-        if cherrypy.session.id != cherrypy.session.originalid:
-            if cherrypy.session.originalid is None:
-                changemsg.append(
-                    'Created new session because no session id was given.')
-            if cherrypy.session.missing:
-                changemsg.append(
-                    'Created new session due to missing '
-                    '(expired or malicious) session.')
-            if cherrypy.session.regenerated:
-                changemsg.append('Application generated a new session.')
-
-        try:
-            expires = cherrypy.response.cookie['session_id']['expires']
-        except KeyError:
-            expires = ''
-
-        return page % {
-            'sessionid': cherrypy.session.id,
-            'changemsg': '<br>'.join(changemsg),
-            'respcookie': cherrypy.response.cookie.output(),
-            'reqcookie': cherrypy.request.cookie.output(),
-            'sessiondata': list(six.iteritems(cherrypy.session)),
-            'servertime': (
-                datetime.utcnow().strftime('%Y/%m/%d %H:%M') + ' UTC'
-            ),
-            'serverunixtime': calendar.timegm(datetime.utcnow().timetuple()),
-            'cpversion': cherrypy.__version__,
-            'pyversion': sys.version,
-            'expires': expires,
-        }
-
-    @cherrypy.expose
-    def index(self):
-        # Must modify data or the session will not be saved.
-        cherrypy.session['color'] = 'green'
-        return self.page()
-
-    @cherrypy.expose
-    def expire(self):
-        sessions.expire()
-        return self.page()
-
-    @cherrypy.expose
-    def regen(self):
-        cherrypy.session.regenerate()
-        # Must modify data or the session will not be saved.
-        cherrypy.session['color'] = 'yellow'
-        return self.page()
-
-
-if __name__ == '__main__':
-    cherrypy.config.update({
-        # 'environment': 'production',
-        'log.screen': True,
-        'tools.sessions.on': True,
-    })
-    cherrypy.quickstart(Root())
diff --git a/libraries/cherrypy/test/static/404.html b/libraries/cherrypy/test/static/404.html
deleted file mode 100644
index 01b17b09..00000000
--- a/libraries/cherrypy/test/static/404.html
+++ /dev/null
@@ -1,5 +0,0 @@
-<html>
- <body>
-  <h1>I couldn't find that thing you were looking for!</h1>
- </body>
-</html>
diff --git a/libraries/cherrypy/test/static/dirback.jpg b/libraries/cherrypy/test/static/dirback.jpg
deleted file mode 100644
index 80403dc227c19b9192158420f5288121e7f2669a..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 16585
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
Sse&CiQ+RTS)_Y=q`u_k!0j`Vy

diff --git a/libraries/cherrypy/test/static/index.html b/libraries/cherrypy/test/static/index.html
deleted file mode 100644
index a5c19667..00000000
--- a/libraries/cherrypy/test/static/index.html
+++ /dev/null
@@ -1 +0,0 @@
-Hello, world
diff --git a/libraries/cherrypy/test/style.css b/libraries/cherrypy/test/style.css
deleted file mode 100644
index b266e93d..00000000
--- a/libraries/cherrypy/test/style.css
+++ /dev/null
@@ -1 +0,0 @@
-Dummy stylesheet
diff --git a/libraries/cherrypy/test/test.pem b/libraries/cherrypy/test/test.pem
deleted file mode 100644
index 47a47042..00000000
--- a/libraries/cherrypy/test/test.pem
+++ /dev/null
@@ -1,38 +0,0 @@
------BEGIN RSA PRIVATE KEY-----
-MIICXAIBAAKBgQDBKo554mzIMY+AByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZ
-R9L4WtImEew05FY3Izerfm3MN3+MC0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Kn
-da+O6xldVSosu8Ev3z9VZ94iC/ZgKzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQAB
-AoGAWOCF0ZrWxn3XMucWq2LNwPKqlvVGwbIwX3cDmX22zmnM4Fy6arXbYh4XlyCj
-9+ofqRrxIFz5k/7tFriTmZ0xag5+Jdx+Kwg0/twiP7XCNKipFogwe1Hznw8OFAoT
-enKBdj2+/n2o0Bvo/tDB59m9L/538d46JGQUmJlzMyqYikECQQDyoq+8CtMNvE18
-8VgHcR/KtApxWAjj4HpaHYL637ATjThetUZkW92mgDgowyplthusxdNqhHWyv7E8
-tWNdYErZAkEAy85ShTR0M5aWmrE7o0r0SpWInAkNBH9aXQRRARFYsdBtNfRu6I0i
-0lvU9wiu3eF57FMEC86yViZ5UBnQfTu7vQJAVesj/Zt7pwaCDfdMa740OsxMUlyR
-MVhhGx4OLpYdPJ8qUecxGQKq13XZ7R1HGyNEY4bd2X80Smq08UFuATfC6QJAH8UB
-yBHtKz2GLIcELOg6PIYizW/7v3+6rlVF60yw7sb2vzpjL40QqIn4IKoR2DSVtOkb
-8FtAIX3N21aq0VrGYQJBAIPiaEc2AZ8Bq2GC4F3wOz/BxJ/izvnkiotR12QK4fh5
-yjZMhTjWCas5zwHR5PDjlD88AWGDMsZ1PicD4348xJQ=
------END RSA PRIVATE KEY-----
------BEGIN CERTIFICATE-----
-MIIDxTCCAy6gAwIBAgIJAI18BD7eQxlGMA0GCSqGSIb3DQEBBAUAMIGeMQswCQYD
-VQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5pYTESMBAGA1UEBxMJU2FuIERpZWdv
-MRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0MREwDwYDVQQLEwhkZXYtdGVzdDEW
-MBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4GCSqGSIb3DQEJARYRcmVtaUBjaGVy
-cnlweS5vcmcwHhcNMDYwOTA5MTkyMDIwWhcNMzQwMTI0MTkyMDIwWjCBnjELMAkG
-A1UEBhMCVVMxEzARBgNVBAgTCkNhbGlmb3JuaWExEjAQBgNVBAcTCVNhbiBEaWVn
-bzEZMBcGA1UEChMQQ2hlcnJ5UHkgUHJvamVjdDERMA8GA1UECxMIZGV2LXRlc3Qx
-FjAUBgNVBAMTDUNoZXJyeVB5IFRlYW0xIDAeBgkqhkiG9w0BCQEWEXJlbWlAY2hl
-cnJ5cHkub3JnMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDBKo554mzIMY+A
-ByUNpaUOP9bJnQ7ZLQe9XgHwoLJR4VzpyZZZR9L4WtImEew05FY3Izerfm3MN3+M
-C0tJ6yQU9sOiU3vBW6RrLIMlfKsnRwBRZ0Knda+O6xldVSosu8Ev3z9VZ94iC/Zg
-KzrH7Mjj/U8/MQO7RBS/LAqee8bFNQIDAQABo4IBBzCCAQMwHQYDVR0OBBYEFDIQ
-2feb71tVZCWpU0qJ/Tw+wdtoMIHTBgNVHSMEgcswgciAFDIQ2feb71tVZCWpU0qJ
-/Tw+wdtooYGkpIGhMIGeMQswCQYDVQQGEwJVUzETMBEGA1UECBMKQ2FsaWZvcm5p
-YTESMBAGA1UEBxMJU2FuIERpZWdvMRkwFwYDVQQKExBDaGVycnlQeSBQcm9qZWN0
-MREwDwYDVQQLEwhkZXYtdGVzdDEWMBQGA1UEAxMNQ2hlcnJ5UHkgVGVhbTEgMB4G
-CSqGSIb3DQEJARYRcmVtaUBjaGVycnlweS5vcmeCCQCNfAQ+3kMZRjAMBgNVHRME
-BTADAQH/MA0GCSqGSIb3DQEBBAUAA4GBAL7AAQz7IePV48ZTAFHKr88ntPALsL5S
-8vHCZPNMevNkLTj3DYUw2BcnENxMjm1kou2F2BkvheBPNZKIhc6z4hAml3ed1xa2
-D7w6e6OTcstdK/+KrPDDHeOP1dhMWNs2JE1bNlfF1LiXzYKSXpe88eCKjCXsCT/T
-NluCaWQys3MS
------END CERTIFICATE-----
diff --git a/libraries/cherrypy/test/test_auth_basic.py b/libraries/cherrypy/test/test_auth_basic.py
deleted file mode 100644
index d7e69a9b..00000000
--- a/libraries/cherrypy/test/test_auth_basic.py
+++ /dev/null
@@ -1,135 +0,0 @@
-# This file is part of CherryPy <http://www.cherrypy.org/>
-# -*- coding: utf-8 -*-
-# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
-
-from hashlib import md5
-
-import cherrypy
-from cherrypy._cpcompat import ntob
-from cherrypy.lib import auth_basic
-from cherrypy.test import helper
-
-
-class BasicAuthTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'This is public.'
-
-        class BasicProtected:
-
-            @cherrypy.expose
-            def index(self):
-                return "Hello %s, you've been authorized." % (
-                    cherrypy.request.login)
-
-        class BasicProtected2:
-
-            @cherrypy.expose
-            def index(self):
-                return "Hello %s, you've been authorized." % (
-                    cherrypy.request.login)
-
-        class BasicProtected2_u:
-
-            @cherrypy.expose
-            def index(self):
-                return "Hello %s, you've been authorized." % (
-                    cherrypy.request.login)
-
-        userpassdict = {'xuser': 'xpassword'}
-        userhashdict = {'xuser': md5(b'xpassword').hexdigest()}
-        userhashdict_u = {'xюзер': md5(ntob('їжа', 'utf-8')).hexdigest()}
-
-        def checkpasshash(realm, user, password):
-            p = userhashdict.get(user)
-            return p and p == md5(ntob(password)).hexdigest() or False
-
-        def checkpasshash_u(realm, user, password):
-            p = userhashdict_u.get(user)
-            return p and p == md5(ntob(password, 'utf-8')).hexdigest() or False
-
-        basic_checkpassword_dict = auth_basic.checkpassword_dict(userpassdict)
-        conf = {
-            '/basic': {
-                'tools.auth_basic.on': True,
-                'tools.auth_basic.realm': 'wonderland',
-                'tools.auth_basic.checkpassword': basic_checkpassword_dict
-            },
-            '/basic2': {
-                'tools.auth_basic.on': True,
-                'tools.auth_basic.realm': 'wonderland',
-                'tools.auth_basic.checkpassword': checkpasshash,
-                'tools.auth_basic.accept_charset': 'ISO-8859-1',
-            },
-            '/basic2_u': {
-                'tools.auth_basic.on': True,
-                'tools.auth_basic.realm': 'wonderland',
-                'tools.auth_basic.checkpassword': checkpasshash_u,
-                'tools.auth_basic.accept_charset': 'UTF-8',
-            },
-        }
-
-        root = Root()
-        root.basic = BasicProtected()
-        root.basic2 = BasicProtected2()
-        root.basic2_u = BasicProtected2_u()
-        cherrypy.tree.mount(root, config=conf)
-
-    def testPublic(self):
-        self.getPage('/')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
-        self.assertBody('This is public.')
-
-    def testBasic(self):
-        self.getPage('/basic/')
-        self.assertStatus(401)
-        self.assertHeader(
-            'WWW-Authenticate',
-            'Basic realm="wonderland", charset="UTF-8"'
-        )
-
-        self.getPage('/basic/',
-                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
-        self.assertStatus(401)
-
-        self.getPage('/basic/',
-                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
-        self.assertStatus('200 OK')
-        self.assertBody("Hello xuser, you've been authorized.")
-
-    def testBasic2(self):
-        self.getPage('/basic2/')
-        self.assertStatus(401)
-        self.assertHeader('WWW-Authenticate', 'Basic realm="wonderland"')
-
-        self.getPage('/basic2/',
-                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3JX')])
-        self.assertStatus(401)
-
-        self.getPage('/basic2/',
-                     [('Authorization', 'Basic eHVzZXI6eHBhc3N3b3Jk')])
-        self.assertStatus('200 OK')
-        self.assertBody("Hello xuser, you've been authorized.")
-
-    def testBasic2_u(self):
-        self.getPage('/basic2_u/')
-        self.assertStatus(401)
-        self.assertHeader(
-            'WWW-Authenticate',
-            'Basic realm="wonderland", charset="UTF-8"'
-        )
-
-        self.getPage('/basic2_u/',
-                     [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbRgw==')])
-        self.assertStatus(401)
-
-        self.getPage('/basic2_u/',
-                     [('Authorization', 'Basic eNGO0LfQtdGAOtGX0LbQsA==')])
-        self.assertStatus('200 OK')
-        self.assertBody("Hello xюзер, you've been authorized.")
diff --git a/libraries/cherrypy/test/test_auth_digest.py b/libraries/cherrypy/test/test_auth_digest.py
deleted file mode 100644
index 512e39a5..00000000
--- a/libraries/cherrypy/test/test_auth_digest.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# This file is part of CherryPy <http://www.cherrypy.org/>
-# -*- coding: utf-8 -*-
-# vim:ts=4:sw=4:expandtab:fileencoding=utf-8
-
-import six
-
-
-import cherrypy
-from cherrypy.lib import auth_digest
-from cherrypy._cpcompat import ntob
-
-from cherrypy.test import helper
-
-
-def _fetch_users():
-    return {'test': 'test', '☃йюзер': 'їпароль'}
-
-
-get_ha1 = cherrypy.lib.auth_digest.get_ha1_dict_plain(_fetch_users())
-
-
-class DigestAuthTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'This is public.'
-
-        class DigestProtected:
-
-            @cherrypy.expose
-            def index(self, *args, **kwargs):
-                return "Hello %s, you've been authorized." % (
-                    cherrypy.request.login)
-
-        conf = {'/digest': {'tools.auth_digest.on': True,
-                            'tools.auth_digest.realm': 'localhost',
-                            'tools.auth_digest.get_ha1': get_ha1,
-                            'tools.auth_digest.key': 'a565c27146791cfb',
-                            'tools.auth_digest.debug': True,
-                            'tools.auth_digest.accept_charset': 'UTF-8'}}
-
-        root = Root()
-        root.digest = DigestProtected()
-        cherrypy.tree.mount(root, config=conf)
-
-    def testPublic(self):
-        self.getPage('/')
-        assert self.status == '200 OK'
-        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
-        assert self.body == b'This is public.'
-
-    def _test_parametric_digest(self, username, realm):
-        test_uri = '/digest/?@/=%2F%40&%f0%9f%99%88=path'
-
-        self.getPage(test_uri)
-        assert self.status_code == 401
-
-        msg = 'Digest authentification scheme was not found'
-        www_auth_digest = tuple(filter(
-            lambda kv: kv[0].lower() == 'www-authenticate'
-            and kv[1].startswith('Digest '),
-            self.headers,
-        ))
-        assert len(www_auth_digest) == 1, msg
-
-        items = www_auth_digest[0][-1][7:].split(', ')
-        tokens = {}
-        for item in items:
-            key, value = item.split('=')
-            tokens[key.lower()] = value
-
-        assert tokens['realm'] == '"localhost"'
-        assert tokens['algorithm'] == '"MD5"'
-        assert tokens['qop'] == '"auth"'
-        assert tokens['charset'] == '"UTF-8"'
-
-        nonce = tokens['nonce'].strip('"')
-
-        # Test user agent response with a wrong value for 'realm'
-        base_auth = ('Digest username="%s", '
-                     'realm="%s", '
-                     'nonce="%s", '
-                     'uri="%s", '
-                     'algorithm=MD5, '
-                     'response="%s", '
-                     'qop=auth, '
-                     'nc=%s, '
-                     'cnonce="1522e61005789929"')
-
-        encoded_user = username
-        if six.PY3:
-            encoded_user = encoded_user.encode('utf-8')
-        encoded_user = encoded_user.decode('latin1')
-        auth_header = base_auth % (
-            encoded_user, realm, nonce, test_uri,
-            '11111111111111111111111111111111', '00000001',
-        )
-        auth = auth_digest.HttpDigestAuthorization(auth_header, 'GET')
-        # calculate the response digest
-        ha1 = get_ha1(auth.realm, auth.username)
-        response = auth.request_digest(ha1)
-        auth_header = base_auth % (
-            encoded_user, realm, nonce, test_uri,
-            response, '00000001',
-        )
-        self.getPage(test_uri, [('Authorization', auth_header)])
-
-    def test_wrong_realm(self):
-        # send response with correct response digest, but wrong realm
-        self._test_parametric_digest(username='test', realm='wrong realm')
-        assert self.status_code == 401
-
-    def test_ascii_user(self):
-        self._test_parametric_digest(username='test', realm='localhost')
-        assert self.status == '200 OK'
-        assert self.body == b"Hello test, you've been authorized."
-
-    def test_unicode_user(self):
-        self._test_parametric_digest(username='☃йюзер', realm='localhost')
-        assert self.status == '200 OK'
-        assert self.body == ntob(
-            "Hello ☃йюзер, you've been authorized.", 'utf-8',
-        )
-
-    def test_wrong_scheme(self):
-        basic_auth = {
-            'Authorization': 'Basic foo:bar',
-        }
-        self.getPage('/digest/', headers=list(basic_auth.items()))
-        assert self.status_code == 401
diff --git a/libraries/cherrypy/test/test_bus.py b/libraries/cherrypy/test/test_bus.py
deleted file mode 100644
index 6026b47e..00000000
--- a/libraries/cherrypy/test/test_bus.py
+++ /dev/null
@@ -1,274 +0,0 @@
-import threading
-import time
-import unittest
-
-from cherrypy.process import wspbus
-
-
-msg = 'Listener %d on channel %s: %s.'
-
-
-class PublishSubscribeTests(unittest.TestCase):
-
-    def get_listener(self, channel, index):
-        def listener(arg=None):
-            self.responses.append(msg % (index, channel, arg))
-        return listener
-
-    def test_builtin_channels(self):
-        b = wspbus.Bus()
-
-        self.responses, expected = [], []
-
-        for channel in b.listeners:
-            for index, priority in enumerate([100, 50, 0, 51]):
-                b.subscribe(channel,
-                            self.get_listener(channel, index), priority)
-
-        for channel in b.listeners:
-            b.publish(channel)
-            expected.extend([msg % (i, channel, None) for i in (2, 1, 3, 0)])
-            b.publish(channel, arg=79347)
-            expected.extend([msg % (i, channel, 79347) for i in (2, 1, 3, 0)])
-
-        self.assertEqual(self.responses, expected)
-
-    def test_custom_channels(self):
-        b = wspbus.Bus()
-
-        self.responses, expected = [], []
-
-        custom_listeners = ('hugh', 'louis', 'dewey')
-        for channel in custom_listeners:
-            for index, priority in enumerate([None, 10, 60, 40]):
-                b.subscribe(channel,
-                            self.get_listener(channel, index), priority)
-
-        for channel in custom_listeners:
-            b.publish(channel, 'ah so')
-            expected.extend([msg % (i, channel, 'ah so')
-                            for i in (1, 3, 0, 2)])
-            b.publish(channel)
-            expected.extend([msg % (i, channel, None) for i in (1, 3, 0, 2)])
-
-        self.assertEqual(self.responses, expected)
-
-    def test_listener_errors(self):
-        b = wspbus.Bus()
-
-        self.responses, expected = [], []
-        channels = [c for c in b.listeners if c != 'log']
-
-        for channel in channels:
-            b.subscribe(channel, self.get_listener(channel, 1))
-            # This will break since the lambda takes no args.
-            b.subscribe(channel, lambda: None, priority=20)
-
-        for channel in channels:
-            self.assertRaises(wspbus.ChannelFailures, b.publish, channel, 123)
-            expected.append(msg % (1, channel, 123))
-
-        self.assertEqual(self.responses, expected)
-
-
-class BusMethodTests(unittest.TestCase):
-
-    def log(self, bus):
-        self._log_entries = []
-
-        def logit(msg, level):
-            self._log_entries.append(msg)
-        bus.subscribe('log', logit)
-
-    def assertLog(self, entries):
-        self.assertEqual(self._log_entries, entries)
-
-    def get_listener(self, channel, index):
-        def listener(arg=None):
-            self.responses.append(msg % (index, channel, arg))
-        return listener
-
-    def test_start(self):
-        b = wspbus.Bus()
-        self.log(b)
-
-        self.responses = []
-        num = 3
-        for index in range(num):
-            b.subscribe('start', self.get_listener('start', index))
-
-        b.start()
-        try:
-            # The start method MUST call all 'start' listeners.
-            self.assertEqual(
-                set(self.responses),
-                set([msg % (i, 'start', None) for i in range(num)]))
-            # The start method MUST move the state to STARTED
-            # (or EXITING, if errors occur)
-            self.assertEqual(b.state, b.states.STARTED)
-            # The start method MUST log its states.
-            self.assertLog(['Bus STARTING', 'Bus STARTED'])
-        finally:
-            # Exit so the atexit handler doesn't complain.
-            b.exit()
-
-    def test_stop(self):
-        b = wspbus.Bus()
-        self.log(b)
-
-        self.responses = []
-        num = 3
-        for index in range(num):
-            b.subscribe('stop', self.get_listener('stop', index))
-
-        b.stop()
-
-        # The stop method MUST call all 'stop' listeners.
-        self.assertEqual(set(self.responses),
-                         set([msg % (i, 'stop', None) for i in range(num)]))
-        # The stop method MUST move the state to STOPPED
-        self.assertEqual(b.state, b.states.STOPPED)
-        # The stop method MUST log its states.
-        self.assertLog(['Bus STOPPING', 'Bus STOPPED'])
-
-    def test_graceful(self):
-        b = wspbus.Bus()
-        self.log(b)
-
-        self.responses = []
-        num = 3
-        for index in range(num):
-            b.subscribe('graceful', self.get_listener('graceful', index))
-
-        b.graceful()
-
-        # The graceful method MUST call all 'graceful' listeners.
-        self.assertEqual(
-            set(self.responses),
-            set([msg % (i, 'graceful', None) for i in range(num)]))
-        # The graceful method MUST log its states.
-        self.assertLog(['Bus graceful'])
-
-    def test_exit(self):
-        b = wspbus.Bus()
-        self.log(b)
-
-        self.responses = []
-        num = 3
-        for index in range(num):
-            b.subscribe('stop', self.get_listener('stop', index))
-            b.subscribe('exit', self.get_listener('exit', index))
-
-        b.exit()
-
-        # The exit method MUST call all 'stop' listeners,
-        # and then all 'exit' listeners.
-        self.assertEqual(set(self.responses),
-                         set([msg % (i, 'stop', None) for i in range(num)] +
-                             [msg % (i, 'exit', None) for i in range(num)]))
-        # The exit method MUST move the state to EXITING
-        self.assertEqual(b.state, b.states.EXITING)
-        # The exit method MUST log its states.
-        self.assertLog(
-            ['Bus STOPPING', 'Bus STOPPED', 'Bus EXITING', 'Bus EXITED'])
-
-    def test_wait(self):
-        b = wspbus.Bus()
-
-        def f(method):
-            time.sleep(0.2)
-            getattr(b, method)()
-
-        for method, states in [('start', [b.states.STARTED]),
-                               ('stop', [b.states.STOPPED]),
-                               ('start',
-                                [b.states.STARTING, b.states.STARTED]),
-                               ('exit', [b.states.EXITING]),
-                               ]:
-            threading.Thread(target=f, args=(method,)).start()
-            b.wait(states)
-
-            # The wait method MUST wait for the given state(s).
-            if b.state not in states:
-                self.fail('State %r not in %r' % (b.state, states))
-
-    def test_block(self):
-        b = wspbus.Bus()
-        self.log(b)
-
-        def f():
-            time.sleep(0.2)
-            b.exit()
-
-        def g():
-            time.sleep(0.4)
-        threading.Thread(target=f).start()
-        threading.Thread(target=g).start()
-        threads = [t for t in threading.enumerate() if not t.daemon]
-        self.assertEqual(len(threads), 3)
-
-        b.block()
-
-        # The block method MUST wait for the EXITING state.
-        self.assertEqual(b.state, b.states.EXITING)
-        # The block method MUST wait for ALL non-main, non-daemon threads to
-        # finish.
-        threads = [t for t in threading.enumerate() if not t.daemon]
-        self.assertEqual(len(threads), 1)
-        # The last message will mention an indeterminable thread name; ignore
-        # it
-        self.assertEqual(self._log_entries[:-1],
-                         ['Bus STOPPING', 'Bus STOPPED',
-                          'Bus EXITING', 'Bus EXITED',
-                          'Waiting for child threads to terminate...'])
-
-    def test_start_with_callback(self):
-        b = wspbus.Bus()
-        self.log(b)
-        try:
-            events = []
-
-            def f(*args, **kwargs):
-                events.append(('f', args, kwargs))
-
-            def g():
-                events.append('g')
-            b.subscribe('start', g)
-            b.start_with_callback(f, (1, 3, 5), {'foo': 'bar'})
-            # Give wait() time to run f()
-            time.sleep(0.2)
-
-            # The callback method MUST wait for the STARTED state.
-            self.assertEqual(b.state, b.states.STARTED)
-            # The callback method MUST run after all start methods.
-            self.assertEqual(events, ['g', ('f', (1, 3, 5), {'foo': 'bar'})])
-        finally:
-            b.exit()
-
-    def test_log(self):
-        b = wspbus.Bus()
-        self.log(b)
-        self.assertLog([])
-
-        # Try a normal message.
-        expected = []
-        for msg in ["O mah darlin'"] * 3 + ['Clementiiiiiiiine']:
-            b.log(msg)
-            expected.append(msg)
-            self.assertLog(expected)
-
-        # Try an error message
-        try:
-            foo
-        except NameError:
-            b.log('You are lost and gone forever', traceback=True)
-            lastmsg = self._log_entries[-1]
-            if 'Traceback' not in lastmsg or 'NameError' not in lastmsg:
-                self.fail('Last log message %r did not contain '
-                          'the expected traceback.' % lastmsg)
-        else:
-            self.fail('NameError was not raised as expected.')
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/libraries/cherrypy/test/test_caching.py b/libraries/cherrypy/test/test_caching.py
deleted file mode 100644
index 1a6ed4f2..00000000
--- a/libraries/cherrypy/test/test_caching.py
+++ /dev/null
@@ -1,392 +0,0 @@
-import datetime
-from itertools import count
-import os
-import threading
-import time
-
-from six.moves import range
-from six.moves import urllib
-
-import pytest
-
-import cherrypy
-from cherrypy.lib import httputil
-
-from cherrypy.test import helper
-
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-gif_bytes = (
-    b'GIF89a\x01\x00\x01\x00\x82\x00\x01\x99"\x1e\x00\x00\x00\x00\x00'
-    b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00'
-    b'\x00,\x00\x00\x00\x00\x01\x00\x01\x00\x02\x03\x02\x08\t\x00;'
-)
-
-
-class CacheTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        @cherrypy.config(**{'tools.caching.on': True})
-        class Root:
-
-            def __init__(self):
-                self.counter = 0
-                self.control_counter = 0
-                self.longlock = threading.Lock()
-
-            @cherrypy.expose
-            def index(self):
-                self.counter += 1
-                msg = 'visit #%s' % self.counter
-                return msg
-
-            @cherrypy.expose
-            def control(self):
-                self.control_counter += 1
-                return 'visit #%s' % self.control_counter
-
-            @cherrypy.expose
-            def a_gif(self):
-                cherrypy.response.headers[
-                    'Last-Modified'] = httputil.HTTPDate()
-                return gif_bytes
-
-            @cherrypy.expose
-            def long_process(self, seconds='1'):
-                try:
-                    self.longlock.acquire()
-                    time.sleep(float(seconds))
-                finally:
-                    self.longlock.release()
-                return 'success!'
-
-            @cherrypy.expose
-            def clear_cache(self, path):
-                cherrypy._cache.store[cherrypy.request.base + path].clear()
-
-        @cherrypy.config(**{
-            'tools.caching.on': True,
-            'tools.response_headers.on': True,
-            'tools.response_headers.headers': [
-                ('Vary', 'Our-Varying-Header')
-            ],
-        })
-        class VaryHeaderCachingServer(object):
-
-            def __init__(self):
-                self.counter = count(1)
-
-            @cherrypy.expose
-            def index(self):
-                return 'visit #%s' % next(self.counter)
-
-        @cherrypy.config(**{
-            'tools.expires.on': True,
-            'tools.expires.secs': 60,
-            'tools.staticdir.on': True,
-            'tools.staticdir.dir': 'static',
-            'tools.staticdir.root': curdir,
-        })
-        class UnCached(object):
-
-            @cherrypy.expose
-            @cherrypy.config(**{'tools.expires.secs': 0})
-            def force(self):
-                cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
-                self._cp_config['tools.expires.force'] = True
-                self._cp_config['tools.expires.secs'] = 0
-                return 'being forceful'
-
-            @cherrypy.expose
-            def dynamic(self):
-                cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
-                cherrypy.response.headers['Cache-Control'] = 'private'
-                return 'D-d-d-dynamic!'
-
-            @cherrypy.expose
-            def cacheable(self):
-                cherrypy.response.headers['Etag'] = 'bibbitybobbityboo'
-                return "Hi, I'm cacheable."
-
-            @cherrypy.expose
-            @cherrypy.config(**{'tools.expires.secs': 86400})
-            def specific(self):
-                cherrypy.response.headers[
-                    'Etag'] = 'need_this_to_make_me_cacheable'
-                return 'I am being specific'
-
-            class Foo(object):
-                pass
-
-            @cherrypy.expose
-            @cherrypy.config(**{'tools.expires.secs': Foo()})
-            def wrongtype(self):
-                cherrypy.response.headers[
-                    'Etag'] = 'need_this_to_make_me_cacheable'
-                return 'Woops'
-
-        @cherrypy.config(**{
-            'tools.gzip.mime_types': ['text/*', 'image/*'],
-            'tools.caching.on': True,
-            'tools.staticdir.on': True,
-            'tools.staticdir.dir': 'static',
-            'tools.staticdir.root': curdir
-        })
-        class GzipStaticCache(object):
-            pass
-
-        cherrypy.tree.mount(Root())
-        cherrypy.tree.mount(UnCached(), '/expires')
-        cherrypy.tree.mount(VaryHeaderCachingServer(), '/varying_headers')
-        cherrypy.tree.mount(GzipStaticCache(), '/gzip_static_cache')
-        cherrypy.config.update({'tools.gzip.on': True})
-
-    def testCaching(self):
-        elapsed = 0.0
-        for trial in range(10):
-            self.getPage('/')
-            # The response should be the same every time,
-            # except for the Age response header.
-            self.assertBody('visit #1')
-            if trial != 0:
-                age = int(self.assertHeader('Age'))
-                self.assert_(age >= elapsed)
-                elapsed = age
-
-        # POST, PUT, DELETE should not be cached.
-        self.getPage('/', method='POST')
-        self.assertBody('visit #2')
-        # Because gzip is turned on, the Vary header should always Vary for
-        # content-encoding
-        self.assertHeader('Vary', 'Accept-Encoding')
-        # The previous request should have invalidated the cache,
-        # so this request will recalc the response.
-        self.getPage('/', method='GET')
-        self.assertBody('visit #3')
-        # ...but this request should get the cached copy.
-        self.getPage('/', method='GET')
-        self.assertBody('visit #3')
-        self.getPage('/', method='DELETE')
-        self.assertBody('visit #4')
-
-        # The previous request should have invalidated the cache,
-        # so this request will recalc the response.
-        self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
-        self.assertHeader('Content-Encoding', 'gzip')
-        self.assertHeader('Vary')
-        self.assertEqual(
-            cherrypy.lib.encoding.decompress(self.body), b'visit #5')
-
-        # Now check that a second request gets the gzip header and gzipped body
-        # This also tests a bug in 3.0 to 3.0.2 whereby the cached, gzipped
-        # response body was being gzipped a second time.
-        self.getPage('/', method='GET', headers=[('Accept-Encoding', 'gzip')])
-        self.assertHeader('Content-Encoding', 'gzip')
-        self.assertEqual(
-            cherrypy.lib.encoding.decompress(self.body), b'visit #5')
-
-        # Now check that a third request that doesn't accept gzip
-        # skips the cache (because the 'Vary' header denies it).
-        self.getPage('/', method='GET')
-        self.assertNoHeader('Content-Encoding')
-        self.assertBody('visit #6')
-
-    def testVaryHeader(self):
-        self.getPage('/varying_headers/')
-        self.assertStatus('200 OK')
-        self.assertHeaderItemValue('Vary', 'Our-Varying-Header')
-        self.assertBody('visit #1')
-
-        # Now check that different 'Vary'-fields don't evict each other.
-        # This test creates 2 requests with different 'Our-Varying-Header'
-        # and then tests if the first one still exists.
-        self.getPage('/varying_headers/',
-                     headers=[('Our-Varying-Header', 'request 2')])
-        self.assertStatus('200 OK')
-        self.assertBody('visit #2')
-
-        self.getPage('/varying_headers/',
-                     headers=[('Our-Varying-Header', 'request 2')])
-        self.assertStatus('200 OK')
-        self.assertBody('visit #2')
-
-        self.getPage('/varying_headers/')
-        self.assertStatus('200 OK')
-        self.assertBody('visit #1')
-
-    def testExpiresTool(self):
-        # test setting an expires header
-        self.getPage('/expires/specific')
-        self.assertStatus('200 OK')
-        self.assertHeader('Expires')
-
-        # test exceptions for bad time values
-        self.getPage('/expires/wrongtype')
-        self.assertStatus(500)
-        self.assertInBody('TypeError')
-
-        # static content should not have "cache prevention" headers
-        self.getPage('/expires/index.html')
-        self.assertStatus('200 OK')
-        self.assertNoHeader('Pragma')
-        self.assertNoHeader('Cache-Control')
-        self.assertHeader('Expires')
-
-        # dynamic content that sets indicators should not have
-        # "cache prevention" headers
-        self.getPage('/expires/cacheable')
-        self.assertStatus('200 OK')
-        self.assertNoHeader('Pragma')
-        self.assertNoHeader('Cache-Control')
-        self.assertHeader('Expires')
-
-        self.getPage('/expires/dynamic')
-        self.assertBody('D-d-d-dynamic!')
-        # the Cache-Control header should be untouched
-        self.assertHeader('Cache-Control', 'private')
-        self.assertHeader('Expires')
-
-        # configure the tool to ignore indicators and replace existing headers
-        self.getPage('/expires/force')
-        self.assertStatus('200 OK')
-        # This also gives us a chance to test 0 expiry with no other headers
-        self.assertHeader('Pragma', 'no-cache')
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
-        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
-
-        # static content should now have "cache prevention" headers
-        self.getPage('/expires/index.html')
-        self.assertStatus('200 OK')
-        self.assertHeader('Pragma', 'no-cache')
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
-        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
-
-        # the cacheable handler should now have "cache prevention" headers
-        self.getPage('/expires/cacheable')
-        self.assertStatus('200 OK')
-        self.assertHeader('Pragma', 'no-cache')
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
-        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
-
-        self.getPage('/expires/dynamic')
-        self.assertBody('D-d-d-dynamic!')
-        # dynamic sets Cache-Control to private but it should  be
-        # overwritten here ...
-        self.assertHeader('Pragma', 'no-cache')
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.assertHeader('Cache-Control', 'no-cache, must-revalidate')
-        self.assertHeader('Expires', 'Sun, 28 Jan 2007 00:00:00 GMT')
-
-    def _assert_resp_len_and_enc_for_gzip(self, uri):
-        """
-        Test that after querying gzipped content it's remains valid in
-        cache and available non-gzipped as well.
-        """
-        ACCEPT_GZIP_HEADERS = [('Accept-Encoding', 'gzip')]
-        content_len = None
-
-        for _ in range(3):
-            self.getPage(uri, method='GET', headers=ACCEPT_GZIP_HEADERS)
-
-            if content_len is not None:
-                # all requests should get the same length
-                self.assertHeader('Content-Length', content_len)
-                self.assertHeader('Content-Encoding', 'gzip')
-
-            content_len = dict(self.headers)['Content-Length']
-
-        # check that we can still get non-gzipped version
-        self.getPage(uri, method='GET')
-        self.assertNoHeader('Content-Encoding')
-        # non-gzipped version should have a different content length
-        self.assertNoHeaderItemValue('Content-Length', content_len)
-
-    def testGzipStaticCache(self):
-        """Test that cache and gzip tools play well together when both enabled.
-
-        Ref GitHub issue #1190.
-        """
-        GZIP_STATIC_CACHE_TMPL = '/gzip_static_cache/{}'
-        resource_files = ('index.html', 'dirback.jpg')
-
-        for f in resource_files:
-            uri = GZIP_STATIC_CACHE_TMPL.format(f)
-            self._assert_resp_len_and_enc_for_gzip(uri)
-
-    def testLastModified(self):
-        self.getPage('/a.gif')
-        self.assertStatus(200)
-        self.assertBody(gif_bytes)
-        lm1 = self.assertHeader('Last-Modified')
-
-        # this request should get the cached copy.
-        self.getPage('/a.gif')
-        self.assertStatus(200)
-        self.assertBody(gif_bytes)
-        self.assertHeader('Age')
-        lm2 = self.assertHeader('Last-Modified')
-        self.assertEqual(lm1, lm2)
-
-        # this request should match the cached copy, but raise 304.
-        self.getPage('/a.gif', [('If-Modified-Since', lm1)])
-        self.assertStatus(304)
-        self.assertNoHeader('Last-Modified')
-        if not getattr(cherrypy.server, 'using_apache', False):
-            self.assertHeader('Age')
-
-    @pytest.mark.xfail(reason='#1536')
-    def test_antistampede(self):
-        SECONDS = 4
-        slow_url = '/long_process?seconds={SECONDS}'.format(**locals())
-        # We MUST make an initial synchronous request in order to create the
-        # AntiStampedeCache object, and populate its selecting_headers,
-        # before the actual stampede.
-        self.getPage(slow_url)
-        self.assertBody('success!')
-        path = urllib.parse.quote(slow_url, safe='')
-        self.getPage('/clear_cache?path=' + path)
-        self.assertStatus(200)
-
-        start = datetime.datetime.now()
-
-        def run():
-            self.getPage(slow_url)
-            # The response should be the same every time
-            self.assertBody('success!')
-        ts = [threading.Thread(target=run) for i in range(100)]
-        for t in ts:
-            t.start()
-        for t in ts:
-            t.join()
-        finish = datetime.datetime.now()
-        # Allow for overhead, two seconds for slow hosts
-        allowance = SECONDS + 2
-        self.assertEqualDates(start, finish, seconds=allowance)
-
-    def test_cache_control(self):
-        self.getPage('/control')
-        self.assertBody('visit #1')
-        self.getPage('/control')
-        self.assertBody('visit #1')
-
-        self.getPage('/control', headers=[('Cache-Control', 'no-cache')])
-        self.assertBody('visit #2')
-        self.getPage('/control')
-        self.assertBody('visit #2')
-
-        self.getPage('/control', headers=[('Pragma', 'no-cache')])
-        self.assertBody('visit #3')
-        self.getPage('/control')
-        self.assertBody('visit #3')
-
-        time.sleep(1)
-        self.getPage('/control', headers=[('Cache-Control', 'max-age=0')])
-        self.assertBody('visit #4')
-        self.getPage('/control')
-        self.assertBody('visit #4')
diff --git a/libraries/cherrypy/test/test_compat.py b/libraries/cherrypy/test/test_compat.py
deleted file mode 100644
index 44a9fa31..00000000
--- a/libraries/cherrypy/test/test_compat.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""Test Python 2/3 compatibility module."""
-from __future__ import unicode_literals
-
-import unittest
-
-import pytest
-import six
-
-from cherrypy import _cpcompat as compat
-
-
-class StringTester(unittest.TestCase):
-    """Tests for string conversion."""
-
-    @pytest.mark.skipif(six.PY3, reason='Only useful on Python 2')
-    def test_ntob_non_native(self):
-        """ntob should raise an Exception on unicode.
-
-        (Python 2 only)
-
-        See #1132 for discussion.
-        """
-        self.assertRaises(TypeError, compat.ntob, 'fight')
-
-
-class EscapeTester(unittest.TestCase):
-    """Class to test escape_html function from _cpcompat."""
-
-    def test_escape_quote(self):
-        """test_escape_quote - Verify the output for &<>"' chars."""
-        self.assertEqual(
-            """xx&amp;&lt;&gt;"aa'""",
-            compat.escape_html("""xx&<>"aa'"""),
-        )
diff --git a/libraries/cherrypy/test/test_config.py b/libraries/cherrypy/test/test_config.py
deleted file mode 100644
index be17df90..00000000
--- a/libraries/cherrypy/test/test_config.py
+++ /dev/null
@@ -1,303 +0,0 @@
-"""Tests for the CherryPy configuration system."""
-
-import io
-import os
-import sys
-import unittest
-
-import six
-
-import cherrypy
-
-from cherrypy.test import helper
-
-
-localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-def StringIOFromNative(x):
-    return io.StringIO(six.text_type(x))
-
-
-def setup_server():
-
-    @cherrypy.config(foo='this', bar='that')
-    class Root:
-
-        def __init__(self):
-            cherrypy.config.namespaces['db'] = self.db_namespace
-
-        def db_namespace(self, k, v):
-            if k == 'scheme':
-                self.db = v
-
-        @cherrypy.expose(alias=('global_', 'xyz'))
-        def index(self, key):
-            return cherrypy.request.config.get(key, 'None')
-
-        @cherrypy.expose
-        def repr(self, key):
-            return repr(cherrypy.request.config.get(key, None))
-
-        @cherrypy.expose
-        def dbscheme(self):
-            return self.db
-
-        @cherrypy.expose
-        @cherrypy.config(**{'request.body.attempt_charsets': ['utf-16']})
-        def plain(self, x):
-            return x
-
-        favicon_ico = cherrypy.tools.staticfile.handler(
-            filename=os.path.join(localDir, '../favicon.ico'))
-
-    @cherrypy.config(foo='this2', baz='that2')
-    class Foo:
-
-        @cherrypy.expose
-        def index(self, key):
-            return cherrypy.request.config.get(key, 'None')
-        nex = index
-
-        @cherrypy.expose
-        @cherrypy.config(**{'response.headers.X-silly': 'sillyval'})
-        def silly(self):
-            return 'Hello world'
-
-        # Test the expose and config decorators
-        @cherrypy.config(foo='this3', **{'bax': 'this4'})
-        @cherrypy.expose
-        def bar(self, key):
-            return repr(cherrypy.request.config.get(key, None))
-
-    class Another:
-
-        @cherrypy.expose
-        def index(self, key):
-            return str(cherrypy.request.config.get(key, 'None'))
-
-    def raw_namespace(key, value):
-        if key == 'input.map':
-            handler = cherrypy.request.handler
-
-            def wrapper():
-                params = cherrypy.request.params
-                for name, coercer in list(value.items()):
-                    try:
-                        params[name] = coercer(params[name])
-                    except KeyError:
-                        pass
-                return handler()
-            cherrypy.request.handler = wrapper
-        elif key == 'output':
-            handler = cherrypy.request.handler
-
-            def wrapper():
-                # 'value' is a type (like int or str).
-                return value(handler())
-            cherrypy.request.handler = wrapper
-
-    @cherrypy.config(**{'raw.output': repr})
-    class Raw:
-
-        @cherrypy.expose
-        @cherrypy.config(**{'raw.input.map': {'num': int}})
-        def incr(self, num):
-            return num + 1
-
-    if not six.PY3:
-        thing3 = "thing3: unicode('test', errors='ignore')"
-    else:
-        thing3 = ''
-
-    ioconf = StringIOFromNative("""
-[/]
-neg: -1234
-filename: os.path.join(sys.prefix, "hello.py")
-thing1: cherrypy.lib.httputil.response_codes[404]
-thing2: __import__('cherrypy.tutorial', globals(), locals(), ['']).thing2
-%s
-complex: 3+2j
-mul: 6*3
-ones: "11"
-twos: "22"
-stradd: %%(ones)s + %%(twos)s + "33"
-
-[/favicon.ico]
-tools.staticfile.filename = %r
-""" % (thing3, os.path.join(localDir, 'static/dirback.jpg')))
-
-    root = Root()
-    root.foo = Foo()
-    root.raw = Raw()
-    app = cherrypy.tree.mount(root, config=ioconf)
-    app.request_class.namespaces['raw'] = raw_namespace
-
-    cherrypy.tree.mount(Another(), '/another')
-    cherrypy.config.update({'luxuryyacht': 'throatwobblermangrove',
-                            'db.scheme': r'sqlite///memory',
-                            })
-
-
-#                             Client-side code                             #
-
-
-class ConfigTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def testConfig(self):
-        tests = [
-            ('/', 'nex', 'None'),
-            ('/', 'foo', 'this'),
-            ('/', 'bar', 'that'),
-            ('/xyz', 'foo', 'this'),
-            ('/foo/', 'foo', 'this2'),
-            ('/foo/', 'bar', 'that'),
-            ('/foo/', 'bax', 'None'),
-            ('/foo/bar', 'baz', "'that2'"),
-            ('/foo/nex', 'baz', 'that2'),
-            # If 'foo' == 'this', then the mount point '/another' leaks into
-            # '/'.
-            ('/another/', 'foo', 'None'),
-        ]
-        for path, key, expected in tests:
-            self.getPage(path + '?key=' + key)
-            self.assertBody(expected)
-
-        expectedconf = {
-            # From CP defaults
-            'tools.log_headers.on': False,
-            'tools.log_tracebacks.on': True,
-            'request.show_tracebacks': True,
-            'log.screen': False,
-            'environment': 'test_suite',
-            'engine.autoreload.on': False,
-            # From global config
-            'luxuryyacht': 'throatwobblermangrove',
-            # From Root._cp_config
-            'bar': 'that',
-            # From Foo._cp_config
-            'baz': 'that2',
-            # From Foo.bar._cp_config
-            'foo': 'this3',
-            'bax': 'this4',
-        }
-        for key, expected in expectedconf.items():
-            self.getPage('/foo/bar?key=' + key)
-            self.assertBody(repr(expected))
-
-    def testUnrepr(self):
-        self.getPage('/repr?key=neg')
-        self.assertBody('-1234')
-
-        self.getPage('/repr?key=filename')
-        self.assertBody(repr(os.path.join(sys.prefix, 'hello.py')))
-
-        self.getPage('/repr?key=thing1')
-        self.assertBody(repr(cherrypy.lib.httputil.response_codes[404]))
-
-        if not getattr(cherrypy.server, 'using_apache', False):
-            # The object ID's won't match up when using Apache, since the
-            # server and client are running in different processes.
-            self.getPage('/repr?key=thing2')
-            from cherrypy.tutorial import thing2
-            self.assertBody(repr(thing2))
-
-        if not six.PY3:
-            self.getPage('/repr?key=thing3')
-            self.assertBody(repr(six.text_type('test')))
-
-        self.getPage('/repr?key=complex')
-        self.assertBody('(3+2j)')
-
-        self.getPage('/repr?key=mul')
-        self.assertBody('18')
-
-        self.getPage('/repr?key=stradd')
-        self.assertBody(repr('112233'))
-
-    def testRespNamespaces(self):
-        self.getPage('/foo/silly')
-        self.assertHeader('X-silly', 'sillyval')
-        self.assertBody('Hello world')
-
-    def testCustomNamespaces(self):
-        self.getPage('/raw/incr?num=12')
-        self.assertBody('13')
-
-        self.getPage('/dbscheme')
-        self.assertBody(r'sqlite///memory')
-
-    def testHandlerToolConfigOverride(self):
-        # Assert that config overrides tool constructor args. Above, we set
-        # the favicon in the page handler to be '../favicon.ico',
-        # but then overrode it in config to be './static/dirback.jpg'.
-        self.getPage('/favicon.ico')
-        self.assertBody(open(os.path.join(localDir, 'static/dirback.jpg'),
-                             'rb').read())
-
-    def test_request_body_namespace(self):
-        self.getPage('/plain', method='POST', headers=[
-            ('Content-Type', 'application/x-www-form-urlencoded'),
-            ('Content-Length', '13')],
-            body=b'\xff\xfex\x00=\xff\xfea\x00b\x00c\x00')
-        self.assertBody('abc')
-
-
-class VariableSubstitutionTests(unittest.TestCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_config(self):
-        from textwrap import dedent
-
-        # variable substitution with [DEFAULT]
-        conf = dedent("""
-        [DEFAULT]
-        dir = "/some/dir"
-        my.dir = %(dir)s + "/sub"
-
-        [my]
-        my.dir = %(dir)s + "/my/dir"
-        my.dir2 = %(my.dir)s + '/dir2'
-
-        """)
-
-        fp = StringIOFromNative(conf)
-
-        cherrypy.config.update(fp)
-        self.assertEqual(cherrypy.config['my']['my.dir'], '/some/dir/my/dir')
-        self.assertEqual(cherrypy.config['my']
-                         ['my.dir2'], '/some/dir/my/dir/dir2')
-
-
-class CallablesInConfigTest(unittest.TestCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_call_with_literal_dict(self):
-        from textwrap import dedent
-        conf = dedent("""
-        [my]
-        value = dict(**{'foo': 'bar'})
-        """)
-        fp = StringIOFromNative(conf)
-        cherrypy.config.update(fp)
-        self.assertEqual(cherrypy.config['my']['value'], {'foo': 'bar'})
-
-    def test_call_with_kwargs(self):
-        from textwrap import dedent
-        conf = dedent("""
-        [my]
-        value = dict(foo="buzz", **cherrypy._test_dict)
-        """)
-        test_dict = {
-            'foo': 'bar',
-            'bar': 'foo',
-            'fizz': 'buzz'
-        }
-        cherrypy._test_dict = test_dict
-        fp = StringIOFromNative(conf)
-        cherrypy.config.update(fp)
-        test_dict['foo'] = 'buzz'
-        self.assertEqual(cherrypy.config['my']['value']['foo'], 'buzz')
-        self.assertEqual(cherrypy.config['my']['value'], test_dict)
-        del cherrypy._test_dict
diff --git a/libraries/cherrypy/test/test_config_server.py b/libraries/cherrypy/test/test_config_server.py
deleted file mode 100644
index 7b183530..00000000
--- a/libraries/cherrypy/test/test_config_server.py
+++ /dev/null
@@ -1,126 +0,0 @@
-"""Tests for the CherryPy configuration system."""
-
-import os
-
-import cherrypy
-from cherrypy.test import helper
-
-
-localDir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-#                             Client-side code                             #
-
-
-class ServerConfigTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return cherrypy.request.wsgi_environ['SERVER_PORT']
-
-            @cherrypy.expose
-            def upload(self, file):
-                return 'Size: %s' % len(file.file.read())
-
-            @cherrypy.expose
-            @cherrypy.config(**{'request.body.maxbytes': 100})
-            def tinyupload(self):
-                return cherrypy.request.body.read()
-
-        cherrypy.tree.mount(Root())
-
-        cherrypy.config.update({
-            'server.socket_host': '0.0.0.0',
-            'server.socket_port': 9876,
-            'server.max_request_body_size': 200,
-            'server.max_request_header_size': 500,
-            'server.socket_timeout': 0.5,
-
-            # Test explicit server.instance
-            'server.2.instance': 'cherrypy._cpwsgi_server.CPWSGIServer',
-            'server.2.socket_port': 9877,
-
-            # Test non-numeric <servername>
-            # Also test default server.instance = builtin server
-            'server.yetanother.socket_port': 9878,
-        })
-
-    PORT = 9876
-
-    def testBasicConfig(self):
-        self.getPage('/')
-        self.assertBody(str(self.PORT))
-
-    def testAdditionalServers(self):
-        if self.scheme == 'https':
-            return self.skip('not available under ssl')
-        self.PORT = 9877
-        self.getPage('/')
-        self.assertBody(str(self.PORT))
-        self.PORT = 9878
-        self.getPage('/')
-        self.assertBody(str(self.PORT))
-
-    def testMaxRequestSizePerHandler(self):
-        if getattr(cherrypy.server, 'using_apache', False):
-            return self.skip('skipped due to known Apache differences... ')
-
-        self.getPage('/tinyupload', method='POST',
-                     headers=[('Content-Type', 'text/plain'),
-                              ('Content-Length', '100')],
-                     body='x' * 100)
-        self.assertStatus(200)
-        self.assertBody('x' * 100)
-
-        self.getPage('/tinyupload', method='POST',
-                     headers=[('Content-Type', 'text/plain'),
-                              ('Content-Length', '101')],
-                     body='x' * 101)
-        self.assertStatus(413)
-
-    def testMaxRequestSize(self):
-        if getattr(cherrypy.server, 'using_apache', False):
-            return self.skip('skipped due to known Apache differences... ')
-
-        for size in (500, 5000, 50000):
-            self.getPage('/', headers=[('From', 'x' * 500)])
-            self.assertStatus(413)
-
-        # Test for https://github.com/cherrypy/cherrypy/issues/421
-        # (Incorrect border condition in readline of SizeCheckWrapper).
-        # This hangs in rev 891 and earlier.
-        lines256 = 'x' * 248
-        self.getPage('/',
-                     headers=[('Host', '%s:%s' % (self.HOST, self.PORT)),
-                              ('From', lines256)])
-
-        # Test upload
-        cd = (
-            'Content-Disposition: form-data; '
-            'name="file"; '
-            'filename="hello.txt"'
-        )
-        body = '\r\n'.join([
-            '--x',
-            cd,
-            'Content-Type: text/plain',
-            '',
-            '%s',
-            '--x--'])
-        partlen = 200 - len(body)
-        b = body % ('x' * partlen)
-        h = [('Content-type', 'multipart/form-data; boundary=x'),
-             ('Content-Length', '%s' % len(b))]
-        self.getPage('/upload', h, 'POST', b)
-        self.assertBody('Size: %d' % partlen)
-
-        b = body % ('x' * 200)
-        h = [('Content-type', 'multipart/form-data; boundary=x'),
-             ('Content-Length', '%s' % len(b))]
-        self.getPage('/upload', h, 'POST', b)
-        self.assertStatus(413)
diff --git a/libraries/cherrypy/test/test_conn.py b/libraries/cherrypy/test/test_conn.py
deleted file mode 100644
index 7d60c6fb..00000000
--- a/libraries/cherrypy/test/test_conn.py
+++ /dev/null
@@ -1,873 +0,0 @@
-"""Tests for TCP connection handling, including proper and timely close."""
-
-import errno
-import socket
-import sys
-import time
-
-import six
-from six.moves import urllib
-from six.moves.http_client import BadStatusLine, HTTPConnection, NotConnected
-
-import pytest
-
-from cheroot.test import webtest
-
-import cherrypy
-from cherrypy._cpcompat import HTTPSConnection, ntob, tonative
-from cherrypy.test import helper
-
-
-timeout = 1
-pov = 'pPeErRsSiIsStTeEnNcCeE oOfF vViIsSiIoOnN'
-
-
-def setup_server():
-
-    def raise500():
-        raise cherrypy.HTTPError(500)
-
-    class Root:
-
-        @cherrypy.expose
-        def index(self):
-            return pov
-        page1 = index
-        page2 = index
-        page3 = index
-
-        @cherrypy.expose
-        def hello(self):
-            return 'Hello, world!'
-
-        @cherrypy.expose
-        def timeout(self, t):
-            return str(cherrypy.server.httpserver.timeout)
-
-        @cherrypy.expose
-        @cherrypy.config(**{'response.stream': True})
-        def stream(self, set_cl=False):
-            if set_cl:
-                cherrypy.response.headers['Content-Length'] = 10
-
-            def content():
-                for x in range(10):
-                    yield str(x)
-
-            return content()
-
-        @cherrypy.expose
-        def error(self, code=500):
-            raise cherrypy.HTTPError(code)
-
-        @cherrypy.expose
-        def upload(self):
-            if not cherrypy.request.method == 'POST':
-                raise AssertionError("'POST' != request.method %r" %
-                                     cherrypy.request.method)
-            return "thanks for '%s'" % cherrypy.request.body.read()
-
-        @cherrypy.expose
-        def custom(self, response_code):
-            cherrypy.response.status = response_code
-            return 'Code = %s' % response_code
-
-        @cherrypy.expose
-        @cherrypy.config(**{'hooks.on_start_resource': raise500})
-        def err_before_read(self):
-            return 'ok'
-
-        @cherrypy.expose
-        def one_megabyte_of_a(self):
-            return ['a' * 1024] * 1024
-
-        @cherrypy.expose
-        # Turn off the encoding tool so it doens't collapse
-        # our response body and reclaculate the Content-Length.
-        @cherrypy.config(**{'tools.encode.on': False})
-        def custom_cl(self, body, cl):
-            cherrypy.response.headers['Content-Length'] = cl
-            if not isinstance(body, list):
-                body = [body]
-            newbody = []
-            for chunk in body:
-                if isinstance(chunk, six.text_type):
-                    chunk = chunk.encode('ISO-8859-1')
-                newbody.append(chunk)
-            return newbody
-
-    cherrypy.tree.mount(Root())
-    cherrypy.config.update({
-        'server.max_request_body_size': 1001,
-        'server.socket_timeout': timeout,
-    })
-
-
-class ConnectionCloseTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_HTTP11(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        self.persistent = True
-
-        # Make the first request and assert there's no "Connection: close".
-        self.getPage('/')
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        self.assertNoHeader('Connection')
-
-        # Make another request on the same connection.
-        self.getPage('/page1')
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        self.assertNoHeader('Connection')
-
-        # Test client-side close.
-        self.getPage('/page2', headers=[('Connection', 'close')])
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        self.assertHeader('Connection', 'close')
-
-        # Make another request on the same connection, which should error.
-        self.assertRaises(NotConnected, self.getPage, '/')
-
-    def test_Streaming_no_len(self):
-        try:
-            self._streaming(set_cl=False)
-        finally:
-            try:
-                self.HTTP_CONN.close()
-            except (TypeError, AttributeError):
-                pass
-
-    def test_Streaming_with_len(self):
-        try:
-            self._streaming(set_cl=True)
-        finally:
-            try:
-                self.HTTP_CONN.close()
-            except (TypeError, AttributeError):
-                pass
-
-    def _streaming(self, set_cl):
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.PROTOCOL = 'HTTP/1.1'
-
-            self.persistent = True
-
-            # Make the first request and assert there's no "Connection: close".
-            self.getPage('/')
-            self.assertStatus('200 OK')
-            self.assertBody(pov)
-            self.assertNoHeader('Connection')
-
-            # Make another, streamed request on the same connection.
-            if set_cl:
-                # When a Content-Length is provided, the content should stream
-                # without closing the connection.
-                self.getPage('/stream?set_cl=Yes')
-                self.assertHeader('Content-Length')
-                self.assertNoHeader('Connection', 'close')
-                self.assertNoHeader('Transfer-Encoding')
-
-                self.assertStatus('200 OK')
-                self.assertBody('0123456789')
-            else:
-                # When no Content-Length response header is provided,
-                # streamed output will either close the connection, or use
-                # chunked encoding, to determine transfer-length.
-                self.getPage('/stream')
-                self.assertNoHeader('Content-Length')
-                self.assertStatus('200 OK')
-                self.assertBody('0123456789')
-
-                chunked_response = False
-                for k, v in self.headers:
-                    if k.lower() == 'transfer-encoding':
-                        if str(v) == 'chunked':
-                            chunked_response = True
-
-                if chunked_response:
-                    self.assertNoHeader('Connection', 'close')
-                else:
-                    self.assertHeader('Connection', 'close')
-
-                    # Make another request on the same connection, which should
-                    # error.
-                    self.assertRaises(NotConnected, self.getPage, '/')
-
-                # Try HEAD. See
-                # https://github.com/cherrypy/cherrypy/issues/864.
-                self.getPage('/stream', method='HEAD')
-                self.assertStatus('200 OK')
-                self.assertBody('')
-                self.assertNoHeader('Transfer-Encoding')
-        else:
-            self.PROTOCOL = 'HTTP/1.0'
-
-            self.persistent = True
-
-            # Make the first request and assert Keep-Alive.
-            self.getPage('/', headers=[('Connection', 'Keep-Alive')])
-            self.assertStatus('200 OK')
-            self.assertBody(pov)
-            self.assertHeader('Connection', 'Keep-Alive')
-
-            # Make another, streamed request on the same connection.
-            if set_cl:
-                # When a Content-Length is provided, the content should
-                # stream without closing the connection.
-                self.getPage('/stream?set_cl=Yes',
-                             headers=[('Connection', 'Keep-Alive')])
-                self.assertHeader('Content-Length')
-                self.assertHeader('Connection', 'Keep-Alive')
-                self.assertNoHeader('Transfer-Encoding')
-                self.assertStatus('200 OK')
-                self.assertBody('0123456789')
-            else:
-                # When a Content-Length is not provided,
-                # the server should close the connection.
-                self.getPage('/stream', headers=[('Connection', 'Keep-Alive')])
-                self.assertStatus('200 OK')
-                self.assertBody('0123456789')
-
-                self.assertNoHeader('Content-Length')
-                self.assertNoHeader('Connection', 'Keep-Alive')
-                self.assertNoHeader('Transfer-Encoding')
-
-                # Make another request on the same connection, which should
-                # error.
-                self.assertRaises(NotConnected, self.getPage, '/')
-
-    def test_HTTP10_KeepAlive(self):
-        self.PROTOCOL = 'HTTP/1.0'
-        if self.scheme == 'https':
-            self.HTTP_CONN = HTTPSConnection
-        else:
-            self.HTTP_CONN = HTTPConnection
-
-        # Test a normal HTTP/1.0 request.
-        self.getPage('/page2')
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        # Apache, for example, may emit a Connection header even for HTTP/1.0
-        # self.assertNoHeader("Connection")
-
-        # Test a keep-alive HTTP/1.0 request.
-        self.persistent = True
-
-        self.getPage('/page3', headers=[('Connection', 'Keep-Alive')])
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        self.assertHeader('Connection', 'Keep-Alive')
-
-        # Remove the keep-alive header again.
-        self.getPage('/page3')
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        # Apache, for example, may emit a Connection header even for HTTP/1.0
-        # self.assertNoHeader("Connection")
-
-
-class PipelineTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_HTTP11_Timeout(self):
-        # If we timeout without sending any data,
-        # the server will close the conn with a 408.
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Connect but send nothing.
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.auto_open = False
-        conn.connect()
-
-        # Wait for our socket timeout
-        time.sleep(timeout * 2)
-
-        # The request should have returned 408 already.
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 408)
-        conn.close()
-
-        # Connect but send half the headers only.
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.auto_open = False
-        conn.connect()
-        conn.send(b'GET /hello HTTP/1.1')
-        conn.send(('Host: %s' % self.HOST).encode('ascii'))
-
-        # Wait for our socket timeout
-        time.sleep(timeout * 2)
-
-        # The conn should have already sent 408.
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 408)
-        conn.close()
-
-    def test_HTTP11_Timeout_after_request(self):
-        # If we timeout after at least one request has succeeded,
-        # the server will close the conn without 408.
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Make an initial request
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('GET', '/timeout?t=%s' % timeout, skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 200)
-        self.body = response.read()
-        self.assertBody(str(timeout))
-
-        # Make a second request on the same socket
-        conn._output(b'GET /hello HTTP/1.1')
-        conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
-        conn._send_output()
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 200)
-        self.body = response.read()
-        self.assertBody('Hello, world!')
-
-        # Wait for our socket timeout
-        time.sleep(timeout * 2)
-
-        # Make another request on the same socket, which should error
-        conn._output(b'GET /hello HTTP/1.1')
-        conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
-        conn._send_output()
-        response = conn.response_class(conn.sock, method='GET')
-        try:
-            response.begin()
-        except Exception:
-            if not isinstance(sys.exc_info()[1],
-                              (socket.error, BadStatusLine)):
-                self.fail("Writing to timed out socket didn't fail"
-                          ' as it should have: %s' % sys.exc_info()[1])
-        else:
-            if response.status != 408:
-                self.fail("Writing to timed out socket didn't fail"
-                          ' as it should have: %s' %
-                          response.read())
-
-        conn.close()
-
-        # Make another request on a new socket, which should work
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('GET', '/', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 200)
-        self.body = response.read()
-        self.assertBody(pov)
-
-        # Make another request on the same socket,
-        # but timeout on the headers
-        conn.send(b'GET /hello HTTP/1.1')
-        # Wait for our socket timeout
-        time.sleep(timeout * 2)
-        response = conn.response_class(conn.sock, method='GET')
-        try:
-            response.begin()
-        except Exception:
-            if not isinstance(sys.exc_info()[1],
-                              (socket.error, BadStatusLine)):
-                self.fail("Writing to timed out socket didn't fail"
-                          ' as it should have: %s' % sys.exc_info()[1])
-        else:
-            self.fail("Writing to timed out socket didn't fail"
-                      ' as it should have: %s' %
-                      response.read())
-
-        conn.close()
-
-        # Retry the request on a new connection, which should work
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('GET', '/', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 200)
-        self.body = response.read()
-        self.assertBody(pov)
-        conn.close()
-
-    def test_HTTP11_pipelining(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Test pipelining. httplib doesn't support this directly.
-        self.persistent = True
-        conn = self.HTTP_CONN
-
-        # Put request 1
-        conn.putrequest('GET', '/hello', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-
-        for trial in range(5):
-            # Put next request
-            conn._output(b'GET /hello HTTP/1.1')
-            conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
-            conn._send_output()
-
-            # Retrieve previous response
-            response = conn.response_class(conn.sock, method='GET')
-            # there is a bug in python3 regarding the buffering of
-            # ``conn.sock``. Until that bug get's fixed we will
-            # monkey patch the ``response`` instance.
-            # https://bugs.python.org/issue23377
-            if six.PY3:
-                response.fp = conn.sock.makefile('rb', 0)
-            response.begin()
-            body = response.read(13)
-            self.assertEqual(response.status, 200)
-            self.assertEqual(body, b'Hello, world!')
-
-        # Retrieve final response
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        body = response.read()
-        self.assertEqual(response.status, 200)
-        self.assertEqual(body, b'Hello, world!')
-
-        conn.close()
-
-    def test_100_Continue(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        self.persistent = True
-        conn = self.HTTP_CONN
-
-        # Try a page without an Expect request header first.
-        # Note that httplib's response.begin automatically ignores
-        # 100 Continue responses, so we must manually check for it.
-        try:
-            conn.putrequest('POST', '/upload', skip_host=True)
-            conn.putheader('Host', self.HOST)
-            conn.putheader('Content-Type', 'text/plain')
-            conn.putheader('Content-Length', '4')
-            conn.endheaders()
-            conn.send(ntob("d'oh"))
-            response = conn.response_class(conn.sock, method='POST')
-            version, status, reason = response._read_status()
-            self.assertNotEqual(status, 100)
-        finally:
-            conn.close()
-
-        # Now try a page with an Expect header...
-        try:
-            conn.connect()
-            conn.putrequest('POST', '/upload', skip_host=True)
-            conn.putheader('Host', self.HOST)
-            conn.putheader('Content-Type', 'text/plain')
-            conn.putheader('Content-Length', '17')
-            conn.putheader('Expect', '100-continue')
-            conn.endheaders()
-            response = conn.response_class(conn.sock, method='POST')
-
-            # ...assert and then skip the 100 response
-            version, status, reason = response._read_status()
-            self.assertEqual(status, 100)
-            while True:
-                line = response.fp.readline().strip()
-                if line:
-                    self.fail(
-                        '100 Continue should not output any headers. Got %r' %
-                        line)
-                else:
-                    break
-
-            # ...send the body
-            body = b'I am a small file'
-            conn.send(body)
-
-            # ...get the final response
-            response.begin()
-            self.status, self.headers, self.body = webtest.shb(response)
-            self.assertStatus(200)
-            self.assertBody("thanks for '%s'" % body)
-        finally:
-            conn.close()
-
-
-class ConnectionTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_readall_or_close(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        if self.scheme == 'https':
-            self.HTTP_CONN = HTTPSConnection
-        else:
-            self.HTTP_CONN = HTTPConnection
-
-        # Test a max of 0 (the default) and then reset to what it was above.
-        old_max = cherrypy.server.max_request_body_size
-        for new_max in (0, old_max):
-            cherrypy.server.max_request_body_size = new_max
-
-            self.persistent = True
-            conn = self.HTTP_CONN
-
-            # Get a POST page with an error
-            conn.putrequest('POST', '/err_before_read', skip_host=True)
-            conn.putheader('Host', self.HOST)
-            conn.putheader('Content-Type', 'text/plain')
-            conn.putheader('Content-Length', '1000')
-            conn.putheader('Expect', '100-continue')
-            conn.endheaders()
-            response = conn.response_class(conn.sock, method='POST')
-
-            # ...assert and then skip the 100 response
-            version, status, reason = response._read_status()
-            self.assertEqual(status, 100)
-            while True:
-                skip = response.fp.readline().strip()
-                if not skip:
-                    break
-
-            # ...send the body
-            conn.send(ntob('x' * 1000))
-
-            # ...get the final response
-            response.begin()
-            self.status, self.headers, self.body = webtest.shb(response)
-            self.assertStatus(500)
-
-            # Now try a working page with an Expect header...
-            conn._output(b'POST /upload HTTP/1.1')
-            conn._output(ntob('Host: %s' % self.HOST, 'ascii'))
-            conn._output(b'Content-Type: text/plain')
-            conn._output(b'Content-Length: 17')
-            conn._output(b'Expect: 100-continue')
-            conn._send_output()
-            response = conn.response_class(conn.sock, method='POST')
-
-            # ...assert and then skip the 100 response
-            version, status, reason = response._read_status()
-            self.assertEqual(status, 100)
-            while True:
-                skip = response.fp.readline().strip()
-                if not skip:
-                    break
-
-            # ...send the body
-            body = b'I am a small file'
-            conn.send(body)
-
-            # ...get the final response
-            response.begin()
-            self.status, self.headers, self.body = webtest.shb(response)
-            self.assertStatus(200)
-            self.assertBody("thanks for '%s'" % body)
-            conn.close()
-
-    def test_No_Message_Body(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Set our HTTP_CONN to an instance so it persists between requests.
-        self.persistent = True
-
-        # Make the first request and assert there's no "Connection: close".
-        self.getPage('/')
-        self.assertStatus('200 OK')
-        self.assertBody(pov)
-        self.assertNoHeader('Connection')
-
-        # Make a 204 request on the same connection.
-        self.getPage('/custom/204')
-        self.assertStatus(204)
-        self.assertNoHeader('Content-Length')
-        self.assertBody('')
-        self.assertNoHeader('Connection')
-
-        # Make a 304 request on the same connection.
-        self.getPage('/custom/304')
-        self.assertStatus(304)
-        self.assertNoHeader('Content-Length')
-        self.assertBody('')
-        self.assertNoHeader('Connection')
-
-    def test_Chunked_Encoding(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        if (hasattr(self, 'harness') and
-                'modpython' in self.harness.__class__.__name__.lower()):
-            # mod_python forbids chunked encoding
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Set our HTTP_CONN to an instance so it persists between requests.
-        self.persistent = True
-        conn = self.HTTP_CONN
-
-        # Try a normal chunked request (with extensions)
-        body = ntob('8;key=value\r\nxx\r\nxxxx\r\n5\r\nyyyyy\r\n0\r\n'
-                    'Content-Type: application/json\r\n'
-                    '\r\n')
-        conn.putrequest('POST', '/upload', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.putheader('Transfer-Encoding', 'chunked')
-        conn.putheader('Trailer', 'Content-Type')
-        # Note that this is somewhat malformed:
-        # we shouldn't be sending Content-Length.
-        # RFC 2616 says the server should ignore it.
-        conn.putheader('Content-Length', '3')
-        conn.endheaders()
-        conn.send(body)
-        response = conn.getresponse()
-        self.status, self.headers, self.body = webtest.shb(response)
-        self.assertStatus('200 OK')
-        self.assertBody("thanks for '%s'" % b'xx\r\nxxxxyyyyy')
-
-        # Try a chunked request that exceeds server.max_request_body_size.
-        # Note that the delimiters and trailer are included.
-        body = ntob('3e3\r\n' + ('x' * 995) + '\r\n0\r\n\r\n')
-        conn.putrequest('POST', '/upload', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.putheader('Transfer-Encoding', 'chunked')
-        conn.putheader('Content-Type', 'text/plain')
-        # Chunked requests don't need a content-length
-        # #        conn.putheader("Content-Length", len(body))
-        conn.endheaders()
-        conn.send(body)
-        response = conn.getresponse()
-        self.status, self.headers, self.body = webtest.shb(response)
-        self.assertStatus(413)
-        conn.close()
-
-    def test_Content_Length_in(self):
-        # Try a non-chunked request where Content-Length exceeds
-        # server.max_request_body_size. Assert error before body send.
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('POST', '/upload', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.putheader('Content-Type', 'text/plain')
-        conn.putheader('Content-Length', '9999')
-        conn.endheaders()
-        response = conn.getresponse()
-        self.status, self.headers, self.body = webtest.shb(response)
-        self.assertStatus(413)
-        self.assertBody('The entity sent with the request exceeds '
-                        'the maximum allowed bytes.')
-        conn.close()
-
-    def test_Content_Length_out_preheaders(self):
-        # Try a non-chunked response where Content-Length is less than
-        # the actual bytes in the response body.
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('GET', '/custom_cl?body=I+have+too+many+bytes&cl=5',
-                        skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.getresponse()
-        self.status, self.headers, self.body = webtest.shb(response)
-        self.assertStatus(500)
-        self.assertBody(
-            'The requested resource returned more bytes than the '
-            'declared Content-Length.')
-        conn.close()
-
-    def test_Content_Length_out_postheaders(self):
-        # Try a non-chunked response where Content-Length is less than
-        # the actual bytes in the response body.
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest(
-            'GET', '/custom_cl?body=I+too&body=+have+too+many&cl=5',
-            skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.getresponse()
-        self.status, self.headers, self.body = webtest.shb(response)
-        self.assertStatus(200)
-        self.assertBody('I too')
-        conn.close()
-
-    def test_598(self):
-        tmpl = '{scheme}://{host}:{port}/one_megabyte_of_a/'
-        url = tmpl.format(
-            scheme=self.scheme,
-            host=self.HOST,
-            port=self.PORT,
-        )
-        remote_data_conn = urllib.request.urlopen(url)
-        buf = remote_data_conn.read(512)
-        time.sleep(timeout * 0.6)
-        remaining = (1024 * 1024) - 512
-        while remaining:
-            data = remote_data_conn.read(remaining)
-            if not data:
-                break
-            else:
-                buf += data
-            remaining -= len(data)
-
-        self.assertEqual(len(buf), 1024 * 1024)
-        self.assertEqual(buf, ntob('a' * 1024 * 1024))
-        self.assertEqual(remaining, 0)
-        remote_data_conn.close()
-
-
-def setup_upload_server():
-
-    class Root:
-        @cherrypy.expose
-        def upload(self):
-            if not cherrypy.request.method == 'POST':
-                raise AssertionError("'POST' != request.method %r" %
-                                     cherrypy.request.method)
-            return "thanks for '%s'" % tonative(cherrypy.request.body.read())
-
-    cherrypy.tree.mount(Root())
-    cherrypy.config.update({
-        'server.max_request_body_size': 1001,
-        'server.socket_timeout': 10,
-        'server.accepted_queue_size': 5,
-        'server.accepted_queue_timeout': 0.1,
-    })
-
-
-reset_names = 'ECONNRESET', 'WSAECONNRESET'
-socket_reset_errors = [
-    getattr(errno, name)
-    for name in reset_names
-    if hasattr(errno, name)
-]
-'reset error numbers available on this platform'
-
-socket_reset_errors += [
-    # Python 3.5 raises an http.client.RemoteDisconnected
-    # with this message
-    'Remote end closed connection without response',
-]
-
-
-class LimitedRequestQueueTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_upload_server)
-
-    @pytest.mark.xfail(reason='#1535')
-    def test_queue_full(self):
-        conns = []
-        overflow_conn = None
-
-        try:
-            # Make 15 initial requests and leave them open, which should use
-            # all of wsgiserver's WorkerThreads and fill its Queue.
-            for i in range(15):
-                conn = self.HTTP_CONN(self.HOST, self.PORT)
-                conn.putrequest('POST', '/upload', skip_host=True)
-                conn.putheader('Host', self.HOST)
-                conn.putheader('Content-Type', 'text/plain')
-                conn.putheader('Content-Length', '4')
-                conn.endheaders()
-                conns.append(conn)
-
-            # Now try a 16th conn, which should be closed by the
-            # server immediately.
-            overflow_conn = self.HTTP_CONN(self.HOST, self.PORT)
-            # Manually connect since httplib won't let us set a timeout
-            for res in socket.getaddrinfo(self.HOST, self.PORT, 0,
-                                          socket.SOCK_STREAM):
-                af, socktype, proto, canonname, sa = res
-                overflow_conn.sock = socket.socket(af, socktype, proto)
-                overflow_conn.sock.settimeout(5)
-                overflow_conn.sock.connect(sa)
-                break
-
-            overflow_conn.putrequest('GET', '/', skip_host=True)
-            overflow_conn.putheader('Host', self.HOST)
-            overflow_conn.endheaders()
-            response = overflow_conn.response_class(
-                overflow_conn.sock,
-                method='GET',
-            )
-            try:
-                response.begin()
-            except socket.error as exc:
-                if exc.args[0] in socket_reset_errors:
-                    pass  # Expected.
-                else:
-                    tmpl = (
-                        'Overflow conn did not get RST. '
-                        'Got {exc.args!r} instead'
-                    )
-                    raise AssertionError(tmpl.format(**locals()))
-            except BadStatusLine:
-                # This is a special case in OS X. Linux and Windows will
-                # RST correctly.
-                assert sys.platform == 'darwin'
-            else:
-                raise AssertionError('Overflow conn did not get RST ')
-        finally:
-            for conn in conns:
-                conn.send(b'done')
-                response = conn.response_class(conn.sock, method='POST')
-                response.begin()
-                self.body = response.read()
-                self.assertBody("thanks for 'done'")
-                self.assertEqual(response.status, 200)
-                conn.close()
-            if overflow_conn:
-                overflow_conn.close()
-
-
-class BadRequestTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_No_CRLF(self):
-        self.persistent = True
-
-        conn = self.HTTP_CONN
-        conn.send(b'GET /hello HTTP/1.1\n\n')
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.body = response.read()
-        self.assertBody('HTTP requires CRLF terminators')
-        conn.close()
-
-        conn.connect()
-        conn.send(b'GET /hello HTTP/1.1\r\n\n')
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.body = response.read()
-        self.assertBody('HTTP requires CRLF terminators')
-        conn.close()
diff --git a/libraries/cherrypy/test/test_core.py b/libraries/cherrypy/test/test_core.py
deleted file mode 100644
index 9834c1f3..00000000
--- a/libraries/cherrypy/test/test_core.py
+++ /dev/null
@@ -1,823 +0,0 @@
-# coding: utf-8
-
-"""Basic tests for the CherryPy core: request handling."""
-
-import os
-import sys
-import types
-
-import six
-
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy import _cptools, tools
-from cherrypy.lib import httputil, static
-
-from cherrypy.test._test_decorators import ExposeExamples
-from cherrypy.test import helper
-
-
-localDir = os.path.dirname(__file__)
-favicon_path = os.path.join(os.getcwd(), localDir, '../favicon.ico')
-
-#                             Client-side code                             #
-
-
-class CoreRequestHandlingTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'hello'
-
-            favicon_ico = tools.staticfile.handler(filename=favicon_path)
-
-            @cherrypy.expose
-            def defct(self, newct):
-                newct = 'text/%s' % newct
-                cherrypy.config.update({'tools.response_headers.on': True,
-                                        'tools.response_headers.headers':
-                                        [('Content-Type', newct)]})
-
-            @cherrypy.expose
-            def baseurl(self, path_info, relative=None):
-                return cherrypy.url(path_info, relative=bool(relative))
-
-        root = Root()
-        root.expose_dec = ExposeExamples()
-
-        class TestType(type):
-
-            """Metaclass which automatically exposes all functions in each
-            subclass, and adds an instance of the subclass as an attribute
-            of root.
-            """
-            def __init__(cls, name, bases, dct):
-                type.__init__(cls, name, bases, dct)
-                for value in six.itervalues(dct):
-                    if isinstance(value, types.FunctionType):
-                        value.exposed = True
-                setattr(root, name.lower(), cls())
-        Test = TestType('Test', (object, ), {})
-
-        @cherrypy.config(**{'tools.trailing_slash.on': False})
-        class URL(Test):
-
-            def index(self, path_info, relative=None):
-                if relative != 'server':
-                    relative = bool(relative)
-                return cherrypy.url(path_info, relative=relative)
-
-            def leaf(self, path_info, relative=None):
-                if relative != 'server':
-                    relative = bool(relative)
-                return cherrypy.url(path_info, relative=relative)
-
-            def qs(self, qs):
-                return cherrypy.url(qs=qs)
-
-        def log_status():
-            Status.statuses.append(cherrypy.response.status)
-        cherrypy.tools.log_status = cherrypy.Tool(
-            'on_end_resource', log_status)
-
-        class Status(Test):
-
-            def index(self):
-                return 'normal'
-
-            def blank(self):
-                cherrypy.response.status = ''
-
-            # According to RFC 2616, new status codes are OK as long as they
-            # are between 100 and 599.
-
-            # Here is an illegal code...
-            def illegal(self):
-                cherrypy.response.status = 781
-                return 'oops'
-
-            # ...and here is an unknown but legal code.
-            def unknown(self):
-                cherrypy.response.status = '431 My custom error'
-                return 'funky'
-
-            # Non-numeric code
-            def bad(self):
-                cherrypy.response.status = 'error'
-                return 'bad news'
-
-            statuses = []
-
-            @cherrypy.config(**{'tools.log_status.on': True})
-            def on_end_resource_stage(self):
-                return repr(self.statuses)
-
-        class Redirect(Test):
-
-            @cherrypy.config(**{
-                'tools.err_redirect.on': True,
-                'tools.err_redirect.url': '/errpage',
-                'tools.err_redirect.internal': False,
-            })
-            class Error:
-                @cherrypy.expose
-                def index(self):
-                    raise NameError('redirect_test')
-
-            error = Error()
-
-            def index(self):
-                return 'child'
-
-            def custom(self, url, code):
-                raise cherrypy.HTTPRedirect(url, code)
-
-            @cherrypy.config(**{'tools.trailing_slash.extra': True})
-            def by_code(self, code):
-                raise cherrypy.HTTPRedirect('somewhere%20else', code)
-
-            def nomodify(self):
-                raise cherrypy.HTTPRedirect('', 304)
-
-            def proxy(self):
-                raise cherrypy.HTTPRedirect('proxy', 305)
-
-            def stringify(self):
-                return str(cherrypy.HTTPRedirect('/'))
-
-            def fragment(self, frag):
-                raise cherrypy.HTTPRedirect('/some/url#%s' % frag)
-
-            def url_with_quote(self):
-                raise cherrypy.HTTPRedirect("/some\"url/that'we/want")
-
-            def url_with_xss(self):
-                raise cherrypy.HTTPRedirect(
-                    "/some<script>alert(1);</script>url/that'we/want")
-
-            def url_with_unicode(self):
-                raise cherrypy.HTTPRedirect(ntou('тест', 'utf-8'))
-
-        def login_redir():
-            if not getattr(cherrypy.request, 'login', None):
-                raise cherrypy.InternalRedirect('/internalredirect/login')
-        tools.login_redir = _cptools.Tool('before_handler', login_redir)
-
-        def redir_custom():
-            raise cherrypy.InternalRedirect('/internalredirect/custom_err')
-
-        class InternalRedirect(Test):
-
-            def index(self):
-                raise cherrypy.InternalRedirect('/')
-
-            @cherrypy.expose
-            @cherrypy.config(**{'hooks.before_error_response': redir_custom})
-            def choke(self):
-                return 3 / 0
-
-            def relative(self, a, b):
-                raise cherrypy.InternalRedirect('cousin?t=6')
-
-            def cousin(self, t):
-                assert cherrypy.request.prev.closed
-                return cherrypy.request.prev.query_string
-
-            def petshop(self, user_id):
-                if user_id == 'parrot':
-                    # Trade it for a slug when redirecting
-                    raise cherrypy.InternalRedirect(
-                        '/image/getImagesByUser?user_id=slug')
-                elif user_id == 'terrier':
-                    # Trade it for a fish when redirecting
-                    raise cherrypy.InternalRedirect(
-                        '/image/getImagesByUser?user_id=fish')
-                else:
-                    # This should pass the user_id through to getImagesByUser
-                    raise cherrypy.InternalRedirect(
-                        '/image/getImagesByUser?user_id=%s' % str(user_id))
-
-            # We support Python 2.3, but the @-deco syntax would look like
-            # this:
-            # @tools.login_redir()
-            def secure(self):
-                return 'Welcome!'
-            secure = tools.login_redir()(secure)
-            # Since calling the tool returns the same function you pass in,
-            # you could skip binding the return value, and just write:
-            # tools.login_redir()(secure)
-
-            def login(self):
-                return 'Please log in'
-
-            def custom_err(self):
-                return 'Something went horribly wrong.'
-
-            @cherrypy.config(**{'hooks.before_request_body': redir_custom})
-            def early_ir(self, arg):
-                return 'whatever'
-
-        class Image(Test):
-
-            def getImagesByUser(self, user_id):
-                return '0 images for %s' % user_id
-
-        class Flatten(Test):
-
-            def as_string(self):
-                return 'content'
-
-            def as_list(self):
-                return ['con', 'tent']
-
-            def as_yield(self):
-                yield b'content'
-
-            @cherrypy.config(**{'tools.flatten.on': True})
-            def as_dblyield(self):
-                yield self.as_yield()
-
-            def as_refyield(self):
-                for chunk in self.as_yield():
-                    yield chunk
-
-        class Ranges(Test):
-
-            def get_ranges(self, bytes):
-                return repr(httputil.get_ranges('bytes=%s' % bytes, 8))
-
-            def slice_file(self):
-                path = os.path.join(os.getcwd(), os.path.dirname(__file__))
-                return static.serve_file(
-                    os.path.join(path, 'static/index.html'))
-
-        class Cookies(Test):
-
-            def single(self, name):
-                cookie = cherrypy.request.cookie[name]
-                # Python2's SimpleCookie.__setitem__ won't take unicode keys.
-                cherrypy.response.cookie[str(name)] = cookie.value
-
-            def multiple(self, names):
-                list(map(self.single, names))
-
-        def append_headers(header_list, debug=False):
-            if debug:
-                cherrypy.log(
-                    'Extending response headers with %s' % repr(header_list),
-                    'TOOLS.APPEND_HEADERS')
-            cherrypy.serving.response.header_list.extend(header_list)
-        cherrypy.tools.append_headers = cherrypy.Tool(
-            'on_end_resource', append_headers)
-
-        class MultiHeader(Test):
-
-            def header_list(self):
-                pass
-            header_list = cherrypy.tools.append_headers(header_list=[
-                (b'WWW-Authenticate', b'Negotiate'),
-                (b'WWW-Authenticate', b'Basic realm="foo"'),
-            ])(header_list)
-
-            def commas(self):
-                cherrypy.response.headers[
-                    'WWW-Authenticate'] = 'Negotiate,Basic realm="foo"'
-
-        cherrypy.tree.mount(root)
-
-    def testStatus(self):
-        self.getPage('/status/')
-        self.assertBody('normal')
-        self.assertStatus(200)
-
-        self.getPage('/status/blank')
-        self.assertBody('')
-        self.assertStatus(200)
-
-        self.getPage('/status/illegal')
-        self.assertStatus(500)
-        msg = 'Illegal response status from server (781 is out of range).'
-        self.assertErrorPage(500, msg)
-
-        if not getattr(cherrypy.server, 'using_apache', False):
-            self.getPage('/status/unknown')
-            self.assertBody('funky')
-            self.assertStatus(431)
-
-        self.getPage('/status/bad')
-        self.assertStatus(500)
-        msg = "Illegal response status from server ('error' is non-numeric)."
-        self.assertErrorPage(500, msg)
-
-    def test_on_end_resource_status(self):
-        self.getPage('/status/on_end_resource_stage')
-        self.assertBody('[]')
-        self.getPage('/status/on_end_resource_stage')
-        self.assertBody(repr(['200 OK']))
-
-    def testSlashes(self):
-        # Test that requests for index methods without a trailing slash
-        # get redirected to the same URI path with a trailing slash.
-        # Make sure GET params are preserved.
-        self.getPage('/redirect?id=3')
-        self.assertStatus(301)
-        self.assertMatchesBody(
-            '<a href=([\'"])%s/redirect/[?]id=3\\1>'
-            '%s/redirect/[?]id=3</a>' % (self.base(), self.base())
-        )
-
-        if self.prefix():
-            # Corner case: the "trailing slash" redirect could be tricky if
-            # we're using a virtual root and the URI is "/vroot" (no slash).
-            self.getPage('')
-            self.assertStatus(301)
-            self.assertMatchesBody("<a href=(['\"])%s/\\1>%s/</a>" %
-                                   (self.base(), self.base()))
-
-        # Test that requests for NON-index methods WITH a trailing slash
-        # get redirected to the same URI path WITHOUT a trailing slash.
-        # Make sure GET params are preserved.
-        self.getPage('/redirect/by_code/?code=307')
-        self.assertStatus(301)
-        self.assertMatchesBody(
-            "<a href=(['\"])%s/redirect/by_code[?]code=307\\1>"
-            '%s/redirect/by_code[?]code=307</a>'
-            % (self.base(), self.base())
-        )
-
-        # If the trailing_slash tool is off, CP should just continue
-        # as if the slashes were correct. But it needs some help
-        # inside cherrypy.url to form correct output.
-        self.getPage('/url?path_info=page1')
-        self.assertBody('%s/url/page1' % self.base())
-        self.getPage('/url/leaf/?path_info=page1')
-        self.assertBody('%s/url/page1' % self.base())
-
-    def testRedirect(self):
-        self.getPage('/redirect/')
-        self.assertBody('child')
-        self.assertStatus(200)
-
-        self.getPage('/redirect/by_code?code=300')
-        self.assertMatchesBody(
-            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
-        self.assertStatus(300)
-
-        self.getPage('/redirect/by_code?code=301')
-        self.assertMatchesBody(
-            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
-        self.assertStatus(301)
-
-        self.getPage('/redirect/by_code?code=302')
-        self.assertMatchesBody(
-            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
-        self.assertStatus(302)
-
-        self.getPage('/redirect/by_code?code=303')
-        self.assertMatchesBody(
-            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
-        self.assertStatus(303)
-
-        self.getPage('/redirect/by_code?code=307')
-        self.assertMatchesBody(
-            r"<a href=(['\"])(.*)somewhere%20else\1>\2somewhere%20else</a>")
-        self.assertStatus(307)
-
-        self.getPage('/redirect/nomodify')
-        self.assertBody('')
-        self.assertStatus(304)
-
-        self.getPage('/redirect/proxy')
-        self.assertBody('')
-        self.assertStatus(305)
-
-        # HTTPRedirect on error
-        self.getPage('/redirect/error/')
-        self.assertStatus(('302 Found', '303 See Other'))
-        self.assertInBody('/errpage')
-
-        # Make sure str(HTTPRedirect()) works.
-        self.getPage('/redirect/stringify', protocol='HTTP/1.0')
-        self.assertStatus(200)
-        self.assertBody("(['%s/'], 302)" % self.base())
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.getPage('/redirect/stringify', protocol='HTTP/1.1')
-            self.assertStatus(200)
-            self.assertBody("(['%s/'], 303)" % self.base())
-
-        # check that #fragments are handled properly
-        # http://skrb.org/ietf/http_errata.html#location-fragments
-        frag = 'foo'
-        self.getPage('/redirect/fragment/%s' % frag)
-        self.assertMatchesBody(
-            r"<a href=(['\"])(.*)\/some\/url\#%s\1>\2\/some\/url\#%s</a>" % (
-                frag, frag))
-        loc = self.assertHeader('Location')
-        assert loc.endswith('#%s' % frag)
-        self.assertStatus(('302 Found', '303 See Other'))
-
-        # check injection protection
-        # See https://github.com/cherrypy/cherrypy/issues/1003
-        self.getPage(
-            '/redirect/custom?'
-            'code=303&url=/foobar/%0d%0aSet-Cookie:%20somecookie=someval')
-        self.assertStatus(303)
-        loc = self.assertHeader('Location')
-        assert 'Set-Cookie' in loc
-        self.assertNoHeader('Set-Cookie')
-
-        def assertValidXHTML():
-            from xml.etree import ElementTree
-            try:
-                ElementTree.fromstring(
-                    '<html><body>%s</body></html>' % self.body,
-                )
-            except ElementTree.ParseError:
-                self._handlewebError(
-                    'automatically generated redirect did not '
-                    'generate well-formed html',
-                )
-
-        # check redirects to URLs generated valid HTML - we check this
-        # by seeing if it appears as valid XHTML.
-        self.getPage('/redirect/by_code?code=303')
-        self.assertStatus(303)
-        assertValidXHTML()
-
-        # do the same with a url containing quote characters.
-        self.getPage('/redirect/url_with_quote')
-        self.assertStatus(303)
-        assertValidXHTML()
-
-    def test_redirect_with_xss(self):
-        """A redirect to a URL with HTML injected should result
-        in page contents escaped."""
-        self.getPage('/redirect/url_with_xss')
-        self.assertStatus(303)
-        assert b'<script>' not in self.body
-        assert b'&lt;script&gt;' in self.body
-
-    def test_redirect_with_unicode(self):
-        """
-        A redirect to a URL with Unicode should return a Location
-        header containing that Unicode URL.
-        """
-        # test disabled due to #1440
-        return
-        self.getPage('/redirect/url_with_unicode')
-        self.assertStatus(303)
-        loc = self.assertHeader('Location')
-        assert ntou('тест', encoding='utf-8') in loc
-
-    def test_InternalRedirect(self):
-        # InternalRedirect
-        self.getPage('/internalredirect/')
-        self.assertBody('hello')
-        self.assertStatus(200)
-
-        # Test passthrough
-        self.getPage(
-            '/internalredirect/petshop?user_id=Sir-not-appearing-in-this-film')
-        self.assertBody('0 images for Sir-not-appearing-in-this-film')
-        self.assertStatus(200)
-
-        # Test args
-        self.getPage('/internalredirect/petshop?user_id=parrot')
-        self.assertBody('0 images for slug')
-        self.assertStatus(200)
-
-        # Test POST
-        self.getPage('/internalredirect/petshop', method='POST',
-                     body='user_id=terrier')
-        self.assertBody('0 images for fish')
-        self.assertStatus(200)
-
-        # Test ir before body read
-        self.getPage('/internalredirect/early_ir', method='POST',
-                     body='arg=aha!')
-        self.assertBody('Something went horribly wrong.')
-        self.assertStatus(200)
-
-        self.getPage('/internalredirect/secure')
-        self.assertBody('Please log in')
-        self.assertStatus(200)
-
-        # Relative path in InternalRedirect.
-        # Also tests request.prev.
-        self.getPage('/internalredirect/relative?a=3&b=5')
-        self.assertBody('a=3&b=5')
-        self.assertStatus(200)
-
-        # InternalRedirect on error
-        self.getPage('/internalredirect/choke')
-        self.assertStatus(200)
-        self.assertBody('Something went horribly wrong.')
-
-    def testFlatten(self):
-        for url in ['/flatten/as_string', '/flatten/as_list',
-                    '/flatten/as_yield', '/flatten/as_dblyield',
-                    '/flatten/as_refyield']:
-            self.getPage(url)
-            self.assertBody('content')
-
-    def testRanges(self):
-        self.getPage('/ranges/get_ranges?bytes=3-6')
-        self.assertBody('[(3, 7)]')
-
-        # Test multiple ranges and a suffix-byte-range-spec, for good measure.
-        self.getPage('/ranges/get_ranges?bytes=2-4,-1')
-        self.assertBody('[(2, 5), (7, 8)]')
-
-        # Test a suffix-byte-range longer than the content
-        # length. Note that in this test, the content length
-        # is 8 bytes.
-        self.getPage('/ranges/get_ranges?bytes=-100')
-        self.assertBody('[(0, 8)]')
-
-        # Get a partial file.
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
-            self.assertStatus(206)
-            self.assertHeader('Content-Type', 'text/html;charset=utf-8')
-            self.assertHeader('Content-Range', 'bytes 2-5/14')
-            self.assertBody('llo,')
-
-            # What happens with overlapping ranges (and out of order, too)?
-            self.getPage('/ranges/slice_file', [('Range', 'bytes=4-6,2-5')])
-            self.assertStatus(206)
-            ct = self.assertHeader('Content-Type')
-            expected_type = 'multipart/byteranges; boundary='
-            self.assert_(ct.startswith(expected_type))
-            boundary = ct[len(expected_type):]
-            expected_body = ('\r\n--%s\r\n'
-                             'Content-type: text/html\r\n'
-                             'Content-range: bytes 4-6/14\r\n'
-                             '\r\n'
-                             'o, \r\n'
-                             '--%s\r\n'
-                             'Content-type: text/html\r\n'
-                             'Content-range: bytes 2-5/14\r\n'
-                             '\r\n'
-                             'llo,\r\n'
-                             '--%s--\r\n' % (boundary, boundary, boundary))
-            self.assertBody(expected_body)
-            self.assertHeader('Content-Length')
-
-            # Test "416 Requested Range Not Satisfiable"
-            self.getPage('/ranges/slice_file', [('Range', 'bytes=2300-2900')])
-            self.assertStatus(416)
-            # "When this status code is returned for a byte-range request,
-            # the response SHOULD include a Content-Range entity-header
-            # field specifying the current length of the selected resource"
-            self.assertHeader('Content-Range', 'bytes */14')
-        elif cherrypy.server.protocol_version == 'HTTP/1.0':
-            # Test Range behavior with HTTP/1.0 request
-            self.getPage('/ranges/slice_file', [('Range', 'bytes=2-5')])
-            self.assertStatus(200)
-            self.assertBody('Hello, world\r\n')
-
-    def testFavicon(self):
-        # favicon.ico is served by staticfile.
-        icofilename = os.path.join(localDir, '../favicon.ico')
-        icofile = open(icofilename, 'rb')
-        data = icofile.read()
-        icofile.close()
-
-        self.getPage('/favicon.ico')
-        self.assertBody(data)
-
-    def skip_if_bad_cookies(self):
-        """
-        cookies module fails to reject invalid cookies
-        https://github.com/cherrypy/cherrypy/issues/1405
-        """
-        cookies = sys.modules.get('http.cookies')
-        _is_legal_key = getattr(cookies, '_is_legal_key', lambda x: False)
-        if not _is_legal_key(','):
-            return
-        issue = 'http://bugs.python.org/issue26302'
-        tmpl = 'Broken cookies module ({issue})'
-        self.skip(tmpl.format(**locals()))
-
-    def testCookies(self):
-        self.skip_if_bad_cookies()
-
-        self.getPage('/cookies/single?name=First',
-                     [('Cookie', 'First=Dinsdale;')])
-        self.assertHeader('Set-Cookie', 'First=Dinsdale')
-
-        self.getPage('/cookies/multiple?names=First&names=Last',
-                     [('Cookie', 'First=Dinsdale; Last=Piranha;'),
-                      ])
-        self.assertHeader('Set-Cookie', 'First=Dinsdale')
-        self.assertHeader('Set-Cookie', 'Last=Piranha')
-
-        self.getPage('/cookies/single?name=Something-With%2CComma',
-                     [('Cookie', 'Something-With,Comma=some-value')])
-        self.assertStatus(400)
-
-    def testDefaultContentType(self):
-        self.getPage('/')
-        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
-        self.getPage('/defct/plain')
-        self.getPage('/')
-        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
-        self.getPage('/defct/html')
-
-    def test_multiple_headers(self):
-        self.getPage('/multiheader/header_list')
-        self.assertEqual(
-            [(k, v) for k, v in self.headers if k == 'WWW-Authenticate'],
-            [('WWW-Authenticate', 'Negotiate'),
-             ('WWW-Authenticate', 'Basic realm="foo"'),
-             ])
-        self.getPage('/multiheader/commas')
-        self.assertHeader('WWW-Authenticate', 'Negotiate,Basic realm="foo"')
-
-    def test_cherrypy_url(self):
-        # Input relative to current
-        self.getPage('/url/leaf?path_info=page1')
-        self.assertBody('%s/url/page1' % self.base())
-        self.getPage('/url/?path_info=page1')
-        self.assertBody('%s/url/page1' % self.base())
-        # Other host header
-        host = 'www.mydomain.example'
-        self.getPage('/url/leaf?path_info=page1',
-                     headers=[('Host', host)])
-        self.assertBody('%s://%s/url/page1' % (self.scheme, host))
-
-        # Input is 'absolute'; that is, relative to script_name
-        self.getPage('/url/leaf?path_info=/page1')
-        self.assertBody('%s/page1' % self.base())
-        self.getPage('/url/?path_info=/page1')
-        self.assertBody('%s/page1' % self.base())
-
-        # Single dots
-        self.getPage('/url/leaf?path_info=./page1')
-        self.assertBody('%s/url/page1' % self.base())
-        self.getPage('/url/leaf?path_info=other/./page1')
-        self.assertBody('%s/url/other/page1' % self.base())
-        self.getPage('/url/?path_info=/other/./page1')
-        self.assertBody('%s/other/page1' % self.base())
-        self.getPage('/url/?path_info=/other/././././page1')
-        self.assertBody('%s/other/page1' % self.base())
-
-        # Double dots
-        self.getPage('/url/leaf?path_info=../page1')
-        self.assertBody('%s/page1' % self.base())
-        self.getPage('/url/leaf?path_info=other/../page1')
-        self.assertBody('%s/url/page1' % self.base())
-        self.getPage('/url/leaf?path_info=/other/../page1')
-        self.assertBody('%s/page1' % self.base())
-        self.getPage('/url/leaf?path_info=/other/../../../page1')
-        self.assertBody('%s/page1' % self.base())
-        self.getPage('/url/leaf?path_info=/other/../../../../../page1')
-        self.assertBody('%s/page1' % self.base())
-
-        # qs param is not normalized as a path
-        self.getPage('/url/qs?qs=/other')
-        self.assertBody('%s/url/qs?/other' % self.base())
-        self.getPage('/url/qs?qs=/other/../page1')
-        self.assertBody('%s/url/qs?/other/../page1' % self.base())
-        self.getPage('/url/qs?qs=../page1')
-        self.assertBody('%s/url/qs?../page1' % self.base())
-        self.getPage('/url/qs?qs=../../page1')
-        self.assertBody('%s/url/qs?../../page1' % self.base())
-
-        # Output relative to current path or script_name
-        self.getPage('/url/?path_info=page1&relative=True')
-        self.assertBody('page1')
-        self.getPage('/url/leaf?path_info=/page1&relative=True')
-        self.assertBody('../page1')
-        self.getPage('/url/leaf?path_info=page1&relative=True')
-        self.assertBody('page1')
-        self.getPage('/url/leaf?path_info=leaf/page1&relative=True')
-        self.assertBody('leaf/page1')
-        self.getPage('/url/leaf?path_info=../page1&relative=True')
-        self.assertBody('../page1')
-        self.getPage('/url/?path_info=other/../page1&relative=True')
-        self.assertBody('page1')
-
-        # Output relative to /
-        self.getPage('/baseurl?path_info=ab&relative=True')
-        self.assertBody('ab')
-        # Output relative to /
-        self.getPage('/baseurl?path_info=/ab&relative=True')
-        self.assertBody('ab')
-
-        # absolute-path references ("server-relative")
-        # Input relative to current
-        self.getPage('/url/leaf?path_info=page1&relative=server')
-        self.assertBody('/url/page1')
-        self.getPage('/url/?path_info=page1&relative=server')
-        self.assertBody('/url/page1')
-        # Input is 'absolute'; that is, relative to script_name
-        self.getPage('/url/leaf?path_info=/page1&relative=server')
-        self.assertBody('/page1')
-        self.getPage('/url/?path_info=/page1&relative=server')
-        self.assertBody('/page1')
-
-    def test_expose_decorator(self):
-        # Test @expose
-        self.getPage('/expose_dec/no_call')
-        self.assertStatus(200)
-        self.assertBody('Mr E. R. Bradshaw')
-
-        # Test @expose()
-        self.getPage('/expose_dec/call_empty')
-        self.assertStatus(200)
-        self.assertBody('Mrs. B.J. Smegma')
-
-        # Test @expose("alias")
-        self.getPage('/expose_dec/call_alias')
-        self.assertStatus(200)
-        self.assertBody('Mr Nesbitt')
-        # Does the original name work?
-        self.getPage('/expose_dec/nesbitt')
-        self.assertStatus(200)
-        self.assertBody('Mr Nesbitt')
-
-        # Test @expose(["alias1", "alias2"])
-        self.getPage('/expose_dec/alias1')
-        self.assertStatus(200)
-        self.assertBody('Mr Ken Andrews')
-        self.getPage('/expose_dec/alias2')
-        self.assertStatus(200)
-        self.assertBody('Mr Ken Andrews')
-        # Does the original name work?
-        self.getPage('/expose_dec/andrews')
-        self.assertStatus(200)
-        self.assertBody('Mr Ken Andrews')
-
-        # Test @expose(alias="alias")
-        self.getPage('/expose_dec/alias3')
-        self.assertStatus(200)
-        self.assertBody('Mr. and Mrs. Watson')
-
-
-class ErrorTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        def break_header():
-            # Add a header after finalize that is invalid
-            cherrypy.serving.response.header_list.append((2, 3))
-        cherrypy.tools.break_header = cherrypy.Tool(
-            'on_end_resource', break_header)
-
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'hello'
-
-            @cherrypy.config(**{'tools.break_header.on': True})
-            def start_response_error(self):
-                return 'salud!'
-
-            @cherrypy.expose
-            def stat(self, path):
-                with cherrypy.HTTPError.handle(OSError, 404):
-                    os.stat(path)
-
-        root = Root()
-
-        cherrypy.tree.mount(root)
-
-    def test_start_response_error(self):
-        self.getPage('/start_response_error')
-        self.assertStatus(500)
-        self.assertInBody(
-            'TypeError: response.header_list key 2 is not a byte string.')
-
-    def test_contextmanager(self):
-        self.getPage('/stat/missing')
-        self.assertStatus(404)
-        body_text = self.body.decode('utf-8')
-        assert (
-            'No such file or directory' in body_text or
-            'cannot find the file specified' in body_text
-        )
-
-
-class TestBinding:
-    def test_bind_ephemeral_port(self):
-        """
-        A server configured to bind to port 0 will bind to an ephemeral
-        port and indicate that port number on startup.
-        """
-        cherrypy.config.reset()
-        bind_ephemeral_conf = {
-            'server.socket_port': 0,
-        }
-        cherrypy.config.update(bind_ephemeral_conf)
-        cherrypy.engine.start()
-        assert cherrypy.server.bound_addr != cherrypy.server.bind_addr
-        _host, port = cherrypy.server.bound_addr
-        assert port > 0
-        cherrypy.engine.stop()
-        assert cherrypy.server.bind_addr == cherrypy.server.bound_addr
diff --git a/libraries/cherrypy/test/test_dynamicobjectmapping.py b/libraries/cherrypy/test/test_dynamicobjectmapping.py
deleted file mode 100644
index 725a3ce0..00000000
--- a/libraries/cherrypy/test/test_dynamicobjectmapping.py
+++ /dev/null
@@ -1,424 +0,0 @@
-import six
-
-import cherrypy
-from cherrypy.test import helper
-
-script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
-
-
-def setup_server():
-    class SubSubRoot:
-
-        @cherrypy.expose
-        def index(self):
-            return 'SubSubRoot index'
-
-        @cherrypy.expose
-        def default(self, *args):
-            return 'SubSubRoot default'
-
-        @cherrypy.expose
-        def handler(self):
-            return 'SubSubRoot handler'
-
-        @cherrypy.expose
-        def dispatch(self):
-            return 'SubSubRoot dispatch'
-
-    subsubnodes = {
-        '1': SubSubRoot(),
-        '2': SubSubRoot(),
-    }
-
-    class SubRoot:
-
-        @cherrypy.expose
-        def index(self):
-            return 'SubRoot index'
-
-        @cherrypy.expose
-        def default(self, *args):
-            return 'SubRoot %s' % (args,)
-
-        @cherrypy.expose
-        def handler(self):
-            return 'SubRoot handler'
-
-        def _cp_dispatch(self, vpath):
-            return subsubnodes.get(vpath[0], None)
-
-    subnodes = {
-        '1': SubRoot(),
-        '2': SubRoot(),
-    }
-
-    class Root:
-
-        @cherrypy.expose
-        def index(self):
-            return 'index'
-
-        @cherrypy.expose
-        def default(self, *args):
-            return 'default %s' % (args,)
-
-        @cherrypy.expose
-        def handler(self):
-            return 'handler'
-
-        def _cp_dispatch(self, vpath):
-            return subnodes.get(vpath[0])
-
-    # -------------------------------------------------------------------------
-    # DynamicNodeAndMethodDispatcher example.
-    # This example exposes a fairly naive HTTP api
-    class User(object):
-
-        def __init__(self, id, name):
-            self.id = id
-            self.name = name
-
-        def __unicode__(self):
-            return six.text_type(self.name)
-
-        def __str__(self):
-            return str(self.name)
-
-    user_lookup = {
-        1: User(1, 'foo'),
-        2: User(2, 'bar'),
-    }
-
-    def make_user(name, id=None):
-        if not id:
-            id = max(*list(user_lookup.keys())) + 1
-        user_lookup[id] = User(id, name)
-        return id
-
-    @cherrypy.expose
-    class UserContainerNode(object):
-
-        def POST(self, name):
-            """
-            Allow the creation of a new Object
-            """
-            return 'POST %d' % make_user(name)
-
-        def GET(self):
-            return six.text_type(sorted(user_lookup.keys()))
-
-        def dynamic_dispatch(self, vpath):
-            try:
-                id = int(vpath[0])
-            except (ValueError, IndexError):
-                return None
-            return UserInstanceNode(id)
-
-    @cherrypy.expose
-    class UserInstanceNode(object):
-
-        def __init__(self, id):
-            self.id = id
-            self.user = user_lookup.get(id, None)
-
-            # For all but PUT methods there MUST be a valid user identified
-            # by self.id
-            if not self.user and cherrypy.request.method != 'PUT':
-                raise cherrypy.HTTPError(404)
-
-        def GET(self, *args, **kwargs):
-            """
-            Return the appropriate representation of the instance.
-            """
-            return six.text_type(self.user)
-
-        def POST(self, name):
-            """
-            Update the fields of the user instance.
-            """
-            self.user.name = name
-            return 'POST %d' % self.user.id
-
-        def PUT(self, name):
-            """
-            Create a new user with the specified id, or edit it if it already
-            exists
-            """
-            if self.user:
-                # Edit the current user
-                self.user.name = name
-                return 'PUT %d' % self.user.id
-            else:
-                # Make a new user with said attributes.
-                return 'PUT %d' % make_user(name, self.id)
-
-        def DELETE(self):
-            """
-            Delete the user specified at the id.
-            """
-            id = self.user.id
-            del user_lookup[self.user.id]
-            del self.user
-            return 'DELETE %d' % id
-
-    class ABHandler:
-
-        class CustomDispatch:
-
-            @cherrypy.expose
-            def index(self, a, b):
-                return 'custom'
-
-        def _cp_dispatch(self, vpath):
-            """Make sure that if we don't pop anything from vpath,
-            processing still works.
-            """
-            return self.CustomDispatch()
-
-        @cherrypy.expose
-        def index(self, a, b=None):
-            body = ['a:' + str(a)]
-            if b is not None:
-                body.append(',b:' + str(b))
-            return ''.join(body)
-
-        @cherrypy.expose
-        def delete(self, a, b):
-            return 'deleting ' + str(a) + ' and ' + str(b)
-
-    class IndexOnly:
-
-        def _cp_dispatch(self, vpath):
-            """Make sure that popping ALL of vpath still shows the index
-            handler.
-            """
-            while vpath:
-                vpath.pop()
-            return self
-
-        @cherrypy.expose
-        def index(self):
-            return 'IndexOnly index'
-
-    class DecoratedPopArgs:
-
-        """Test _cp_dispatch with @cherrypy.popargs."""
-
-        @cherrypy.expose
-        def index(self):
-            return 'no params'
-
-        @cherrypy.expose
-        def hi(self):
-            return "hi was not interpreted as 'a' param"
-    DecoratedPopArgs = cherrypy.popargs(
-        'a', 'b', handler=ABHandler())(DecoratedPopArgs)
-
-    class NonDecoratedPopArgs:
-
-        """Test _cp_dispatch = cherrypy.popargs()"""
-
-        _cp_dispatch = cherrypy.popargs('a')
-
-        @cherrypy.expose
-        def index(self, a):
-            return 'index: ' + str(a)
-
-    class ParameterizedHandler:
-
-        """Special handler created for each request"""
-
-        def __init__(self, a):
-            self.a = a
-
-        @cherrypy.expose
-        def index(self):
-            if 'a' in cherrypy.request.params:
-                raise Exception(
-                    'Parameterized handler argument ended up in '
-                    'request.params')
-            return self.a
-
-    class ParameterizedPopArgs:
-
-        """Test cherrypy.popargs() with a function call handler"""
-    ParameterizedPopArgs = cherrypy.popargs(
-        'a', handler=ParameterizedHandler)(ParameterizedPopArgs)
-
-    Root.decorated = DecoratedPopArgs()
-    Root.undecorated = NonDecoratedPopArgs()
-    Root.index_only = IndexOnly()
-    Root.parameter_test = ParameterizedPopArgs()
-
-    Root.users = UserContainerNode()
-
-    md = cherrypy.dispatch.MethodDispatcher('dynamic_dispatch')
-    for url in script_names:
-        conf = {
-            '/': {
-                'user': (url or '/').split('/')[-2],
-            },
-            '/users': {
-                'request.dispatch': md
-            },
-        }
-        cherrypy.tree.mount(Root(), url, conf)
-
-
-class DynamicObjectMappingTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def testObjectMapping(self):
-        for url in script_names:
-            self.script_name = url
-
-            self.getPage('/')
-            self.assertBody('index')
-
-            self.getPage('/handler')
-            self.assertBody('handler')
-
-            # Dynamic dispatch will succeed here for the subnodes
-            # so the subroot gets called
-            self.getPage('/1/')
-            self.assertBody('SubRoot index')
-
-            self.getPage('/2/')
-            self.assertBody('SubRoot index')
-
-            self.getPage('/1/handler')
-            self.assertBody('SubRoot handler')
-
-            self.getPage('/2/handler')
-            self.assertBody('SubRoot handler')
-
-            # Dynamic dispatch will fail here for the subnodes
-            # so the default gets called
-            self.getPage('/asdf/')
-            self.assertBody("default ('asdf',)")
-
-            self.getPage('/asdf/asdf')
-            self.assertBody("default ('asdf', 'asdf')")
-
-            self.getPage('/asdf/handler')
-            self.assertBody("default ('asdf', 'handler')")
-
-            # Dynamic dispatch will succeed here for the subsubnodes
-            # so the subsubroot gets called
-            self.getPage('/1/1/')
-            self.assertBody('SubSubRoot index')
-
-            self.getPage('/2/2/')
-            self.assertBody('SubSubRoot index')
-
-            self.getPage('/1/1/handler')
-            self.assertBody('SubSubRoot handler')
-
-            self.getPage('/2/2/handler')
-            self.assertBody('SubSubRoot handler')
-
-            self.getPage('/2/2/dispatch')
-            self.assertBody('SubSubRoot dispatch')
-
-            # The exposed dispatch will not be called as a dispatch
-            # method.
-            self.getPage('/2/2/foo/foo')
-            self.assertBody('SubSubRoot default')
-
-            # Dynamic dispatch will fail here for the subsubnodes
-            # so the SubRoot gets called
-            self.getPage('/1/asdf/')
-            self.assertBody("SubRoot ('asdf',)")
-
-            self.getPage('/1/asdf/asdf')
-            self.assertBody("SubRoot ('asdf', 'asdf')")
-
-            self.getPage('/1/asdf/handler')
-            self.assertBody("SubRoot ('asdf', 'handler')")
-
-    def testMethodDispatch(self):
-        # GET acts like a container
-        self.getPage('/users')
-        self.assertBody('[1, 2]')
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        # POST to the container URI allows creation
-        self.getPage('/users', method='POST', body='name=baz')
-        self.assertBody('POST 3')
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        # POST to a specific instanct URI results in a 404
-        # as the resource does not exit.
-        self.getPage('/users/5', method='POST', body='name=baz')
-        self.assertStatus(404)
-
-        # PUT to a specific instanct URI results in creation
-        self.getPage('/users/5', method='PUT', body='name=boris')
-        self.assertBody('PUT 5')
-        self.assertHeader('Allow', 'DELETE, GET, HEAD, POST, PUT')
-
-        # GET acts like a container
-        self.getPage('/users')
-        self.assertBody('[1, 2, 3, 5]')
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        test_cases = (
-            (1, 'foo', 'fooupdated', 'DELETE, GET, HEAD, POST, PUT'),
-            (2, 'bar', 'barupdated', 'DELETE, GET, HEAD, POST, PUT'),
-            (3, 'baz', 'bazupdated', 'DELETE, GET, HEAD, POST, PUT'),
-            (5, 'boris', 'borisupdated', 'DELETE, GET, HEAD, POST, PUT'),
-        )
-        for id, name, updatedname, headers in test_cases:
-            self.getPage('/users/%d' % id)
-            self.assertBody(name)
-            self.assertHeader('Allow', headers)
-
-            # Make sure POSTs update already existings resources
-            self.getPage('/users/%d' %
-                         id, method='POST', body='name=%s' % updatedname)
-            self.assertBody('POST %d' % id)
-            self.assertHeader('Allow', headers)
-
-            # Make sure PUTs Update already existing resources.
-            self.getPage('/users/%d' %
-                         id, method='PUT', body='name=%s' % updatedname)
-            self.assertBody('PUT %d' % id)
-            self.assertHeader('Allow', headers)
-
-            # Make sure DELETES Remove already existing resources.
-            self.getPage('/users/%d' % id, method='DELETE')
-            self.assertBody('DELETE %d' % id)
-            self.assertHeader('Allow', headers)
-
-        # GET acts like a container
-        self.getPage('/users')
-        self.assertBody('[]')
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-    def testVpathDispatch(self):
-        self.getPage('/decorated/')
-        self.assertBody('no params')
-
-        self.getPage('/decorated/hi')
-        self.assertBody("hi was not interpreted as 'a' param")
-
-        self.getPage('/decorated/yo/')
-        self.assertBody('a:yo')
-
-        self.getPage('/decorated/yo/there/')
-        self.assertBody('a:yo,b:there')
-
-        self.getPage('/decorated/yo/there/delete')
-        self.assertBody('deleting yo and there')
-
-        self.getPage('/decorated/yo/there/handled_by_dispatch/')
-        self.assertBody('custom')
-
-        self.getPage('/undecorated/blah/')
-        self.assertBody('index: blah')
-
-        self.getPage('/index_only/a/b/c/d/e/f/g/')
-        self.assertBody('IndexOnly index')
-
-        self.getPage('/parameter_test/argument2/')
-        self.assertBody('argument2')
diff --git a/libraries/cherrypy/test/test_encoding.py b/libraries/cherrypy/test/test_encoding.py
deleted file mode 100644
index ab24ab93..00000000
--- a/libraries/cherrypy/test/test_encoding.py
+++ /dev/null
@@ -1,426 +0,0 @@
-# coding: utf-8
-
-import gzip
-import io
-from unittest import mock
-
-from six.moves.http_client import IncompleteRead
-from six.moves.urllib.parse import quote as url_quote
-
-import cherrypy
-from cherrypy._cpcompat import ntob, ntou
-
-from cherrypy.test import helper
-
-
-europoundUnicode = ntou('£', encoding='utf-8')
-sing = ntou('毛泽东: Sing, Little Birdie?', encoding='utf-8')
-
-sing8 = sing.encode('utf-8')
-sing16 = sing.encode('utf-16')
-
-
-class EncodingTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self, param):
-                assert param == europoundUnicode, '%r != %r' % (
-                    param, europoundUnicode)
-                yield europoundUnicode
-
-            @cherrypy.expose
-            def mao_zedong(self):
-                return sing
-
-            @cherrypy.expose
-            @cherrypy.config(**{'tools.encode.encoding': 'utf-8'})
-            def utf8(self):
-                return sing8
-
-            @cherrypy.expose
-            def cookies_and_headers(self):
-                # if the headers have non-ascii characters and a cookie has
-                #  any part which is unicode (even ascii), the response
-                #  should not fail.
-                cherrypy.response.cookie['candy'] = 'bar'
-                cherrypy.response.cookie['candy']['domain'] = 'cherrypy.org'
-                cherrypy.response.headers[
-                    'Some-Header'] = 'My d\xc3\xb6g has fleas'
-                return 'Any content'
-
-            @cherrypy.expose
-            def reqparams(self, *args, **kwargs):
-                return b', '.join(
-                    [': '.join((k, v)).encode('utf8')
-                     for k, v in sorted(cherrypy.request.params.items())]
-                )
-
-            @cherrypy.expose
-            @cherrypy.config(**{
-                'tools.encode.text_only': False,
-                'tools.encode.add_charset': True,
-            })
-            def nontext(self, *args, **kwargs):
-                cherrypy.response.headers[
-                    'Content-Type'] = 'application/binary'
-                return '\x00\x01\x02\x03'
-
-        class GZIP:
-
-            @cherrypy.expose
-            def index(self):
-                yield 'Hello, world'
-
-            @cherrypy.expose
-            # Turn encoding off so the gzip tool is the one doing the collapse.
-            @cherrypy.config(**{'tools.encode.on': False})
-            def noshow(self):
-                # Test for ticket #147, where yield showed no exceptions
-                # (content-encoding was still gzip even though traceback
-                # wasn't zipped).
-                raise IndexError()
-                yield 'Here be dragons'
-
-            @cherrypy.expose
-            @cherrypy.config(**{'response.stream': True})
-            def noshow_stream(self):
-                # Test for ticket #147, where yield showed no exceptions
-                # (content-encoding was still gzip even though traceback
-                # wasn't zipped).
-                raise IndexError()
-                yield 'Here be dragons'
-
-        class Decode:
-
-            @cherrypy.expose
-            @cherrypy.config(**{
-                'tools.decode.on': True,
-                'tools.decode.default_encoding': ['utf-16'],
-            })
-            def extra_charset(self, *args, **kwargs):
-                return ', '.join([': '.join((k, v))
-                                  for k, v in cherrypy.request.params.items()])
-
-            @cherrypy.expose
-            @cherrypy.config(**{
-                'tools.decode.on': True,
-                'tools.decode.encoding': 'utf-16',
-            })
-            def force_charset(self, *args, **kwargs):
-                return ', '.join([': '.join((k, v))
-                                  for k, v in cherrypy.request.params.items()])
-
-        root = Root()
-        root.gzip = GZIP()
-        root.decode = Decode()
-        cherrypy.tree.mount(root, config={'/gzip': {'tools.gzip.on': True}})
-
-    def test_query_string_decoding(self):
-        URI_TMPL = '/reqparams?q={q}'
-
-        europoundUtf8_2_bytes = europoundUnicode.encode('utf-8')
-        europoundUtf8_2nd_byte = europoundUtf8_2_bytes[1:2]
-
-        # Encoded utf8 query strings MUST be parsed correctly.
-        # Here, q is the POUND SIGN U+00A3 encoded in utf8 and then %HEX
-        self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2_bytes)))
-        # The return value will be encoded as utf8.
-        self.assertBody(b'q: ' + europoundUtf8_2_bytes)
-
-        # Query strings that are incorrectly encoded MUST raise 404.
-        # Here, q is the second byte of POUND SIGN U+A3 encoded in utf8
-        # and then %HEX
-        # TODO: check whether this shouldn't raise 400 Bad Request instead
-        self.getPage(URI_TMPL.format(q=url_quote(europoundUtf8_2nd_byte)))
-        self.assertStatus(404)
-        self.assertErrorPage(
-            404,
-            'The given query string could not be processed. Query '
-            "strings for this resource must be encoded with 'utf8'.")
-
-    def test_urlencoded_decoding(self):
-        # Test the decoding of an application/x-www-form-urlencoded entity.
-        europoundUtf8 = europoundUnicode.encode('utf-8')
-        body = b'param=' + europoundUtf8
-        self.getPage('/',
-                     method='POST',
-                     headers=[
-                         ('Content-Type', 'application/x-www-form-urlencoded'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(europoundUtf8)
-
-        # Encoded utf8 entities MUST be parsed and decoded correctly.
-        # Here, q is the POUND SIGN U+00A3 encoded in utf8
-        body = b'q=\xc2\xa3'
-        self.getPage('/reqparams', method='POST',
-                     headers=[(
-                         'Content-Type', 'application/x-www-form-urlencoded'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(b'q: \xc2\xa3')
-
-        # ...and in utf16, which is not in the default attempt_charsets list:
-        body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
-        self.getPage('/reqparams',
-                     method='POST',
-                     headers=[
-                         ('Content-Type',
-                          'application/x-www-form-urlencoded;charset=utf-16'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(b'q: \xc2\xa3')
-
-        # Entities that are incorrectly encoded MUST raise 400.
-        # Here, q is the POUND SIGN U+00A3 encoded in utf16, but
-        # the Content-Type incorrectly labels it utf-8.
-        body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
-        self.getPage('/reqparams',
-                     method='POST',
-                     headers=[
-                         ('Content-Type',
-                          'application/x-www-form-urlencoded;charset=utf-8'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertStatus(400)
-        self.assertErrorPage(
-            400,
-            'The request entity could not be decoded. The following charsets '
-            "were attempted: ['utf-8']")
-
-    def test_decode_tool(self):
-        # An extra charset should be tried first, and succeed if it matches.
-        # Here, we add utf-16 as a charset and pass a utf-16 body.
-        body = b'\xff\xfeq\x00=\xff\xfe\xa3\x00'
-        self.getPage('/decode/extra_charset', method='POST',
-                     headers=[(
-                         'Content-Type', 'application/x-www-form-urlencoded'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(b'q: \xc2\xa3')
-
-        # An extra charset should be tried first, and continue to other default
-        # charsets if it doesn't match.
-        # Here, we add utf-16 as a charset but still pass a utf-8 body.
-        body = b'q=\xc2\xa3'
-        self.getPage('/decode/extra_charset', method='POST',
-                     headers=[(
-                         'Content-Type', 'application/x-www-form-urlencoded'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(b'q: \xc2\xa3')
-
-        # An extra charset should error if force is True and it doesn't match.
-        # Here, we force utf-16 as a charset but still pass a utf-8 body.
-        body = b'q=\xc2\xa3'
-        self.getPage('/decode/force_charset', method='POST',
-                     headers=[(
-                         'Content-Type', 'application/x-www-form-urlencoded'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertErrorPage(
-            400,
-            'The request entity could not be decoded. The following charsets '
-            "were attempted: ['utf-16']")
-
-    def test_multipart_decoding(self):
-        # Test the decoding of a multipart entity when the charset (utf16) is
-        # explicitly given.
-        body = ntob('\r\n'.join([
-            '--X',
-            'Content-Type: text/plain;charset=utf-16',
-            'Content-Disposition: form-data; name="text"',
-            '',
-            '\xff\xfea\x00b\x00\x1c c\x00',
-            '--X',
-            'Content-Type: text/plain;charset=utf-16',
-            'Content-Disposition: form-data; name="submit"',
-            '',
-            '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
-            '--X--'
-        ]))
-        self.getPage('/reqparams', method='POST',
-                     headers=[(
-                         'Content-Type', 'multipart/form-data;boundary=X'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(b'submit: Create, text: ab\xe2\x80\x9cc')
-
-    @mock.patch('cherrypy._cpreqbody.Part.maxrambytes', 1)
-    def test_multipart_decoding_bigger_maxrambytes(self):
-        """
-        Decoding of a multipart entity should also pass when
-        the entity is bigger than maxrambytes. See ticket #1352.
-        """
-        self.test_multipart_decoding()
-
-    def test_multipart_decoding_no_charset(self):
-        # Test the decoding of a multipart entity when the charset (utf8) is
-        # NOT explicitly given, but is in the list of charsets to attempt.
-        body = ntob('\r\n'.join([
-            '--X',
-            'Content-Disposition: form-data; name="text"',
-            '',
-            '\xe2\x80\x9c',
-            '--X',
-            'Content-Disposition: form-data; name="submit"',
-            '',
-            'Create',
-            '--X--'
-        ]))
-        self.getPage('/reqparams', method='POST',
-                     headers=[(
-                         'Content-Type', 'multipart/form-data;boundary=X'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(b'submit: Create, text: \xe2\x80\x9c')
-
-    def test_multipart_decoding_no_successful_charset(self):
-        # Test the decoding of a multipart entity when the charset (utf16) is
-        # NOT explicitly given, and is NOT in the list of charsets to attempt.
-        body = ntob('\r\n'.join([
-            '--X',
-            'Content-Disposition: form-data; name="text"',
-            '',
-            '\xff\xfea\x00b\x00\x1c c\x00',
-            '--X',
-            'Content-Disposition: form-data; name="submit"',
-            '',
-            '\xff\xfeC\x00r\x00e\x00a\x00t\x00e\x00',
-            '--X--'
-        ]))
-        self.getPage('/reqparams', method='POST',
-                     headers=[(
-                         'Content-Type', 'multipart/form-data;boundary=X'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertStatus(400)
-        self.assertErrorPage(
-            400,
-            'The request entity could not be decoded. The following charsets '
-            "were attempted: ['us-ascii', 'utf-8']")
-
-    def test_nontext(self):
-        self.getPage('/nontext')
-        self.assertHeader('Content-Type', 'application/binary;charset=utf-8')
-        self.assertBody('\x00\x01\x02\x03')
-
-    def testEncoding(self):
-        # Default encoding should be utf-8
-        self.getPage('/mao_zedong')
-        self.assertBody(sing8)
-
-        # Ask for utf-16.
-        self.getPage('/mao_zedong', [('Accept-Charset', 'utf-16')])
-        self.assertHeader('Content-Type', 'text/html;charset=utf-16')
-        self.assertBody(sing16)
-
-        # Ask for multiple encodings. ISO-8859-1 should fail, and utf-16
-        # should be produced.
-        self.getPage('/mao_zedong', [('Accept-Charset',
-                                      'iso-8859-1;q=1, utf-16;q=0.5')])
-        self.assertBody(sing16)
-
-        # The "*" value should default to our default_encoding, utf-8
-        self.getPage('/mao_zedong', [('Accept-Charset', '*;q=1, utf-7;q=.2')])
-        self.assertBody(sing8)
-
-        # Only allow iso-8859-1, which should fail and raise 406.
-        self.getPage('/mao_zedong', [('Accept-Charset', 'iso-8859-1, *;q=0')])
-        self.assertStatus('406 Not Acceptable')
-        self.assertInBody('Your client sent this Accept-Charset header: '
-                          'iso-8859-1, *;q=0. We tried these charsets: '
-                          'iso-8859-1.')
-
-        # Ask for x-mac-ce, which should be unknown. See ticket #569.
-        self.getPage('/mao_zedong', [('Accept-Charset',
-                                      'us-ascii, ISO-8859-1, x-mac-ce')])
-        self.assertStatus('406 Not Acceptable')
-        self.assertInBody('Your client sent this Accept-Charset header: '
-                          'us-ascii, ISO-8859-1, x-mac-ce. We tried these '
-                          'charsets: ISO-8859-1, us-ascii, x-mac-ce.')
-
-        # Test the 'encoding' arg to encode.
-        self.getPage('/utf8')
-        self.assertBody(sing8)
-        self.getPage('/utf8', [('Accept-Charset', 'us-ascii, ISO-8859-1')])
-        self.assertStatus('406 Not Acceptable')
-
-        # Test malformed quality value, which should raise 400.
-        self.getPage('/mao_zedong', [('Accept-Charset',
-                                      'ISO-8859-1,utf-8;q=0.7,*;q=0.7)')])
-        self.assertStatus('400 Bad Request')
-
-    def testGzip(self):
-        zbuf = io.BytesIO()
-        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
-        zfile.write(b'Hello, world')
-        zfile.close()
-
-        self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip')])
-        self.assertInBody(zbuf.getvalue()[:3])
-        self.assertHeader('Vary', 'Accept-Encoding')
-        self.assertHeader('Content-Encoding', 'gzip')
-
-        # Test when gzip is denied.
-        self.getPage('/gzip/', headers=[('Accept-Encoding', 'identity')])
-        self.assertHeader('Vary', 'Accept-Encoding')
-        self.assertNoHeader('Content-Encoding')
-        self.assertBody('Hello, world')
-
-        self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip;q=0')])
-        self.assertHeader('Vary', 'Accept-Encoding')
-        self.assertNoHeader('Content-Encoding')
-        self.assertBody('Hello, world')
-
-        # Test that trailing comma doesn't cause IndexError
-        # Ref: https://github.com/cherrypy/cherrypy/issues/988
-        self.getPage('/gzip/', headers=[('Accept-Encoding', 'gzip,deflate,')])
-        self.assertStatus(200)
-        self.assertNotInBody('IndexError')
-
-        self.getPage('/gzip/', headers=[('Accept-Encoding', '*;q=0')])
-        self.assertStatus(406)
-        self.assertNoHeader('Content-Encoding')
-        self.assertErrorPage(406, 'identity, gzip')
-
-        # Test for ticket #147
-        self.getPage('/gzip/noshow', headers=[('Accept-Encoding', 'gzip')])
-        self.assertNoHeader('Content-Encoding')
-        self.assertStatus(500)
-        self.assertErrorPage(500, pattern='IndexError\n')
-
-        # In this case, there's nothing we can do to deliver a
-        # readable page, since 1) the gzip header is already set,
-        # and 2) we may have already written some of the body.
-        # The fix is to never stream yields when using gzip.
-        if (cherrypy.server.protocol_version == 'HTTP/1.0' or
-                getattr(cherrypy.server, 'using_apache', False)):
-            self.getPage('/gzip/noshow_stream',
-                         headers=[('Accept-Encoding', 'gzip')])
-            self.assertHeader('Content-Encoding', 'gzip')
-            self.assertInBody('\x1f\x8b\x08\x00')
-        else:
-            # The wsgiserver will simply stop sending data, and the HTTP client
-            # will error due to an incomplete chunk-encoded stream.
-            self.assertRaises((ValueError, IncompleteRead), self.getPage,
-                              '/gzip/noshow_stream',
-                              headers=[('Accept-Encoding', 'gzip')])
-
-    def test_UnicodeHeaders(self):
-        self.getPage('/cookies_and_headers')
-        self.assertBody('Any content')
diff --git a/libraries/cherrypy/test/test_etags.py b/libraries/cherrypy/test/test_etags.py
deleted file mode 100644
index 293eb866..00000000
--- a/libraries/cherrypy/test/test_etags.py
+++ /dev/null
@@ -1,84 +0,0 @@
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy.test import helper
-
-
-class ETagTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def resource(self):
-                return 'Oh wah ta goo Siam.'
-
-            @cherrypy.expose
-            def fail(self, code):
-                code = int(code)
-                if 300 <= code <= 399:
-                    raise cherrypy.HTTPRedirect([], code)
-                else:
-                    raise cherrypy.HTTPError(code)
-
-            @cherrypy.expose
-            # In Python 3, tools.encode is on by default
-            @cherrypy.config(**{'tools.encode.on': True})
-            def unicoded(self):
-                return ntou('I am a \u1ee4nicode string.', 'escape')
-
-        conf = {'/': {'tools.etags.on': True,
-                      'tools.etags.autotags': True,
-                      }}
-        cherrypy.tree.mount(Root(), config=conf)
-
-    def test_etags(self):
-        self.getPage('/resource')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
-        self.assertBody('Oh wah ta goo Siam.')
-        etag = self.assertHeader('ETag')
-
-        # Test If-Match (both valid and invalid)
-        self.getPage('/resource', headers=[('If-Match', etag)])
-        self.assertStatus('200 OK')
-        self.getPage('/resource', headers=[('If-Match', '*')])
-        self.assertStatus('200 OK')
-        self.getPage('/resource', headers=[('If-Match', '*')], method='POST')
-        self.assertStatus('200 OK')
-        self.getPage('/resource', headers=[('If-Match', 'a bogus tag')])
-        self.assertStatus('412 Precondition Failed')
-
-        # Test If-None-Match (both valid and invalid)
-        self.getPage('/resource', headers=[('If-None-Match', etag)])
-        self.assertStatus(304)
-        self.getPage('/resource', method='POST',
-                     headers=[('If-None-Match', etag)])
-        self.assertStatus('412 Precondition Failed')
-        self.getPage('/resource', headers=[('If-None-Match', '*')])
-        self.assertStatus(304)
-        self.getPage('/resource', headers=[('If-None-Match', 'a bogus tag')])
-        self.assertStatus('200 OK')
-
-    def test_errors(self):
-        self.getPage('/resource')
-        self.assertStatus(200)
-        etag = self.assertHeader('ETag')
-
-        # Test raising errors in page handler
-        self.getPage('/fail/412', headers=[('If-Match', etag)])
-        self.assertStatus(412)
-        self.getPage('/fail/304', headers=[('If-Match', etag)])
-        self.assertStatus(304)
-        self.getPage('/fail/412', headers=[('If-None-Match', '*')])
-        self.assertStatus(412)
-        self.getPage('/fail/304', headers=[('If-None-Match', '*')])
-        self.assertStatus(304)
-
-    def test_unicode_body(self):
-        self.getPage('/unicoded')
-        self.assertStatus(200)
-        etag1 = self.assertHeader('ETag')
-        self.getPage('/unicoded', headers=[('If-Match', etag1)])
-        self.assertStatus(200)
-        self.assertHeader('ETag', etag1)
diff --git a/libraries/cherrypy/test/test_http.py b/libraries/cherrypy/test/test_http.py
deleted file mode 100644
index 0899d4d0..00000000
--- a/libraries/cherrypy/test/test_http.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# coding: utf-8
-"""Tests for managing HTTP issues (malformed requests, etc)."""
-
-import errno
-import mimetypes
-import socket
-import sys
-from unittest import mock
-
-import six
-from six.moves.http_client import HTTPConnection
-from six.moves import urllib
-
-import cherrypy
-from cherrypy._cpcompat import HTTPSConnection, quote
-
-from cherrypy.test import helper
-
-
-def is_ascii(text):
-    """
-    Return True if the text encodes as ascii.
-    """
-    try:
-        text.encode('ascii')
-        return True
-    except Exception:
-        pass
-    return False
-
-
-def encode_filename(filename):
-    """
-    Given a filename to be used in a multipart/form-data,
-    encode the name. Return the key and encoded filename.
-    """
-    if is_ascii(filename):
-        return 'filename', '"{filename}"'.format(**locals())
-    encoded = quote(filename, encoding='utf-8')
-    return 'filename*', "'".join((
-        'UTF-8',
-        '',  # lang
-        encoded,
-    ))
-
-
-def encode_multipart_formdata(files):
-    """Return (content_type, body) ready for httplib.HTTP instance.
-
-    files: a sequence of (name, filename, value) tuples for multipart uploads.
-    filename can be a string or a tuple ('filename string', 'encoding')
-    """
-    BOUNDARY = '________ThIs_Is_tHe_bouNdaRY_$'
-    L = []
-    for key, filename, value in files:
-        L.append('--' + BOUNDARY)
-
-        fn_key, encoded = encode_filename(filename)
-        tmpl = \
-            'Content-Disposition: form-data; name="{key}"; {fn_key}={encoded}'
-        L.append(tmpl.format(**locals()))
-        ct = mimetypes.guess_type(filename)[0] or 'application/octet-stream'
-        L.append('Content-Type: %s' % ct)
-        L.append('')
-        L.append(value)
-    L.append('--' + BOUNDARY + '--')
-    L.append('')
-    body = '\r\n'.join(L)
-    content_type = 'multipart/form-data; boundary=%s' % BOUNDARY
-    return content_type, body
-
-
-class HTTPTests(helper.CPWebCase):
-
-    def make_connection(self):
-        if self.scheme == 'https':
-            return HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
-        else:
-            return HTTPConnection('%s:%s' % (self.interface(), self.PORT))
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self, *args, **kwargs):
-                return 'Hello world!'
-
-            @cherrypy.expose
-            @cherrypy.config(**{'request.process_request_body': False})
-            def no_body(self, *args, **kwargs):
-                return 'Hello world!'
-
-            @cherrypy.expose
-            def post_multipart(self, file):
-                """Return a summary ("a * 65536\nb * 65536") of the uploaded
-                file.
-                """
-                contents = file.file.read()
-                summary = []
-                curchar = None
-                count = 0
-                for c in contents:
-                    if c == curchar:
-                        count += 1
-                    else:
-                        if count:
-                            if six.PY3:
-                                curchar = chr(curchar)
-                            summary.append('%s * %d' % (curchar, count))
-                        count = 1
-                        curchar = c
-                if count:
-                    if six.PY3:
-                        curchar = chr(curchar)
-                    summary.append('%s * %d' % (curchar, count))
-                return ', '.join(summary)
-
-            @cherrypy.expose
-            def post_filename(self, myfile):
-                '''Return the name of the file which was uploaded.'''
-                return myfile.filename
-
-        cherrypy.tree.mount(Root())
-        cherrypy.config.update({'server.max_request_body_size': 30000000})
-
-    def test_no_content_length(self):
-        # "The presence of a message-body in a request is signaled by the
-        # inclusion of a Content-Length or Transfer-Encoding header field in
-        # the request's message-headers."
-        #
-        # Send a message with neither header and no body. Even though
-        # the request is of method POST, this should be OK because we set
-        # request.process_request_body to False for our handler.
-        c = self.make_connection()
-        c.request('POST', '/no_body')
-        response = c.getresponse()
-        self.body = response.fp.read()
-        self.status = str(response.status)
-        self.assertStatus(200)
-        self.assertBody(b'Hello world!')
-
-        # Now send a message that has no Content-Length, but does send a body.
-        # Verify that CP times out the socket and responds
-        # with 411 Length Required.
-        if self.scheme == 'https':
-            c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
-        else:
-            c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
-
-        # `_get_content_length` is needed for Python 3.6+
-        with mock.patch.object(
-                c,
-                '_get_content_length',
-                lambda body, method: None,
-                create=True):
-            # `_set_content_length` is needed for Python 2.7-3.5
-            with mock.patch.object(c, '_set_content_length', create=True):
-                c.request('POST', '/')
-
-        response = c.getresponse()
-        self.body = response.fp.read()
-        self.status = str(response.status)
-        self.assertStatus(411)
-
-    def test_post_multipart(self):
-        alphabet = 'abcdefghijklmnopqrstuvwxyz'
-        # generate file contents for a large post
-        contents = ''.join([c * 65536 for c in alphabet])
-
-        # encode as multipart form data
-        files = [('file', 'file.txt', contents)]
-        content_type, body = encode_multipart_formdata(files)
-        body = body.encode('Latin-1')
-
-        # post file
-        c = self.make_connection()
-        c.putrequest('POST', '/post_multipart')
-        c.putheader('Content-Type', content_type)
-        c.putheader('Content-Length', str(len(body)))
-        c.endheaders()
-        c.send(body)
-
-        response = c.getresponse()
-        self.body = response.fp.read()
-        self.status = str(response.status)
-        self.assertStatus(200)
-        parts = ['%s * 65536' % ch for ch in alphabet]
-        self.assertBody(', '.join(parts))
-
-    def test_post_filename_with_special_characters(self):
-        '''Testing that we can handle filenames with special characters. This
-        was reported as a bug in:
-           https://github.com/cherrypy/cherrypy/issues/1146/
-           https://github.com/cherrypy/cherrypy/issues/1397/
-           https://github.com/cherrypy/cherrypy/issues/1694/
-        '''
-        # We'll upload a bunch of files with differing names.
-        fnames = [
-            'boop.csv', 'foo, bar.csv', 'bar, xxxx.csv', 'file"name.csv',
-            'file;name.csv', 'file; name.csv', u'test_łóąä.txt',
-        ]
-        for fname in fnames:
-            files = [('myfile', fname, 'yunyeenyunyue')]
-            content_type, body = encode_multipart_formdata(files)
-            body = body.encode('Latin-1')
-
-            # post file
-            c = self.make_connection()
-            c.putrequest('POST', '/post_filename')
-            c.putheader('Content-Type', content_type)
-            c.putheader('Content-Length', str(len(body)))
-            c.endheaders()
-            c.send(body)
-
-            response = c.getresponse()
-            self.body = response.fp.read()
-            self.status = str(response.status)
-            self.assertStatus(200)
-            self.assertBody(fname)
-
-    def test_malformed_request_line(self):
-        if getattr(cherrypy.server, 'using_apache', False):
-            return self.skip('skipped due to known Apache differences...')
-
-        # Test missing version in Request-Line
-        c = self.make_connection()
-        c._output(b'geT /')
-        c._send_output()
-        if hasattr(c, 'strict'):
-            response = c.response_class(c.sock, strict=c.strict, method='GET')
-        else:
-            # Python 3.2 removed the 'strict' feature, saying:
-            # "http.client now always assumes HTTP/1.x compliant servers."
-            response = c.response_class(c.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 400)
-        self.assertEqual(response.fp.read(22), b'Malformed Request-Line')
-        c.close()
-
-    def test_request_line_split_issue_1220(self):
-        params = {
-            'intervenant-entreprise-evenement_classaction':
-                'evenement-mailremerciements',
-            '_path': 'intervenant-entreprise-evenement',
-            'intervenant-entreprise-evenement_action-id': 19404,
-            'intervenant-entreprise-evenement_id': 19404,
-            'intervenant-entreprise_id': 28092,
-        }
-        Request_URI = '/index?' + urllib.parse.urlencode(params)
-        self.assertEqual(len('GET %s HTTP/1.1\r\n' % Request_URI), 256)
-        self.getPage(Request_URI)
-        self.assertBody('Hello world!')
-
-    def test_malformed_header(self):
-        c = self.make_connection()
-        c.putrequest('GET', '/')
-        c.putheader('Content-Type', 'text/plain')
-        # See https://github.com/cherrypy/cherrypy/issues/941
-        c._output(b're, 1.2.3.4#015#012')
-        c.endheaders()
-
-        response = c.getresponse()
-        self.status = str(response.status)
-        self.assertStatus(400)
-        self.body = response.fp.read(20)
-        self.assertBody('Illegal header line.')
-
-    def test_http_over_https(self):
-        if self.scheme != 'https':
-            return self.skip('skipped (not running HTTPS)... ')
-
-        # Try connecting without SSL.
-        conn = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
-        conn.putrequest('GET', '/', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method='GET')
-        try:
-            response.begin()
-            self.assertEqual(response.status, 400)
-            self.body = response.read()
-            self.assertBody('The client sent a plain HTTP request, but this '
-                            'server only speaks HTTPS on this port.')
-        except socket.error:
-            e = sys.exc_info()[1]
-            # "Connection reset by peer" is also acceptable.
-            if e.errno != errno.ECONNRESET:
-                raise
-
-    def test_garbage_in(self):
-        # Connect without SSL regardless of server.scheme
-        c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
-        c._output(b'gjkgjklsgjklsgjkljklsg')
-        c._send_output()
-        response = c.response_class(c.sock, method='GET')
-        try:
-            response.begin()
-            self.assertEqual(response.status, 400)
-            self.assertEqual(response.fp.read(22),
-                             b'Malformed Request-Line')
-            c.close()
-        except socket.error:
-            e = sys.exc_info()[1]
-            # "Connection reset by peer" is also acceptable.
-            if e.errno != errno.ECONNRESET:
-                raise
diff --git a/libraries/cherrypy/test/test_httputil.py b/libraries/cherrypy/test/test_httputil.py
deleted file mode 100644
index 656b8a3d..00000000
--- a/libraries/cherrypy/test/test_httputil.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""Test helpers from ``cherrypy.lib.httputil`` module."""
-import pytest
-from six.moves import http_client
-
-from cherrypy.lib import httputil
-
-
-@pytest.mark.parametrize(
-    'script_name,path_info,expected_url',
-    [
-        ('/sn/', '/pi/', '/sn/pi/'),
-        ('/sn/', '/pi', '/sn/pi'),
-        ('/sn/', '/', '/sn/'),
-        ('/sn/', '', '/sn/'),
-        ('/sn', '/pi/', '/sn/pi/'),
-        ('/sn', '/pi', '/sn/pi'),
-        ('/sn', '/', '/sn/'),
-        ('/sn', '', '/sn'),
-        ('/', '/pi/', '/pi/'),
-        ('/', '/pi', '/pi'),
-        ('/', '/', '/'),
-        ('/', '', '/'),
-        ('', '/pi/', '/pi/'),
-        ('', '/pi', '/pi'),
-        ('', '/', '/'),
-        ('', '', '/'),
-    ]
-)
-def test_urljoin(script_name, path_info, expected_url):
-    """Test all slash+atom combinations for SCRIPT_NAME and PATH_INFO."""
-    actual_url = httputil.urljoin(script_name, path_info)
-    assert actual_url == expected_url
-
-
-EXPECTED_200 = (200, 'OK', 'Request fulfilled, document follows')
-EXPECTED_500 = (
-    500,
-    'Internal Server Error',
-    'The server encountered an unexpected condition which '
-    'prevented it from fulfilling the request.',
-)
-EXPECTED_404 = (404, 'Not Found', 'Nothing matches the given URI')
-EXPECTED_444 = (444, 'Non-existent reason', '')
-
-
-@pytest.mark.parametrize(
-    'status,expected_status',
-    [
-        (None, EXPECTED_200),
-        (200, EXPECTED_200),
-        ('500', EXPECTED_500),
-        (http_client.NOT_FOUND, EXPECTED_404),
-        ('444 Non-existent reason', EXPECTED_444),
-    ]
-)
-def test_valid_status(status, expected_status):
-    """Check valid int, string and http_client-constants
-    statuses processing."""
-    assert httputil.valid_status(status) == expected_status
-
-
-@pytest.mark.parametrize(
-    'status_code,error_msg',
-    [
-        ('hey', "Illegal response status from server ('hey' is non-numeric)."),
-        (
-            {'hey': 'hi'},
-            'Illegal response status from server '
-            "({'hey': 'hi'} is non-numeric).",
-        ),
-        (1, 'Illegal response status from server (1 is out of range).'),
-        (600, 'Illegal response status from server (600 is out of range).'),
-    ]
-)
-def test_invalid_status(status_code, error_msg):
-    """Check that invalid status cause certain errors."""
-    with pytest.raises(ValueError) as excinfo:
-        httputil.valid_status(status_code)
-
-    assert error_msg in str(excinfo)
diff --git a/libraries/cherrypy/test/test_iterator.py b/libraries/cherrypy/test/test_iterator.py
deleted file mode 100644
index 92f08e7c..00000000
--- a/libraries/cherrypy/test/test_iterator.py
+++ /dev/null
@@ -1,196 +0,0 @@
-import six
-
-import cherrypy
-from cherrypy.test import helper
-
-
-class IteratorBase(object):
-
-    created = 0
-    datachunk = 'butternut squash' * 256
-
-    @classmethod
-    def incr(cls):
-        cls.created += 1
-
-    @classmethod
-    def decr(cls):
-        cls.created -= 1
-
-
-class OurGenerator(IteratorBase):
-
-    def __iter__(self):
-        self.incr()
-        try:
-            for i in range(1024):
-                yield self.datachunk
-        finally:
-            self.decr()
-
-
-class OurIterator(IteratorBase):
-
-    started = False
-    closed_off = False
-    count = 0
-
-    def increment(self):
-        self.incr()
-
-    def decrement(self):
-        if not self.closed_off:
-            self.closed_off = True
-            self.decr()
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        if not self.started:
-            self.started = True
-            self.increment()
-        self.count += 1
-        if self.count > 1024:
-            raise StopIteration
-        return self.datachunk
-
-    next = __next__
-
-    def __del__(self):
-        self.decrement()
-
-
-class OurClosableIterator(OurIterator):
-
-    def close(self):
-        self.decrement()
-
-
-class OurNotClosableIterator(OurIterator):
-
-    # We can't close something which requires an additional argument.
-    def close(self, somearg):
-        self.decrement()
-
-
-class OurUnclosableIterator(OurIterator):
-    close = 'close'  # not callable!
-
-
-class IteratorTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        class Root(object):
-
-            @cherrypy.expose
-            def count(self, clsname):
-                cherrypy.response.headers['Content-Type'] = 'text/plain'
-                return six.text_type(globals()[clsname].created)
-
-            @cherrypy.expose
-            def getall(self, clsname):
-                cherrypy.response.headers['Content-Type'] = 'text/plain'
-                return globals()[clsname]()
-
-            @cherrypy.expose
-            @cherrypy.config(**{'response.stream': True})
-            def stream(self, clsname):
-                return self.getall(clsname)
-
-        cherrypy.tree.mount(Root())
-
-    def test_iterator(self):
-        try:
-            self._test_iterator()
-        except Exception:
-            'Test fails intermittently. See #1419'
-
-    def _test_iterator(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Check the counts of all the classes, they should be zero.
-        closables = ['OurClosableIterator', 'OurGenerator']
-        unclosables = ['OurUnclosableIterator', 'OurNotClosableIterator']
-        all_classes = closables + unclosables
-
-        import random
-        random.shuffle(all_classes)
-
-        for clsname in all_classes:
-            self.getPage('/count/' + clsname)
-            self.assertStatus(200)
-            self.assertBody('0')
-
-        # We should also be able to read the entire content body
-        # successfully, though we don't need to, we just want to
-        # check the header.
-        for clsname in all_classes:
-            itr_conn = self.get_conn()
-            itr_conn.putrequest('GET', '/getall/' + clsname)
-            itr_conn.endheaders()
-            response = itr_conn.getresponse()
-            self.assertEqual(response.status, 200)
-            headers = response.getheaders()
-            for header_name, header_value in headers:
-                if header_name.lower() == 'content-length':
-                    expected = six.text_type(1024 * 16 * 256)
-                    assert header_value == expected, header_value
-                    break
-            else:
-                raise AssertionError('No Content-Length header found')
-
-            # As the response should be fully consumed by CherryPy
-            # before sending back, the count should still be at zero
-            # by the time the response has been sent.
-            self.getPage('/count/' + clsname)
-            self.assertStatus(200)
-            self.assertBody('0')
-
-        # Now we do the same check with streaming - some classes will
-        # be automatically closed, while others cannot.
-        stream_counts = {}
-        for clsname in all_classes:
-            itr_conn = self.get_conn()
-            itr_conn.putrequest('GET', '/stream/' + clsname)
-            itr_conn.endheaders()
-            response = itr_conn.getresponse()
-            self.assertEqual(response.status, 200)
-            response.fp.read(65536)
-
-            # Let's check the count - this should always be one.
-            self.getPage('/count/' + clsname)
-            self.assertBody('1')
-
-            # Now if we close the connection, the count should go back
-            # to zero.
-            itr_conn.close()
-            self.getPage('/count/' + clsname)
-
-            # If this is a response which should be easily closed, then
-            # we will test to see if the value has gone back down to
-            # zero.
-            if clsname in closables:
-
-                # Sometimes we try to get the answer too quickly - we
-                # will wait for 100 ms before asking again if we didn't
-                # get the answer we wanted.
-                if self.body != '0':
-                    import time
-                    time.sleep(0.1)
-                    self.getPage('/count/' + clsname)
-
-            stream_counts[clsname] = int(self.body)
-
-        # Check that we closed off the classes which should provide
-        # easy mechanisms for doing so.
-        for clsname in closables:
-            assert stream_counts[clsname] == 0, (
-                'did not close off stream response correctly, expected '
-                'count of zero for %s: %s' % (clsname, stream_counts)
-            )
diff --git a/libraries/cherrypy/test/test_json.py b/libraries/cherrypy/test/test_json.py
deleted file mode 100644
index 1585f6e6..00000000
--- a/libraries/cherrypy/test/test_json.py
+++ /dev/null
@@ -1,102 +0,0 @@
-import cherrypy
-from cherrypy.test import helper
-
-from cherrypy._cpcompat import json
-
-
-json_out = cherrypy.config(**{'tools.json_out.on': True})
-json_in = cherrypy.config(**{'tools.json_in.on': True})
-
-
-class JsonTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root(object):
-
-            @cherrypy.expose
-            def plain(self):
-                return 'hello'
-
-            @cherrypy.expose
-            @json_out
-            def json_string(self):
-                return 'hello'
-
-            @cherrypy.expose
-            @json_out
-            def json_list(self):
-                return ['a', 'b', 42]
-
-            @cherrypy.expose
-            @json_out
-            def json_dict(self):
-                return {'answer': 42}
-
-            @cherrypy.expose
-            @json_in
-            def json_post(self):
-                if cherrypy.request.json == [13, 'c']:
-                    return 'ok'
-                else:
-                    return 'nok'
-
-            @cherrypy.expose
-            @json_out
-            @cherrypy.config(**{'tools.caching.on': True})
-            def json_cached(self):
-                return 'hello there'
-
-        root = Root()
-        cherrypy.tree.mount(root)
-
-    def test_json_output(self):
-        if json is None:
-            self.skip('json not found ')
-            return
-
-        self.getPage('/plain')
-        self.assertBody('hello')
-
-        self.getPage('/json_string')
-        self.assertBody('"hello"')
-
-        self.getPage('/json_list')
-        self.assertBody('["a", "b", 42]')
-
-        self.getPage('/json_dict')
-        self.assertBody('{"answer": 42}')
-
-    def test_json_input(self):
-        if json is None:
-            self.skip('json not found ')
-            return
-
-        body = '[13, "c"]'
-        headers = [('Content-Type', 'application/json'),
-                   ('Content-Length', str(len(body)))]
-        self.getPage('/json_post', method='POST', headers=headers, body=body)
-        self.assertBody('ok')
-
-        body = '[13, "c"]'
-        headers = [('Content-Type', 'text/plain'),
-                   ('Content-Length', str(len(body)))]
-        self.getPage('/json_post', method='POST', headers=headers, body=body)
-        self.assertStatus(415, 'Expected an application/json content type')
-
-        body = '[13, -]'
-        headers = [('Content-Type', 'application/json'),
-                   ('Content-Length', str(len(body)))]
-        self.getPage('/json_post', method='POST', headers=headers, body=body)
-        self.assertStatus(400, 'Invalid JSON document')
-
-    def test_cached(self):
-        if json is None:
-            self.skip('json not found ')
-            return
-
-        self.getPage('/json_cached')
-        self.assertStatus(200, '"hello"')
-
-        self.getPage('/json_cached')  # 2'nd time to hit cache
-        self.assertStatus(200, '"hello"')
diff --git a/libraries/cherrypy/test/test_logging.py b/libraries/cherrypy/test/test_logging.py
deleted file mode 100644
index c4948c20..00000000
--- a/libraries/cherrypy/test/test_logging.py
+++ /dev/null
@@ -1,209 +0,0 @@
-"""Basic tests for the CherryPy core: request handling."""
-
-import os
-from unittest import mock
-
-import six
-
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy.test import helper, logtest
-
-localDir = os.path.dirname(__file__)
-access_log = os.path.join(localDir, 'access.log')
-error_log = os.path.join(localDir, 'error.log')
-
-# Some unicode strings.
-tartaros = ntou('\u03a4\u1f71\u03c1\u03c4\u03b1\u03c1\u03bf\u03c2', 'escape')
-erebos = ntou('\u0388\u03c1\u03b5\u03b2\u03bf\u03c2.com', 'escape')
-
-
-def setup_server():
-    class Root:
-
-        @cherrypy.expose
-        def index(self):
-            return 'hello'
-
-        @cherrypy.expose
-        def uni_code(self):
-            cherrypy.request.login = tartaros
-            cherrypy.request.remote.name = erebos
-
-        @cherrypy.expose
-        def slashes(self):
-            cherrypy.request.request_line = r'GET /slashed\path HTTP/1.1'
-
-        @cherrypy.expose
-        def whitespace(self):
-            # User-Agent = "User-Agent" ":" 1*( product | comment )
-            # comment    = "(" *( ctext | quoted-pair | comment ) ")"
-            # ctext      = <any TEXT excluding "(" and ")">
-            # TEXT       = <any OCTET except CTLs, but including LWS>
-            # LWS        = [CRLF] 1*( SP | HT )
-            cherrypy.request.headers['User-Agent'] = 'Browzuh (1.0\r\n\t\t.3)'
-
-        @cherrypy.expose
-        def as_string(self):
-            return 'content'
-
-        @cherrypy.expose
-        def as_yield(self):
-            yield 'content'
-
-        @cherrypy.expose
-        @cherrypy.config(**{'tools.log_tracebacks.on': True})
-        def error(self):
-            raise ValueError()
-
-    root = Root()
-
-    cherrypy.config.update({
-        'log.error_file': error_log,
-        'log.access_file': access_log,
-    })
-    cherrypy.tree.mount(root)
-
-
-class AccessLogTests(helper.CPWebCase, logtest.LogCase):
-    setup_server = staticmethod(setup_server)
-
-    logfile = access_log
-
-    def testNormalReturn(self):
-        self.markLog()
-        self.getPage('/as_string',
-                     headers=[('Referer', 'http://www.cherrypy.org/'),
-                              ('User-Agent', 'Mozilla/5.0')])
-        self.assertBody('content')
-        self.assertStatus(200)
-
-        intro = '%s - - [' % self.interface()
-
-        self.assertLog(-1, intro)
-
-        if [k for k, v in self.headers if k.lower() == 'content-length']:
-            self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 7 '
-                           '"http://www.cherrypy.org/" "Mozilla/5.0"'
-                           % self.prefix())
-        else:
-            self.assertLog(-1, '] "GET %s/as_string HTTP/1.1" 200 - '
-                           '"http://www.cherrypy.org/" "Mozilla/5.0"'
-                           % self.prefix())
-
-    def testNormalYield(self):
-        self.markLog()
-        self.getPage('/as_yield')
-        self.assertBody('content')
-        self.assertStatus(200)
-
-        intro = '%s - - [' % self.interface()
-
-        self.assertLog(-1, intro)
-        if [k for k, v in self.headers if k.lower() == 'content-length']:
-            self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 7 "" ""' %
-                           self.prefix())
-        else:
-            self.assertLog(-1, '] "GET %s/as_yield HTTP/1.1" 200 - "" ""'
-                           % self.prefix())
-
-    @mock.patch(
-        'cherrypy._cplogging.LogManager.access_log_format',
-        '{h} {l} {u} {t} "{r}" {s} {b} "{f}" "{a}" {o}'
-        if six.PY3 else
-        '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
-    )
-    def testCustomLogFormat(self):
-        """Test a customized access_log_format string, which is a
-        feature of _cplogging.LogManager.access()."""
-        self.markLog()
-        self.getPage('/as_string', headers=[('Referer', 'REFERER'),
-                                            ('User-Agent', 'USERAGENT'),
-                                            ('Host', 'HOST')])
-        self.assertLog(-1, '%s - - [' % self.interface())
-        self.assertLog(-1, '] "GET /as_string HTTP/1.1" '
-                           '200 7 "REFERER" "USERAGENT" HOST')
-
-    @mock.patch(
-        'cherrypy._cplogging.LogManager.access_log_format',
-        '{h} {l} {u} {z} "{r}" {s} {b} "{f}" "{a}" {o}'
-        if six.PY3 else
-        '%(h)s %(l)s %(u)s %(z)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s" %(o)s'
-    )
-    def testTimezLogFormat(self):
-        """Test a customized access_log_format string, which is a
-        feature of _cplogging.LogManager.access()."""
-        self.markLog()
-
-        expected_time = str(cherrypy._cplogging.LazyRfc3339UtcTime())
-        with mock.patch(
-                'cherrypy._cplogging.LazyRfc3339UtcTime',
-                lambda: expected_time):
-            self.getPage('/as_string', headers=[('Referer', 'REFERER'),
-                                                ('User-Agent', 'USERAGENT'),
-                                                ('Host', 'HOST')])
-
-        self.assertLog(-1, '%s - - ' % self.interface())
-        self.assertLog(-1, expected_time)
-        self.assertLog(-1, ' "GET /as_string HTTP/1.1" '
-                           '200 7 "REFERER" "USERAGENT" HOST')
-
-    @mock.patch(
-        'cherrypy._cplogging.LogManager.access_log_format',
-        '{i}' if six.PY3 else '%(i)s'
-    )
-    def testUUIDv4ParameterLogFormat(self):
-        """Test rendering of UUID4 within access log."""
-        self.markLog()
-        self.getPage('/as_string')
-        self.assertValidUUIDv4()
-
-    def testEscapedOutput(self):
-        # Test unicode in access log pieces.
-        self.markLog()
-        self.getPage('/uni_code')
-        self.assertStatus(200)
-        if six.PY3:
-            # The repr of a bytestring in six.PY3 includes a b'' prefix
-            self.assertLog(-1, repr(tartaros.encode('utf8'))[2:-1])
-        else:
-            self.assertLog(-1, repr(tartaros.encode('utf8'))[1:-1])
-        # Test the erebos value. Included inline for your enlightenment.
-        # Note the 'r' prefix--those backslashes are literals.
-        self.assertLog(-1, r'\xce\x88\xcf\x81\xce\xb5\xce\xb2\xce\xbf\xcf\x82')
-
-        # Test backslashes in output.
-        self.markLog()
-        self.getPage('/slashes')
-        self.assertStatus(200)
-        if six.PY3:
-            self.assertLog(-1, b'"GET /slashed\\path HTTP/1.1"')
-        else:
-            self.assertLog(-1, r'"GET /slashed\\path HTTP/1.1"')
-
-        # Test whitespace in output.
-        self.markLog()
-        self.getPage('/whitespace')
-        self.assertStatus(200)
-        # Again, note the 'r' prefix.
-        self.assertLog(-1, r'"Browzuh (1.0\r\n\t\t.3)"')
-
-
-class ErrorLogTests(helper.CPWebCase, logtest.LogCase):
-    setup_server = staticmethod(setup_server)
-
-    logfile = error_log
-
-    def testTracebacks(self):
-        # Test that tracebacks get written to the error log.
-        self.markLog()
-        ignore = helper.webtest.ignored_exceptions
-        ignore.append(ValueError)
-        try:
-            self.getPage('/error')
-            self.assertInBody('raise ValueError()')
-            self.assertLog(0, 'HTTP')
-            self.assertLog(1, 'Traceback (most recent call last):')
-            self.assertLog(-2, 'raise ValueError()')
-        finally:
-            ignore.pop()
diff --git a/libraries/cherrypy/test/test_mime.py b/libraries/cherrypy/test/test_mime.py
deleted file mode 100644
index ef35d10e..00000000
--- a/libraries/cherrypy/test/test_mime.py
+++ /dev/null
@@ -1,134 +0,0 @@
-"""Tests for various MIME issues, including the safe_multipart Tool."""
-
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy.test import helper
-
-
-def setup_server():
-
-    class Root:
-
-        @cherrypy.expose
-        def multipart(self, parts):
-            return repr(parts)
-
-        @cherrypy.expose
-        def multipart_form_data(self, **kwargs):
-            return repr(list(sorted(kwargs.items())))
-
-        @cherrypy.expose
-        def flashupload(self, Filedata, Upload, Filename):
-            return ('Upload: %s, Filename: %s, Filedata: %r' %
-                    (Upload, Filename, Filedata.file.read()))
-
-    cherrypy.config.update({'server.max_request_body_size': 0})
-    cherrypy.tree.mount(Root())
-
-
-#                             Client-side code                             #
-
-
-class MultipartTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_multipart(self):
-        text_part = ntou('This is the text version')
-        html_part = ntou(
-            """<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
-<html>
-<head>
- <meta content="text/html;charset=ISO-8859-1" http-equiv="Content-Type">
-</head>
-<body bgcolor="#ffffff" text="#000000">
-
-This is the <strong>HTML</strong> version
-</body>
-</html>
-""")
-        body = '\r\n'.join([
-            '--123456789',
-            "Content-Type: text/plain; charset='ISO-8859-1'",
-            'Content-Transfer-Encoding: 7bit',
-            '',
-            text_part,
-            '--123456789',
-            "Content-Type: text/html; charset='ISO-8859-1'",
-            '',
-            html_part,
-            '--123456789--'])
-        headers = [
-            ('Content-Type', 'multipart/mixed; boundary=123456789'),
-            ('Content-Length', str(len(body))),
-        ]
-        self.getPage('/multipart', headers, 'POST', body)
-        self.assertBody(repr([text_part, html_part]))
-
-    def test_multipart_form_data(self):
-        body = '\r\n'.join([
-            '--X',
-            'Content-Disposition: form-data; name="foo"',
-            '',
-            'bar',
-            '--X',
-            # Test a param with more than one value.
-            # See
-            # https://github.com/cherrypy/cherrypy/issues/1028
-            'Content-Disposition: form-data; name="baz"',
-            '',
-            '111',
-            '--X',
-            'Content-Disposition: form-data; name="baz"',
-            '',
-            '333',
-            '--X--'
-        ])
-        self.getPage('/multipart_form_data', method='POST',
-                     headers=[(
-                         'Content-Type', 'multipart/form-data;boundary=X'),
-                         ('Content-Length', str(len(body))),
-                     ],
-                     body=body),
-        self.assertBody(
-            repr([('baz', [ntou('111'), ntou('333')]), ('foo', ntou('bar'))]))
-
-
-class SafeMultipartHandlingTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_Flash_Upload(self):
-        headers = [
-            ('Accept', 'text/*'),
-            ('Content-Type', 'multipart/form-data; '
-             'boundary=----------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6'),
-            ('User-Agent', 'Shockwave Flash'),
-            ('Host', 'www.example.com:54583'),
-            ('Content-Length', '499'),
-            ('Connection', 'Keep-Alive'),
-            ('Cache-Control', 'no-cache'),
-        ]
-        filedata = (b'<?xml version="1.0" encoding="UTF-8"?>\r\n'
-                    b'<projectDescription>\r\n'
-                    b'</projectDescription>\r\n')
-        body = (
-            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
-            b'Content-Disposition: form-data; name="Filename"\r\n'
-            b'\r\n'
-            b'.project\r\n'
-            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
-            b'Content-Disposition: form-data; '
-            b'name="Filedata"; filename=".project"\r\n'
-            b'Content-Type: application/octet-stream\r\n'
-            b'\r\n' +
-            filedata +
-            b'\r\n'
-            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6\r\n'
-            b'Content-Disposition: form-data; name="Upload"\r\n'
-            b'\r\n'
-            b'Submit Query\r\n'
-            # Flash apps omit the trailing \r\n on the last line:
-            b'------------KM7Ij5cH2KM7Ef1gL6ae0ae0cH2gL6--'
-        )
-        self.getPage('/flashupload', headers, 'POST', body)
-        self.assertBody('Upload: Submit Query, Filename: .project, '
-                        'Filedata: %r' % filedata)
diff --git a/libraries/cherrypy/test/test_misc_tools.py b/libraries/cherrypy/test/test_misc_tools.py
deleted file mode 100644
index fb85b8f8..00000000
--- a/libraries/cherrypy/test/test_misc_tools.py
+++ /dev/null
@@ -1,210 +0,0 @@
-import os
-
-import cherrypy
-from cherrypy import tools
-from cherrypy.test import helper
-
-
-localDir = os.path.dirname(__file__)
-logfile = os.path.join(localDir, 'test_misc_tools.log')
-
-
-def setup_server():
-    class Root:
-
-        @cherrypy.expose
-        def index(self):
-            yield 'Hello, world'
-        h = [('Content-Language', 'en-GB'), ('Content-Type', 'text/plain')]
-        tools.response_headers(headers=h)(index)
-
-        @cherrypy.expose
-        @cherrypy.config(**{
-            'tools.response_headers.on': True,
-            'tools.response_headers.headers': [
-                ('Content-Language', 'fr'),
-                ('Content-Type', 'text/plain'),
-            ],
-            'tools.log_hooks.on': True,
-        })
-        def other(self):
-            return 'salut'
-
-    @cherrypy.config(**{'tools.accept.on': True})
-    class Accept:
-
-        @cherrypy.expose
-        def index(self):
-            return '<a href="feed">Atom feed</a>'
-
-        @cherrypy.expose
-        @tools.accept(media='application/atom+xml')
-        def feed(self):
-            return """<?xml version="1.0" encoding="utf-8"?>
-<feed xmlns="http://www.w3.org/2005/Atom">
-    <title>Unknown Blog</title>
-</feed>"""
-
-        @cherrypy.expose
-        def select(self):
-            # We could also write this: mtype = cherrypy.lib.accept.accept(...)
-            mtype = tools.accept.callable(['text/html', 'text/plain'])
-            if mtype == 'text/html':
-                return '<h2>Page Title</h2>'
-            else:
-                return 'PAGE TITLE'
-
-    class Referer:
-
-        @cherrypy.expose
-        def accept(self):
-            return 'Accepted!'
-        reject = accept
-
-    class AutoVary:
-
-        @cherrypy.expose
-        def index(self):
-            # Read a header directly with 'get'
-            cherrypy.request.headers.get('Accept-Encoding')
-            # Read a header directly with '__getitem__'
-            cherrypy.request.headers['Host']
-            # Read a header directly with '__contains__'
-            'If-Modified-Since' in cherrypy.request.headers
-            # Read a header directly
-            'Range' in cherrypy.request.headers
-            # Call a lib function
-            tools.accept.callable(['text/html', 'text/plain'])
-            return 'Hello, world!'
-
-    conf = {'/referer': {'tools.referer.on': True,
-                         'tools.referer.pattern': r'http://[^/]*example\.com',
-                         },
-            '/referer/reject': {'tools.referer.accept': False,
-                                'tools.referer.accept_missing': True,
-                                },
-            '/autovary': {'tools.autovary.on': True},
-            }
-
-    root = Root()
-    root.referer = Referer()
-    root.accept = Accept()
-    root.autovary = AutoVary()
-    cherrypy.tree.mount(root, config=conf)
-    cherrypy.config.update({'log.error_file': logfile})
-
-
-class ResponseHeadersTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def testResponseHeadersDecorator(self):
-        self.getPage('/')
-        self.assertHeader('Content-Language', 'en-GB')
-        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
-
-    def testResponseHeaders(self):
-        self.getPage('/other')
-        self.assertHeader('Content-Language', 'fr')
-        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
-
-
-class RefererTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def testReferer(self):
-        self.getPage('/referer/accept')
-        self.assertErrorPage(403, 'Forbidden Referer header.')
-
-        self.getPage('/referer/accept',
-                     headers=[('Referer', 'http://www.example.com/')])
-        self.assertStatus(200)
-        self.assertBody('Accepted!')
-
-        # Reject
-        self.getPage('/referer/reject')
-        self.assertStatus(200)
-        self.assertBody('Accepted!')
-
-        self.getPage('/referer/reject',
-                     headers=[('Referer', 'http://www.example.com/')])
-        self.assertErrorPage(403, 'Forbidden Referer header.')
-
-
-class AcceptTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def test_Accept_Tool(self):
-        # Test with no header provided
-        self.getPage('/accept/feed')
-        self.assertStatus(200)
-        self.assertInBody('<title>Unknown Blog</title>')
-
-        # Specify exact media type
-        self.getPage('/accept/feed',
-                     headers=[('Accept', 'application/atom+xml')])
-        self.assertStatus(200)
-        self.assertInBody('<title>Unknown Blog</title>')
-
-        # Specify matching media range
-        self.getPage('/accept/feed', headers=[('Accept', 'application/*')])
-        self.assertStatus(200)
-        self.assertInBody('<title>Unknown Blog</title>')
-
-        # Specify all media ranges
-        self.getPage('/accept/feed', headers=[('Accept', '*/*')])
-        self.assertStatus(200)
-        self.assertInBody('<title>Unknown Blog</title>')
-
-        # Specify unacceptable media types
-        self.getPage('/accept/feed', headers=[('Accept', 'text/html')])
-        self.assertErrorPage(406,
-                             'Your client sent this Accept header: text/html. '
-                             'But this resource only emits these media types: '
-                             'application/atom+xml.')
-
-        # Test resource where tool is 'on' but media is None (not set).
-        self.getPage('/accept/')
-        self.assertStatus(200)
-        self.assertBody('<a href="feed">Atom feed</a>')
-
-    def test_accept_selection(self):
-        # Try both our expected media types
-        self.getPage('/accept/select', [('Accept', 'text/html')])
-        self.assertStatus(200)
-        self.assertBody('<h2>Page Title</h2>')
-        self.getPage('/accept/select', [('Accept', 'text/plain')])
-        self.assertStatus(200)
-        self.assertBody('PAGE TITLE')
-        self.getPage('/accept/select',
-                     [('Accept', 'text/plain, text/*;q=0.5')])
-        self.assertStatus(200)
-        self.assertBody('PAGE TITLE')
-
-        # text/* and */* should prefer text/html since it comes first
-        # in our 'media' argument to tools.accept
-        self.getPage('/accept/select', [('Accept', 'text/*')])
-        self.assertStatus(200)
-        self.assertBody('<h2>Page Title</h2>')
-        self.getPage('/accept/select', [('Accept', '*/*')])
-        self.assertStatus(200)
-        self.assertBody('<h2>Page Title</h2>')
-
-        # Try unacceptable media types
-        self.getPage('/accept/select', [('Accept', 'application/xml')])
-        self.assertErrorPage(
-            406,
-            'Your client sent this Accept header: application/xml. '
-            'But this resource only emits these media types: '
-            'text/html, text/plain.')
-
-
-class AutoVaryTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def testAutoVary(self):
-        self.getPage('/autovary/')
-        self.assertHeader(
-            'Vary',
-            'Accept, Accept-Charset, Accept-Encoding, '
-            'Host, If-Modified-Since, Range'
-        )
diff --git a/libraries/cherrypy/test/test_native.py b/libraries/cherrypy/test/test_native.py
deleted file mode 100644
index caebc3f4..00000000
--- a/libraries/cherrypy/test/test_native.py
+++ /dev/null
@@ -1,35 +0,0 @@
-"""Test the native server."""
-
-import pytest
-from requests_toolbelt import sessions
-
-import cherrypy._cpnative_server
-
-
-pytestmark = pytest.mark.skipif(
-    'sys.platform == "win32"',
-    reason='tests fail on Windows',
-)
-
-
-@pytest.fixture
-def cp_native_server(request):
-    """A native server."""
-    class Root(object):
-        @cherrypy.expose
-        def index(self):
-            return 'Hello World!'
-
-    cls = cherrypy._cpnative_server.CPHTTPServer
-    cherrypy.server.httpserver = cls(cherrypy.server)
-
-    cherrypy.tree.mount(Root(), '/')
-    cherrypy.engine.start()
-    request.addfinalizer(cherrypy.engine.stop)
-    url = 'http://localhost:{cherrypy.server.socket_port}'.format(**globals())
-    return sessions.BaseUrlSession(url)
-
-
-def test_basic_request(cp_native_server):
-    """A request to a native server should succeed."""
-    cp_native_server.get('/')
diff --git a/libraries/cherrypy/test/test_objectmapping.py b/libraries/cherrypy/test/test_objectmapping.py
deleted file mode 100644
index 98402b8b..00000000
--- a/libraries/cherrypy/test/test_objectmapping.py
+++ /dev/null
@@ -1,430 +0,0 @@
-import sys
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy._cptree import Application
-from cherrypy.test import helper
-
-script_names = ['', '/foo', '/users/fred/blog', '/corp/blog']
-
-
-class ObjectMappingTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self, name='world'):
-                return name
-
-            @cherrypy.expose
-            def foobar(self):
-                return 'bar'
-
-            @cherrypy.expose
-            def default(self, *params, **kwargs):
-                return 'default:' + repr(params)
-
-            @cherrypy.expose
-            def other(self):
-                return 'other'
-
-            @cherrypy.expose
-            def extra(self, *p):
-                return repr(p)
-
-            @cherrypy.expose
-            def redirect(self):
-                raise cherrypy.HTTPRedirect('dir1/', 302)
-
-            def notExposed(self):
-                return 'not exposed'
-
-            @cherrypy.expose
-            def confvalue(self):
-                return cherrypy.request.config.get('user')
-
-            @cherrypy.expose
-            def redirect_via_url(self, path):
-                raise cherrypy.HTTPRedirect(cherrypy.url(path))
-
-            @cherrypy.expose
-            def translate_html(self):
-                return 'OK'
-
-        @cherrypy.expose
-        def mapped_func(self, ID=None):
-            return 'ID is %s' % ID
-        setattr(Root, 'Von B\xfclow', mapped_func)
-
-        class Exposing:
-
-            @cherrypy.expose
-            def base(self):
-                return 'expose works!'
-            cherrypy.expose(base, '1')
-            cherrypy.expose(base, '2')
-
-        class ExposingNewStyle(object):
-
-            @cherrypy.expose
-            def base(self):
-                return 'expose works!'
-            cherrypy.expose(base, '1')
-            cherrypy.expose(base, '2')
-
-        class Dir1:
-
-            @cherrypy.expose
-            def index(self):
-                return 'index for dir1'
-
-            @cherrypy.expose
-            @cherrypy.config(**{'tools.trailing_slash.extra': True})
-            def myMethod(self):
-                return 'myMethod from dir1, path_info is:' + repr(
-                    cherrypy.request.path_info)
-
-            @cherrypy.expose
-            def default(self, *params):
-                return 'default for dir1, param is:' + repr(params)
-
-        class Dir2:
-
-            @cherrypy.expose
-            def index(self):
-                return 'index for dir2, path is:' + cherrypy.request.path_info
-
-            @cherrypy.expose
-            def script_name(self):
-                return cherrypy.tree.script_name()
-
-            @cherrypy.expose
-            def cherrypy_url(self):
-                return cherrypy.url('/extra')
-
-            @cherrypy.expose
-            def posparam(self, *vpath):
-                return '/'.join(vpath)
-
-        class Dir3:
-
-            def default(self):
-                return 'default for dir3, not exposed'
-
-        class Dir4:
-
-            def index(self):
-                return 'index for dir4, not exposed'
-
-        class DefNoIndex:
-
-            @cherrypy.expose
-            def default(self, *args):
-                raise cherrypy.HTTPRedirect('contact')
-
-        # MethodDispatcher code
-        @cherrypy.expose
-        class ByMethod:
-
-            def __init__(self, *things):
-                self.things = list(things)
-
-            def GET(self):
-                return repr(self.things)
-
-            def POST(self, thing):
-                self.things.append(thing)
-
-        class Collection:
-            default = ByMethod('a', 'bit')
-
-        Root.exposing = Exposing()
-        Root.exposingnew = ExposingNewStyle()
-        Root.dir1 = Dir1()
-        Root.dir1.dir2 = Dir2()
-        Root.dir1.dir2.dir3 = Dir3()
-        Root.dir1.dir2.dir3.dir4 = Dir4()
-        Root.defnoindex = DefNoIndex()
-        Root.bymethod = ByMethod('another')
-        Root.collection = Collection()
-
-        d = cherrypy.dispatch.MethodDispatcher()
-        for url in script_names:
-            conf = {'/': {'user': (url or '/').split('/')[-2]},
-                    '/bymethod': {'request.dispatch': d},
-                    '/collection': {'request.dispatch': d},
-                    }
-            cherrypy.tree.mount(Root(), url, conf)
-
-        class Isolated:
-
-            @cherrypy.expose
-            def index(self):
-                return 'made it!'
-
-        cherrypy.tree.mount(Isolated(), '/isolated')
-
-        @cherrypy.expose
-        class AnotherApp:
-
-            def GET(self):
-                return 'milk'
-
-        cherrypy.tree.mount(AnotherApp(), '/app',
-                            {'/': {'request.dispatch': d}})
-
-    def testObjectMapping(self):
-        for url in script_names:
-            self.script_name = url
-
-            self.getPage('/')
-            self.assertBody('world')
-
-            self.getPage('/dir1/myMethod')
-            self.assertBody(
-                "myMethod from dir1, path_info is:'/dir1/myMethod'")
-
-            self.getPage('/this/method/does/not/exist')
-            self.assertBody(
-                "default:('this', 'method', 'does', 'not', 'exist')")
-
-            self.getPage('/extra/too/much')
-            self.assertBody("('too', 'much')")
-
-            self.getPage('/other')
-            self.assertBody('other')
-
-            self.getPage('/notExposed')
-            self.assertBody("default:('notExposed',)")
-
-            self.getPage('/dir1/dir2/')
-            self.assertBody('index for dir2, path is:/dir1/dir2/')
-
-            # Test omitted trailing slash (should be redirected by default).
-            self.getPage('/dir1/dir2')
-            self.assertStatus(301)
-            self.assertHeader('Location', '%s/dir1/dir2/' % self.base())
-
-            # Test extra trailing slash (should be redirected if configured).
-            self.getPage('/dir1/myMethod/')
-            self.assertStatus(301)
-            self.assertHeader('Location', '%s/dir1/myMethod' % self.base())
-
-            # Test that default method must be exposed in order to match.
-            self.getPage('/dir1/dir2/dir3/dir4/index')
-            self.assertBody(
-                "default for dir1, param is:('dir2', 'dir3', 'dir4', 'index')")
-
-            # Test *vpath when default() is defined but not index()
-            # This also tests HTTPRedirect with default.
-            self.getPage('/defnoindex')
-            self.assertStatus((302, 303))
-            self.assertHeader('Location', '%s/contact' % self.base())
-            self.getPage('/defnoindex/')
-            self.assertStatus((302, 303))
-            self.assertHeader('Location', '%s/defnoindex/contact' %
-                              self.base())
-            self.getPage('/defnoindex/page')
-            self.assertStatus((302, 303))
-            self.assertHeader('Location', '%s/defnoindex/contact' %
-                              self.base())
-
-            self.getPage('/redirect')
-            self.assertStatus('302 Found')
-            self.assertHeader('Location', '%s/dir1/' % self.base())
-
-            if not getattr(cherrypy.server, 'using_apache', False):
-                # Test that we can use URL's which aren't all valid Python
-                # identifiers
-                # This should also test the %XX-unquoting of URL's.
-                self.getPage('/Von%20B%fclow?ID=14')
-                self.assertBody('ID is 14')
-
-                # Test that %2F in the path doesn't get unquoted too early;
-                # that is, it should not be used to separate path components.
-                # See ticket #393.
-                self.getPage('/page%2Fname')
-                self.assertBody("default:('page/name',)")
-
-            self.getPage('/dir1/dir2/script_name')
-            self.assertBody(url)
-            self.getPage('/dir1/dir2/cherrypy_url')
-            self.assertBody('%s/extra' % self.base())
-
-            # Test that configs don't overwrite each other from different apps
-            self.getPage('/confvalue')
-            self.assertBody((url or '/').split('/')[-2])
-
-        self.script_name = ''
-
-        # Test absoluteURI's in the Request-Line
-        self.getPage('http://%s:%s/' % (self.interface(), self.PORT))
-        self.assertBody('world')
-
-        self.getPage('http://%s:%s/abs/?service=http://192.168.0.1/x/y/z' %
-                     (self.interface(), self.PORT))
-        self.assertBody("default:('abs',)")
-
-        self.getPage('/rel/?service=http://192.168.120.121:8000/x/y/z')
-        self.assertBody("default:('rel',)")
-
-        # Test that the "isolated" app doesn't leak url's into the root app.
-        # If it did leak, Root.default() would answer with
-        #   "default:('isolated', 'doesnt', 'exist')".
-        self.getPage('/isolated/')
-        self.assertStatus('200 OK')
-        self.assertBody('made it!')
-        self.getPage('/isolated/doesnt/exist')
-        self.assertStatus('404 Not Found')
-
-        # Make sure /foobar maps to Root.foobar and not to the app
-        # mounted at /foo. See
-        # https://github.com/cherrypy/cherrypy/issues/573
-        self.getPage('/foobar')
-        self.assertBody('bar')
-
-    def test_translate(self):
-        self.getPage('/translate_html')
-        self.assertStatus('200 OK')
-        self.assertBody('OK')
-
-        self.getPage('/translate.html')
-        self.assertStatus('200 OK')
-        self.assertBody('OK')
-
-        self.getPage('/translate-html')
-        self.assertStatus('200 OK')
-        self.assertBody('OK')
-
-    def test_redir_using_url(self):
-        for url in script_names:
-            self.script_name = url
-
-            # Test the absolute path to the parent (leading slash)
-            self.getPage('/redirect_via_url?path=./')
-            self.assertStatus(('302 Found', '303 See Other'))
-            self.assertHeader('Location', '%s/' % self.base())
-
-            # Test the relative path to the parent (no leading slash)
-            self.getPage('/redirect_via_url?path=./')
-            self.assertStatus(('302 Found', '303 See Other'))
-            self.assertHeader('Location', '%s/' % self.base())
-
-            # Test the absolute path to the parent (leading slash)
-            self.getPage('/redirect_via_url/?path=./')
-            self.assertStatus(('302 Found', '303 See Other'))
-            self.assertHeader('Location', '%s/' % self.base())
-
-            # Test the relative path to the parent (no leading slash)
-            self.getPage('/redirect_via_url/?path=./')
-            self.assertStatus(('302 Found', '303 See Other'))
-            self.assertHeader('Location', '%s/' % self.base())
-
-    def testPositionalParams(self):
-        self.getPage('/dir1/dir2/posparam/18/24/hut/hike')
-        self.assertBody('18/24/hut/hike')
-
-        # intermediate index methods should not receive posparams;
-        # only the "final" index method should do so.
-        self.getPage('/dir1/dir2/5/3/sir')
-        self.assertBody("default for dir1, param is:('dir2', '5', '3', 'sir')")
-
-        # test that extra positional args raises an 404 Not Found
-        # See https://github.com/cherrypy/cherrypy/issues/733.
-        self.getPage('/dir1/dir2/script_name/extra/stuff')
-        self.assertStatus(404)
-
-    def testExpose(self):
-        # Test the cherrypy.expose function/decorator
-        self.getPage('/exposing/base')
-        self.assertBody('expose works!')
-
-        self.getPage('/exposing/1')
-        self.assertBody('expose works!')
-
-        self.getPage('/exposing/2')
-        self.assertBody('expose works!')
-
-        self.getPage('/exposingnew/base')
-        self.assertBody('expose works!')
-
-        self.getPage('/exposingnew/1')
-        self.assertBody('expose works!')
-
-        self.getPage('/exposingnew/2')
-        self.assertBody('expose works!')
-
-    def testMethodDispatch(self):
-        self.getPage('/bymethod')
-        self.assertBody("['another']")
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        self.getPage('/bymethod', method='HEAD')
-        self.assertBody('')
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        self.getPage('/bymethod', method='POST', body='thing=one')
-        self.assertBody('')
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        self.getPage('/bymethod')
-        self.assertBody(repr(['another', ntou('one')]))
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        self.getPage('/bymethod', method='PUT')
-        self.assertErrorPage(405)
-        self.assertHeader('Allow', 'GET, HEAD, POST')
-
-        # Test default with posparams
-        self.getPage('/collection/silly', method='POST')
-        self.getPage('/collection', method='GET')
-        self.assertBody("['a', 'bit', 'silly']")
-
-        # Test custom dispatcher set on app root (see #737).
-        self.getPage('/app')
-        self.assertBody('milk')
-
-    def testTreeMounting(self):
-        class Root(object):
-
-            @cherrypy.expose
-            def hello(self):
-                return 'Hello world!'
-
-        # When mounting an application instance,
-        # we can't specify a different script name in the call to mount.
-        a = Application(Root(), '/somewhere')
-        self.assertRaises(ValueError, cherrypy.tree.mount, a, '/somewhereelse')
-
-        # When mounting an application instance...
-        a = Application(Root(), '/somewhere')
-        # ...we MUST allow in identical script name in the call to mount...
-        cherrypy.tree.mount(a, '/somewhere')
-        self.getPage('/somewhere/hello')
-        self.assertStatus(200)
-        # ...and MUST allow a missing script_name.
-        del cherrypy.tree.apps['/somewhere']
-        cherrypy.tree.mount(a)
-        self.getPage('/somewhere/hello')
-        self.assertStatus(200)
-
-        # In addition, we MUST be able to create an Application using
-        # script_name == None for access to the wsgi_environ.
-        a = Application(Root(), script_name=None)
-        # However, this does not apply to tree.mount
-        self.assertRaises(TypeError, cherrypy.tree.mount, a, None)
-
-    def testKeywords(self):
-        if sys.version_info < (3,):
-            return self.skip('skipped (Python 3 only)')
-        exec("""class Root(object):
-    @cherrypy.expose
-    def hello(self, *, name='world'):
-        return 'Hello %s!' % name
-cherrypy.tree.mount(Application(Root(), '/keywords'))""")
-
-        self.getPage('/keywords/hello')
-        self.assertStatus(200)
-        self.getPage('/keywords/hello/extra')
-        self.assertStatus(404)
diff --git a/libraries/cherrypy/test/test_params.py b/libraries/cherrypy/test/test_params.py
deleted file mode 100644
index 73b4cb4c..00000000
--- a/libraries/cherrypy/test/test_params.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import sys
-import textwrap
-
-import cherrypy
-from cherrypy.test import helper
-
-
-class ParamsTest(helper.CPWebCase):
-    @staticmethod
-    def setup_server():
-        class Root:
-            @cherrypy.expose
-            @cherrypy.tools.json_out()
-            @cherrypy.tools.params()
-            def resource(self, limit=None, sort=None):
-                return type(limit).__name__
-            # for testing on Py 2
-            resource.__annotations__ = {'limit': int}
-        conf = {'/': {'tools.params.on': True}}
-        cherrypy.tree.mount(Root(), config=conf)
-
-    def test_pass(self):
-        self.getPage('/resource')
-        self.assertStatus(200)
-        self.assertBody('"NoneType"')
-
-        self.getPage('/resource?limit=0')
-        self.assertStatus(200)
-        self.assertBody('"int"')
-
-    def test_error(self):
-        self.getPage('/resource?limit=')
-        self.assertStatus(400)
-        self.assertInBody('invalid literal for int')
-
-        cherrypy.config['tools.params.error'] = 422
-        self.getPage('/resource?limit=')
-        self.assertStatus(422)
-        self.assertInBody('invalid literal for int')
-
-        cherrypy.config['tools.params.exception'] = TypeError
-        self.getPage('/resource?limit=')
-        self.assertStatus(500)
-
-    def test_syntax(self):
-        if sys.version_info < (3,):
-            return self.skip('skipped (Python 3 only)')
-        code = textwrap.dedent("""
-            class Root:
-                @cherrypy.expose
-                @cherrypy.tools.params()
-                def resource(self, limit: int):
-                    return type(limit).__name__
-            conf = {'/': {'tools.params.on': True}}
-            cherrypy.tree.mount(Root(), config=conf)
-            """)
-        exec(code)
-
-        self.getPage('/resource?limit=0')
-        self.assertStatus(200)
-        self.assertBody('int')
diff --git a/libraries/cherrypy/test/test_plugins.py b/libraries/cherrypy/test/test_plugins.py
deleted file mode 100644
index 4d3aa6b1..00000000
--- a/libraries/cherrypy/test/test_plugins.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from cherrypy.process import plugins
-
-
-__metaclass__ = type
-
-
-class TestAutoreloader:
-    def test_file_for_file_module_when_None(self):
-        """No error when module.__file__ is None.
-        """
-        class test_module:
-            __file__ = None
-
-        assert plugins.Autoreloader._file_for_file_module(test_module) is None
diff --git a/libraries/cherrypy/test/test_proxy.py b/libraries/cherrypy/test/test_proxy.py
deleted file mode 100644
index 4d34440a..00000000
--- a/libraries/cherrypy/test/test_proxy.py
+++ /dev/null
@@ -1,154 +0,0 @@
-import cherrypy
-from cherrypy.test import helper
-
-script_names = ['', '/path/to/myapp']
-
-
-class ProxyTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        # Set up site
-        cherrypy.config.update({
-            'tools.proxy.on': True,
-            'tools.proxy.base': 'www.mydomain.test',
-        })
-
-        # Set up application
-
-        class Root:
-
-            def __init__(self, sn):
-                # Calculate a URL outside of any requests.
-                self.thisnewpage = cherrypy.url(
-                    '/this/new/page', script_name=sn)
-
-            @cherrypy.expose
-            def pageurl(self):
-                return self.thisnewpage
-
-            @cherrypy.expose
-            def index(self):
-                raise cherrypy.HTTPRedirect('dummy')
-
-            @cherrypy.expose
-            def remoteip(self):
-                return cherrypy.request.remote.ip
-
-            @cherrypy.expose
-            @cherrypy.config(**{
-                'tools.proxy.local': 'X-Host',
-                'tools.trailing_slash.extra': True,
-            })
-            def xhost(self):
-                raise cherrypy.HTTPRedirect('blah')
-
-            @cherrypy.expose
-            def base(self):
-                return cherrypy.request.base
-
-            @cherrypy.expose
-            @cherrypy.config(**{'tools.proxy.scheme': 'X-Forwarded-Ssl'})
-            def ssl(self):
-                return cherrypy.request.base
-
-            @cherrypy.expose
-            def newurl(self):
-                return ("Browse to <a href='%s'>this page</a>."
-                        % cherrypy.url('/this/new/page'))
-
-            @cherrypy.expose
-            @cherrypy.config(**{
-                'tools.proxy.base': None,
-            })
-            def base_no_base(self):
-                return cherrypy.request.base
-
-        for sn in script_names:
-            cherrypy.tree.mount(Root(sn), sn)
-
-    def testProxy(self):
-        self.getPage('/')
-        self.assertHeader('Location',
-                          '%s://www.mydomain.test%s/dummy' %
-                          (self.scheme, self.prefix()))
-
-        # Test X-Forwarded-Host (Apache 1.3.33+ and Apache 2)
-        self.getPage(
-            '/', headers=[('X-Forwarded-Host', 'http://www.example.test')])
-        self.assertHeader('Location', 'http://www.example.test/dummy')
-        self.getPage('/', headers=[('X-Forwarded-Host', 'www.example.test')])
-        self.assertHeader('Location', '%s://www.example.test/dummy' %
-                          self.scheme)
-        # Test multiple X-Forwarded-Host headers
-        self.getPage('/', headers=[
-            ('X-Forwarded-Host', 'http://www.example.test, www.cherrypy.test'),
-        ])
-        self.assertHeader('Location', 'http://www.example.test/dummy')
-
-        # Test X-Forwarded-For (Apache2)
-        self.getPage('/remoteip',
-                     headers=[('X-Forwarded-For', '192.168.0.20')])
-        self.assertBody('192.168.0.20')
-        # Fix bug #1268
-        self.getPage('/remoteip',
-                     headers=[
-                         ('X-Forwarded-For', '67.15.36.43, 192.168.0.20')
-                     ])
-        self.assertBody('67.15.36.43')
-
-        # Test X-Host (lighttpd; see https://trac.lighttpd.net/trac/ticket/418)
-        self.getPage('/xhost', headers=[('X-Host', 'www.example.test')])
-        self.assertHeader('Location', '%s://www.example.test/blah' %
-                          self.scheme)
-
-        # Test X-Forwarded-Proto (lighttpd)
-        self.getPage('/base', headers=[('X-Forwarded-Proto', 'https')])
-        self.assertBody('https://www.mydomain.test')
-
-        # Test X-Forwarded-Ssl (webfaction?)
-        self.getPage('/ssl', headers=[('X-Forwarded-Ssl', 'on')])
-        self.assertBody('https://www.mydomain.test')
-
-        # Test cherrypy.url()
-        for sn in script_names:
-            # Test the value inside requests
-            self.getPage(sn + '/newurl')
-            self.assertBody(
-                "Browse to <a href='%s://www.mydomain.test" % self.scheme +
-                sn + "/this/new/page'>this page</a>.")
-            self.getPage(sn + '/newurl', headers=[('X-Forwarded-Host',
-                                                   'http://www.example.test')])
-            self.assertBody("Browse to <a href='http://www.example.test" +
-                            sn + "/this/new/page'>this page</a>.")
-
-            # Test the value outside requests
-            port = ''
-            if self.scheme == 'http' and self.PORT != 80:
-                port = ':%s' % self.PORT
-            elif self.scheme == 'https' and self.PORT != 443:
-                port = ':%s' % self.PORT
-            host = self.HOST
-            if host in ('0.0.0.0', '::'):
-                import socket
-                host = socket.gethostname()
-            expected = ('%s://%s%s%s/this/new/page'
-                        % (self.scheme, host, port, sn))
-            self.getPage(sn + '/pageurl')
-            self.assertBody(expected)
-
-        # Test trailing slash (see
-        # https://github.com/cherrypy/cherrypy/issues/562).
-        self.getPage('/xhost/', headers=[('X-Host', 'www.example.test')])
-        self.assertHeader('Location', '%s://www.example.test/xhost'
-                          % self.scheme)
-
-    def test_no_base_port_in_host(self):
-        """
-        If no base is indicated, and the host header is used to resolve
-        the base, it should rely on the host header for the port also.
-        """
-        headers = {'Host': 'localhost:8080'}.items()
-        self.getPage('/base_no_base', headers=headers)
-        self.assertBody('http://localhost:8080')
diff --git a/libraries/cherrypy/test/test_refleaks.py b/libraries/cherrypy/test/test_refleaks.py
deleted file mode 100644
index c2fe9e66..00000000
--- a/libraries/cherrypy/test/test_refleaks.py
+++ /dev/null
@@ -1,66 +0,0 @@
-"""Tests for refleaks."""
-
-import itertools
-import platform
-import threading
-
-from six.moves.http_client import HTTPConnection
-
-import cherrypy
-from cherrypy._cpcompat import HTTPSConnection
-from cherrypy.test import helper
-
-
-data = object()
-
-
-class ReferenceTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        class Root:
-
-            @cherrypy.expose
-            def index(self, *args, **kwargs):
-                cherrypy.request.thing = data
-                return 'Hello world!'
-
-        cherrypy.tree.mount(Root())
-
-    def test_threadlocal_garbage(self):
-        if platform.system() == 'Darwin':
-            self.skip('queue issues; see #1474')
-        success = itertools.count()
-
-        def getpage():
-            host = '%s:%s' % (self.interface(), self.PORT)
-            if self.scheme == 'https':
-                c = HTTPSConnection(host)
-            else:
-                c = HTTPConnection(host)
-            try:
-                c.putrequest('GET', '/')
-                c.endheaders()
-                response = c.getresponse()
-                body = response.read()
-                self.assertEqual(response.status, 200)
-                self.assertEqual(body, b'Hello world!')
-            finally:
-                c.close()
-            next(success)
-
-        ITERATIONS = 25
-
-        ts = [
-            threading.Thread(target=getpage)
-            for _ in range(ITERATIONS)
-        ]
-
-        for t in ts:
-            t.start()
-
-        for t in ts:
-            t.join()
-
-        self.assertEqual(next(success), ITERATIONS)
diff --git a/libraries/cherrypy/test/test_request_obj.py b/libraries/cherrypy/test/test_request_obj.py
deleted file mode 100644
index 6b93e13d..00000000
--- a/libraries/cherrypy/test/test_request_obj.py
+++ /dev/null
@@ -1,932 +0,0 @@
-"""Basic tests for the cherrypy.Request object."""
-
-from functools import wraps
-import os
-import sys
-import types
-import uuid
-
-import six
-from six.moves.http_client import IncompleteRead
-
-import cherrypy
-from cherrypy._cpcompat import ntou
-from cherrypy.lib import httputil
-from cherrypy.test import helper
-
-localDir = os.path.dirname(__file__)
-
-defined_http_methods = ('OPTIONS', 'GET', 'HEAD', 'POST', 'PUT', 'DELETE',
-                        'TRACE', 'PROPFIND', 'PATCH')
-
-
-#                             Client-side code                             #
-
-
-class RequestObjectTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'hello'
-
-            @cherrypy.expose
-            def scheme(self):
-                return cherrypy.request.scheme
-
-            @cherrypy.expose
-            def created_example_com_3128(self):
-                """Handle CONNECT method."""
-                cherrypy.response.status = 204
-
-            @cherrypy.expose
-            def body_example_com_3128(self):
-                """Handle CONNECT method."""
-                return (
-                    cherrypy.request.method
-                    + 'ed to '
-                    + cherrypy.request.path_info
-                )
-
-            @cherrypy.expose
-            def request_uuid4(self):
-                return [
-                    str(cherrypy.request.unique_id),
-                    ' ',
-                    str(cherrypy.request.unique_id),
-                ]
-
-        root = Root()
-
-        class TestType(type):
-            """Metaclass which automatically exposes all functions in each
-            subclass, and adds an instance of the subclass as an attribute
-            of root.
-            """
-            def __init__(cls, name, bases, dct):
-                type.__init__(cls, name, bases, dct)
-                for value in dct.values():
-                    if isinstance(value, types.FunctionType):
-                        value.exposed = True
-                setattr(root, name.lower(), cls())
-        Test = TestType('Test', (object,), {})
-
-        class PathInfo(Test):
-
-            def default(self, *args):
-                return cherrypy.request.path_info
-
-        class Params(Test):
-
-            def index(self, thing):
-                return repr(thing)
-
-            def ismap(self, x, y):
-                return 'Coordinates: %s, %s' % (x, y)
-
-            @cherrypy.config(**{'request.query_string_encoding': 'latin1'})
-            def default(self, *args, **kwargs):
-                return 'args: %s kwargs: %s' % (args, sorted(kwargs.items()))
-
-        @cherrypy.expose
-        class ParamErrorsCallable(object):
-
-            def __call__(self):
-                return 'data'
-
-        def handler_dec(f):
-            @wraps(f)
-            def wrapper(handler, *args, **kwargs):
-                return f(handler, *args, **kwargs)
-            return wrapper
-
-        class ParamErrors(Test):
-
-            @cherrypy.expose
-            def one_positional(self, param1):
-                return 'data'
-
-            @cherrypy.expose
-            def one_positional_args(self, param1, *args):
-                return 'data'
-
-            @cherrypy.expose
-            def one_positional_args_kwargs(self, param1, *args, **kwargs):
-                return 'data'
-
-            @cherrypy.expose
-            def one_positional_kwargs(self, param1, **kwargs):
-                return 'data'
-
-            @cherrypy.expose
-            def no_positional(self):
-                return 'data'
-
-            @cherrypy.expose
-            def no_positional_args(self, *args):
-                return 'data'
-
-            @cherrypy.expose
-            def no_positional_args_kwargs(self, *args, **kwargs):
-                return 'data'
-
-            @cherrypy.expose
-            def no_positional_kwargs(self, **kwargs):
-                return 'data'
-
-            callable_object = ParamErrorsCallable()
-
-            @cherrypy.expose
-            def raise_type_error(self, **kwargs):
-                raise TypeError('Client Error')
-
-            @cherrypy.expose
-            def raise_type_error_with_default_param(self, x, y=None):
-                return '%d' % 'a'  # throw an exception
-
-            @cherrypy.expose
-            @handler_dec
-            def raise_type_error_decorated(self, *args, **kwargs):
-                raise TypeError('Client Error')
-
-        def callable_error_page(status, **kwargs):
-            return "Error %s - Well, I'm very sorry but you haven't paid!" % (
-                status)
-
-        @cherrypy.config(**{'tools.log_tracebacks.on': True})
-        class Error(Test):
-
-            def reason_phrase(self):
-                raise cherrypy.HTTPError("410 Gone fishin'")
-
-            @cherrypy.config(**{
-                'error_page.404': os.path.join(localDir, 'static/index.html'),
-                'error_page.401': callable_error_page,
-            })
-            def custom(self, err='404'):
-                raise cherrypy.HTTPError(
-                    int(err), 'No, <b>really</b>, not found!')
-
-            @cherrypy.config(**{
-                'error_page.default': callable_error_page,
-            })
-            def custom_default(self):
-                return 1 + 'a'  # raise an unexpected error
-
-            @cherrypy.config(**{'error_page.404': 'nonexistent.html'})
-            def noexist(self):
-                raise cherrypy.HTTPError(404, 'No, <b>really</b>, not found!')
-
-            def page_method(self):
-                raise ValueError()
-
-            def page_yield(self):
-                yield 'howdy'
-                raise ValueError()
-
-            @cherrypy.config(**{'response.stream': True})
-            def page_streamed(self):
-                yield 'word up'
-                raise ValueError()
-                yield 'very oops'
-
-            @cherrypy.config(**{'request.show_tracebacks': False})
-            def cause_err_in_finalize(self):
-                # Since status must start with an int, this should error.
-                cherrypy.response.status = 'ZOO OK'
-
-            @cherrypy.config(**{'request.throw_errors': True})
-            def rethrow(self):
-                """Test that an error raised here will be thrown out to
-                the server.
-                """
-                raise ValueError()
-
-        class Expect(Test):
-
-            def expectation_failed(self):
-                expect = cherrypy.request.headers.elements('Expect')
-                if expect and expect[0].value != '100-continue':
-                    raise cherrypy.HTTPError(400)
-                raise cherrypy.HTTPError(417, 'Expectation Failed')
-
-        class Headers(Test):
-
-            def default(self, headername):
-                """Spit back out the value for the requested header."""
-                return cherrypy.request.headers[headername]
-
-            def doubledheaders(self):
-                # From https://github.com/cherrypy/cherrypy/issues/165:
-                # "header field names should not be case sensitive sayes the
-                # rfc. if i set a headerfield in complete lowercase i end up
-                # with two header fields, one in lowercase, the other in
-                # mixed-case."
-
-                # Set the most common headers
-                hMap = cherrypy.response.headers
-                hMap['content-type'] = 'text/html'
-                hMap['content-length'] = 18
-                hMap['server'] = 'CherryPy headertest'
-                hMap['location'] = ('%s://%s:%s/headers/'
-                                    % (cherrypy.request.local.ip,
-                                       cherrypy.request.local.port,
-                                       cherrypy.request.scheme))
-
-                # Set a rare header for fun
-                hMap['Expires'] = 'Thu, 01 Dec 2194 16:00:00 GMT'
-
-                return 'double header test'
-
-            def ifmatch(self):
-                val = cherrypy.request.headers['If-Match']
-                assert isinstance(val, six.text_type)
-                cherrypy.response.headers['ETag'] = val
-                return val
-
-        class HeaderElements(Test):
-
-            def get_elements(self, headername):
-                e = cherrypy.request.headers.elements(headername)
-                return '\n'.join([six.text_type(x) for x in e])
-
-        class Method(Test):
-
-            def index(self):
-                m = cherrypy.request.method
-                if m in defined_http_methods or m == 'CONNECT':
-                    return m
-
-                if m == 'LINK':
-                    raise cherrypy.HTTPError(405)
-                else:
-                    raise cherrypy.HTTPError(501)
-
-            def parameterized(self, data):
-                return data
-
-            def request_body(self):
-                # This should be a file object (temp file),
-                # which CP will just pipe back out if we tell it to.
-                return cherrypy.request.body
-
-            def reachable(self):
-                return 'success'
-
-        class Divorce(Test):
-
-            """HTTP Method handlers shouldn't collide with normal method names.
-            For example, a GET-handler shouldn't collide with a method named
-            'get'.
-
-            If you build HTTP method dispatching into CherryPy, rewrite this
-            class to use your new dispatch mechanism and make sure that:
-                "GET /divorce HTTP/1.1" maps to divorce.index() and
-                "GET /divorce/get?ID=13 HTTP/1.1" maps to divorce.get()
-            """
-
-            documents = {}
-
-            @cherrypy.expose
-            def index(self):
-                yield '<h1>Choose your document</h1>\n'
-                yield '<ul>\n'
-                for id, contents in self.documents.items():
-                    yield (
-                        "    <li><a href='/divorce/get?ID=%s'>%s</a>:"
-                        ' %s</li>\n' % (id, id, contents))
-                yield '</ul>'
-
-            @cherrypy.expose
-            def get(self, ID):
-                return ('Divorce document %s: %s' %
-                        (ID, self.documents.get(ID, 'empty')))
-
-        class ThreadLocal(Test):
-
-            def index(self):
-                existing = repr(getattr(cherrypy.request, 'asdf', None))
-                cherrypy.request.asdf = 'rassfrassin'
-                return existing
-
-        appconf = {
-            '/method': {
-                'request.methods_with_bodies': ('POST', 'PUT', 'PROPFIND',
-                                                'PATCH')
-            },
-        }
-        cherrypy.tree.mount(root, config=appconf)
-
-    def test_scheme(self):
-        self.getPage('/scheme')
-        self.assertBody(self.scheme)
-
-    def test_per_request_uuid4(self):
-        self.getPage('/request_uuid4')
-        first_uuid4, _, second_uuid4 = self.body.decode().partition(' ')
-        assert (
-            uuid.UUID(first_uuid4, version=4)
-            == uuid.UUID(second_uuid4, version=4)
-        )
-
-        self.getPage('/request_uuid4')
-        third_uuid4, _, _ = self.body.decode().partition(' ')
-        assert (
-            uuid.UUID(first_uuid4, version=4)
-            != uuid.UUID(third_uuid4, version=4)
-        )
-
-    def testRelativeURIPathInfo(self):
-        self.getPage('/pathinfo/foo/bar')
-        self.assertBody('/pathinfo/foo/bar')
-
-    def testAbsoluteURIPathInfo(self):
-        # http://cherrypy.org/ticket/1061
-        self.getPage('http://localhost/pathinfo/foo/bar')
-        self.assertBody('/pathinfo/foo/bar')
-
-    def testParams(self):
-        self.getPage('/params/?thing=a')
-        self.assertBody(repr(ntou('a')))
-
-        self.getPage('/params/?thing=a&thing=b&thing=c')
-        self.assertBody(repr([ntou('a'), ntou('b'), ntou('c')]))
-
-        # Test friendly error message when given params are not accepted.
-        cherrypy.config.update({'request.show_mismatched_params': True})
-        self.getPage('/params/?notathing=meeting')
-        self.assertInBody('Missing parameters: thing')
-        self.getPage('/params/?thing=meeting&notathing=meeting')
-        self.assertInBody('Unexpected query string parameters: notathing')
-
-        # Test ability to turn off friendly error messages
-        cherrypy.config.update({'request.show_mismatched_params': False})
-        self.getPage('/params/?notathing=meeting')
-        self.assertInBody('Not Found')
-        self.getPage('/params/?thing=meeting&notathing=meeting')
-        self.assertInBody('Not Found')
-
-        # Test "% HEX HEX"-encoded URL, param keys, and values
-        self.getPage('/params/%d4%20%e3/cheese?Gruy%E8re=Bulgn%e9ville')
-        self.assertBody('args: %s kwargs: %s' %
-                        (('\xd4 \xe3', 'cheese'),
-                         [('Gruy\xe8re', ntou('Bulgn\xe9ville'))]))
-
-        # Make sure that encoded = and & get parsed correctly
-        self.getPage(
-            '/params/code?url=http%3A//cherrypy.org/index%3Fa%3D1%26b%3D2')
-        self.assertBody('args: %s kwargs: %s' %
-                        (('code',),
-                         [('url', ntou('http://cherrypy.org/index?a=1&b=2'))]))
-
-        # Test coordinates sent by <img ismap>
-        self.getPage('/params/ismap?223,114')
-        self.assertBody('Coordinates: 223, 114')
-
-        # Test "name[key]" dict-like params
-        self.getPage('/params/dictlike?a[1]=1&a[2]=2&b=foo&b[bar]=baz')
-        self.assertBody('args: %s kwargs: %s' %
-                        (('dictlike',),
-                         [('a[1]', ntou('1')), ('a[2]', ntou('2')),
-                          ('b', ntou('foo')), ('b[bar]', ntou('baz'))]))
-
-    def testParamErrors(self):
-
-        # test that all of the handlers work when given
-        # the correct parameters in order to ensure that the
-        # errors below aren't coming from some other source.
-        for uri in (
-                '/paramerrors/one_positional?param1=foo',
-                '/paramerrors/one_positional_args?param1=foo',
-                '/paramerrors/one_positional_args/foo',
-                '/paramerrors/one_positional_args/foo/bar/baz',
-                '/paramerrors/one_positional_args_kwargs?'
-                'param1=foo&param2=bar',
-                '/paramerrors/one_positional_args_kwargs/foo?'
-                'param2=bar&param3=baz',
-                '/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
-                'param2=bar&param3=baz',
-                '/paramerrors/one_positional_kwargs?'
-                'param1=foo&param2=bar&param3=baz',
-                '/paramerrors/one_positional_kwargs/foo?'
-                'param4=foo&param2=bar&param3=baz',
-                '/paramerrors/no_positional',
-                '/paramerrors/no_positional_args/foo',
-                '/paramerrors/no_positional_args/foo/bar/baz',
-                '/paramerrors/no_positional_args_kwargs?param1=foo&param2=bar',
-                '/paramerrors/no_positional_args_kwargs/foo?param2=bar',
-                '/paramerrors/no_positional_args_kwargs/foo/bar/baz?'
-                'param2=bar&param3=baz',
-                '/paramerrors/no_positional_kwargs?param1=foo&param2=bar',
-                '/paramerrors/callable_object',
-        ):
-            self.getPage(uri)
-            self.assertStatus(200)
-
-        error_msgs = [
-            'Missing parameters',
-            'Nothing matches the given URI',
-            'Multiple values for parameters',
-            'Unexpected query string parameters',
-            'Unexpected body parameters',
-            'Invalid path in Request-URI',
-            'Illegal #fragment in Request-URI',
-        ]
-
-        # uri should be tested for valid absolute path, the status must be 400.
-        for uri, error_idx in (
-            ('invalid/path/without/leading/slash', 5),
-            ('/valid/path#invalid=fragment', 6),
-        ):
-            self.getPage(uri)
-            self.assertStatus(400)
-            self.assertInBody(error_msgs[error_idx])
-
-        # query string parameters are part of the URI, so if they are wrong
-        # for a particular handler, the status MUST be a 404.
-        for uri, msg in (
-            ('/paramerrors/one_positional', error_msgs[0]),
-            ('/paramerrors/one_positional?foo=foo', error_msgs[0]),
-            ('/paramerrors/one_positional/foo/bar/baz', error_msgs[1]),
-            ('/paramerrors/one_positional/foo?param1=foo', error_msgs[2]),
-            ('/paramerrors/one_positional/foo?param1=foo&param2=foo',
-             error_msgs[2]),
-            ('/paramerrors/one_positional_args/foo?param1=foo&param2=foo',
-             error_msgs[2]),
-            ('/paramerrors/one_positional_args/foo/bar/baz?param2=foo',
-             error_msgs[3]),
-            ('/paramerrors/one_positional_args_kwargs/foo/bar/baz?'
-             'param1=bar&param3=baz',
-             error_msgs[2]),
-            ('/paramerrors/one_positional_kwargs/foo?'
-             'param1=foo&param2=bar&param3=baz',
-             error_msgs[2]),
-            ('/paramerrors/no_positional/boo', error_msgs[1]),
-            ('/paramerrors/no_positional?param1=foo', error_msgs[3]),
-            ('/paramerrors/no_positional_args/boo?param1=foo', error_msgs[3]),
-            ('/paramerrors/no_positional_kwargs/boo?param1=foo',
-             error_msgs[1]),
-            ('/paramerrors/callable_object?param1=foo', error_msgs[3]),
-            ('/paramerrors/callable_object/boo', error_msgs[1]),
-        ):
-            for show_mismatched_params in (True, False):
-                cherrypy.config.update(
-                    {'request.show_mismatched_params': show_mismatched_params})
-                self.getPage(uri)
-                self.assertStatus(404)
-                if show_mismatched_params:
-                    self.assertInBody(msg)
-                else:
-                    self.assertInBody('Not Found')
-
-        # if body parameters are wrong, a 400 must be returned.
-        for uri, body, msg in (
-                ('/paramerrors/one_positional/foo',
-                 'param1=foo', error_msgs[2]),
-                ('/paramerrors/one_positional/foo',
-                 'param1=foo&param2=foo', error_msgs[2]),
-                ('/paramerrors/one_positional_args/foo',
-                 'param1=foo&param2=foo', error_msgs[2]),
-                ('/paramerrors/one_positional_args/foo/bar/baz',
-                 'param2=foo', error_msgs[4]),
-                ('/paramerrors/one_positional_args_kwargs/foo/bar/baz',
-                 'param1=bar&param3=baz', error_msgs[2]),
-                ('/paramerrors/one_positional_kwargs/foo',
-                 'param1=foo&param2=bar&param3=baz', error_msgs[2]),
-                ('/paramerrors/no_positional', 'param1=foo', error_msgs[4]),
-                ('/paramerrors/no_positional_args/boo',
-                 'param1=foo', error_msgs[4]),
-                ('/paramerrors/callable_object', 'param1=foo', error_msgs[4]),
-        ):
-            for show_mismatched_params in (True, False):
-                cherrypy.config.update(
-                    {'request.show_mismatched_params': show_mismatched_params})
-                self.getPage(uri, method='POST', body=body)
-                self.assertStatus(400)
-                if show_mismatched_params:
-                    self.assertInBody(msg)
-                else:
-                    self.assertInBody('400 Bad')
-
-        # even if body parameters are wrong, if we get the uri wrong, then
-        # it's a 404
-        for uri, body, msg in (
-                ('/paramerrors/one_positional?param2=foo',
-                 'param1=foo', error_msgs[3]),
-                ('/paramerrors/one_positional/foo/bar',
-                 'param2=foo', error_msgs[1]),
-                ('/paramerrors/one_positional_args/foo/bar?param2=foo',
-                 'param3=foo', error_msgs[3]),
-                ('/paramerrors/one_positional_kwargs/foo/bar',
-                 'param2=bar&param3=baz', error_msgs[1]),
-                ('/paramerrors/no_positional?param1=foo',
-                 'param2=foo', error_msgs[3]),
-                ('/paramerrors/no_positional_args/boo?param2=foo',
-                 'param1=foo', error_msgs[3]),
-                ('/paramerrors/callable_object?param2=bar',
-                 'param1=foo', error_msgs[3]),
-        ):
-            for show_mismatched_params in (True, False):
-                cherrypy.config.update(
-                    {'request.show_mismatched_params': show_mismatched_params})
-                self.getPage(uri, method='POST', body=body)
-                self.assertStatus(404)
-                if show_mismatched_params:
-                    self.assertInBody(msg)
-                else:
-                    self.assertInBody('Not Found')
-
-        # In the case that a handler raises a TypeError we should
-        # let that type error through.
-        for uri in (
-                '/paramerrors/raise_type_error',
-                '/paramerrors/raise_type_error_with_default_param?x=0',
-                '/paramerrors/raise_type_error_with_default_param?x=0&y=0',
-                '/paramerrors/raise_type_error_decorated',
-        ):
-            self.getPage(uri, method='GET')
-            self.assertStatus(500)
-            self.assertTrue('Client Error', self.body)
-
-    def testErrorHandling(self):
-        self.getPage('/error/missing')
-        self.assertStatus(404)
-        self.assertErrorPage(404, "The path '/error/missing' was not found.")
-
-        ignore = helper.webtest.ignored_exceptions
-        ignore.append(ValueError)
-        try:
-            valerr = '\n    raise ValueError()\nValueError'
-            self.getPage('/error/page_method')
-            self.assertErrorPage(500, pattern=valerr)
-
-            self.getPage('/error/page_yield')
-            self.assertErrorPage(500, pattern=valerr)
-
-            if (cherrypy.server.protocol_version == 'HTTP/1.0' or
-                    getattr(cherrypy.server, 'using_apache', False)):
-                self.getPage('/error/page_streamed')
-                # Because this error is raised after the response body has
-                # started, the status should not change to an error status.
-                self.assertStatus(200)
-                self.assertBody('word up')
-            else:
-                # Under HTTP/1.1, the chunked transfer-coding is used.
-                # The HTTP client will choke when the output is incomplete.
-                self.assertRaises((ValueError, IncompleteRead), self.getPage,
-                                  '/error/page_streamed')
-
-            # No traceback should be present
-            self.getPage('/error/cause_err_in_finalize')
-            msg = "Illegal response status from server ('ZOO' is non-numeric)."
-            self.assertErrorPage(500, msg, None)
-        finally:
-            ignore.pop()
-
-        # Test HTTPError with a reason-phrase in the status arg.
-        self.getPage('/error/reason_phrase')
-        self.assertStatus("410 Gone fishin'")
-
-        # Test custom error page for a specific error.
-        self.getPage('/error/custom')
-        self.assertStatus(404)
-        self.assertBody('Hello, world\r\n' + (' ' * 499))
-
-        # Test custom error page for a specific error.
-        self.getPage('/error/custom?err=401')
-        self.assertStatus(401)
-        self.assertBody(
-            'Error 401 Unauthorized - '
-            "Well, I'm very sorry but you haven't paid!")
-
-        # Test default custom error page.
-        self.getPage('/error/custom_default')
-        self.assertStatus(500)
-        self.assertBody(
-            'Error 500 Internal Server Error - '
-            "Well, I'm very sorry but you haven't paid!".ljust(513))
-
-        # Test error in custom error page (ticket #305).
-        # Note that the message is escaped for HTML (ticket #310).
-        self.getPage('/error/noexist')
-        self.assertStatus(404)
-        if sys.version_info >= (3, 3):
-            exc_name = 'FileNotFoundError'
-        else:
-            exc_name = 'IOError'
-        msg = ('No, &lt;b&gt;really&lt;/b&gt;, not found!<br />'
-               'In addition, the custom error page failed:\n<br />'
-               '%s: [Errno 2] '
-               "No such file or directory: 'nonexistent.html'") % (exc_name,)
-        self.assertInBody(msg)
-
-        if getattr(cherrypy.server, 'using_apache', False):
-            pass
-        else:
-            # Test throw_errors (ticket #186).
-            self.getPage('/error/rethrow')
-            self.assertInBody('raise ValueError()')
-
-    def testExpect(self):
-        e = ('Expect', '100-continue')
-        self.getPage('/headerelements/get_elements?headername=Expect', [e])
-        self.assertBody('100-continue')
-
-        self.getPage('/expect/expectation_failed', [e])
-        self.assertStatus(417)
-
-    def testHeaderElements(self):
-        # Accept-* header elements should be sorted, with most preferred first.
-        h = [('Accept', 'audio/*; q=0.2, audio/basic')]
-        self.getPage('/headerelements/get_elements?headername=Accept', h)
-        self.assertStatus(200)
-        self.assertBody('audio/basic\n'
-                        'audio/*;q=0.2')
-
-        h = [
-            ('Accept',
-             'text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c')
-        ]
-        self.getPage('/headerelements/get_elements?headername=Accept', h)
-        self.assertStatus(200)
-        self.assertBody('text/x-c\n'
-                        'text/html\n'
-                        'text/x-dvi;q=0.8\n'
-                        'text/plain;q=0.5')
-
-        # Test that more specific media ranges get priority.
-        h = [('Accept', 'text/*, text/html, text/html;level=1, */*')]
-        self.getPage('/headerelements/get_elements?headername=Accept', h)
-        self.assertStatus(200)
-        self.assertBody('text/html;level=1\n'
-                        'text/html\n'
-                        'text/*\n'
-                        '*/*')
-
-        # Test Accept-Charset
-        h = [('Accept-Charset', 'iso-8859-5, unicode-1-1;q=0.8')]
-        self.getPage(
-            '/headerelements/get_elements?headername=Accept-Charset', h)
-        self.assertStatus('200 OK')
-        self.assertBody('iso-8859-5\n'
-                        'unicode-1-1;q=0.8')
-
-        # Test Accept-Encoding
-        h = [('Accept-Encoding', 'gzip;q=1.0, identity; q=0.5, *;q=0')]
-        self.getPage(
-            '/headerelements/get_elements?headername=Accept-Encoding', h)
-        self.assertStatus('200 OK')
-        self.assertBody('gzip;q=1.0\n'
-                        'identity;q=0.5\n'
-                        '*;q=0')
-
-        # Test Accept-Language
-        h = [('Accept-Language', 'da, en-gb;q=0.8, en;q=0.7')]
-        self.getPage(
-            '/headerelements/get_elements?headername=Accept-Language', h)
-        self.assertStatus('200 OK')
-        self.assertBody('da\n'
-                        'en-gb;q=0.8\n'
-                        'en;q=0.7')
-
-        # Test malformed header parsing. See
-        # https://github.com/cherrypy/cherrypy/issues/763.
-        self.getPage('/headerelements/get_elements?headername=Content-Type',
-                     # Note the illegal trailing ";"
-                     headers=[('Content-Type', 'text/html; charset=utf-8;')])
-        self.assertStatus(200)
-        self.assertBody('text/html;charset=utf-8')
-
-    def test_repeated_headers(self):
-        # Test that two request headers are collapsed into one.
-        # See https://github.com/cherrypy/cherrypy/issues/542.
-        self.getPage('/headers/Accept-Charset',
-                     headers=[('Accept-Charset', 'iso-8859-5'),
-                              ('Accept-Charset', 'unicode-1-1;q=0.8')])
-        self.assertBody('iso-8859-5, unicode-1-1;q=0.8')
-
-        # Tests that each header only appears once, regardless of case.
-        self.getPage('/headers/doubledheaders')
-        self.assertBody('double header test')
-        hnames = [name.title() for name, val in self.headers]
-        for key in ['Content-Length', 'Content-Type', 'Date',
-                    'Expires', 'Location', 'Server']:
-            self.assertEqual(hnames.count(key), 1, self.headers)
-
-    def test_encoded_headers(self):
-        # First, make sure the innards work like expected.
-        self.assertEqual(
-            httputil.decode_TEXT(ntou('=?utf-8?q?f=C3=BCr?=')), ntou('f\xfcr'))
-
-        if cherrypy.server.protocol_version == 'HTTP/1.1':
-            # Test RFC-2047-encoded request and response header values
-            u = ntou('\u212bngstr\xf6m', 'escape')
-            c = ntou('=E2=84=ABngstr=C3=B6m')
-            self.getPage('/headers/ifmatch',
-                         [('If-Match', ntou('=?utf-8?q?%s?=') % c)])
-            # The body should be utf-8 encoded.
-            self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m')
-            # But the Etag header should be RFC-2047 encoded (binary)
-            self.assertHeader('ETag', ntou('=?utf-8?b?4oSrbmdzdHLDtm0=?='))
-
-            # Test a *LONG* RFC-2047-encoded request and response header value
-            self.getPage('/headers/ifmatch',
-                         [('If-Match', ntou('=?utf-8?q?%s?=') % (c * 10))])
-            self.assertBody(b'\xe2\x84\xabngstr\xc3\xb6m' * 10)
-            # Note: this is different output for Python3, but it decodes fine.
-            etag = self.assertHeader(
-                'ETag',
-                '=?utf-8?b?4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
-                '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
-                '4oSrbmdzdHLDtm3ihKtuZ3N0csO2beKEq25nc3Ryw7Zt'
-                '4oSrbmdzdHLDtm0=?=')
-            self.assertEqual(httputil.decode_TEXT(etag), u * 10)
-
-    def test_header_presence(self):
-        # If we don't pass a Content-Type header, it should not be present
-        # in cherrypy.request.headers
-        self.getPage('/headers/Content-Type',
-                     headers=[])
-        self.assertStatus(500)
-
-        # If Content-Type is present in the request, it should be present in
-        # cherrypy.request.headers
-        self.getPage('/headers/Content-Type',
-                     headers=[('Content-type', 'application/json')])
-        self.assertBody('application/json')
-
-    def test_basic_HTTPMethods(self):
-        helper.webtest.methods_with_bodies = ('POST', 'PUT', 'PROPFIND',
-                                              'PATCH')
-
-        # Test that all defined HTTP methods work.
-        for m in defined_http_methods:
-            self.getPage('/method/', method=m)
-
-            # HEAD requests should not return any body.
-            if m == 'HEAD':
-                self.assertBody('')
-            elif m == 'TRACE':
-                # Some HTTP servers (like modpy) have their own TRACE support
-                self.assertEqual(self.body[:5], b'TRACE')
-            else:
-                self.assertBody(m)
-
-        # test of PATCH requests
-        # Request a PATCH method with a form-urlencoded body
-        self.getPage('/method/parameterized', method='PATCH',
-                     body='data=on+top+of+other+things')
-        self.assertBody('on top of other things')
-
-        # Request a PATCH method with a file body
-        b = 'one thing on top of another'
-        h = [('Content-Type', 'text/plain'),
-             ('Content-Length', str(len(b)))]
-        self.getPage('/method/request_body', headers=h, method='PATCH', body=b)
-        self.assertStatus(200)
-        self.assertBody(b)
-
-        # Request a PATCH method with a file body but no Content-Type.
-        # See https://github.com/cherrypy/cherrypy/issues/790.
-        b = b'one thing on top of another'
-        self.persistent = True
-        try:
-            conn = self.HTTP_CONN
-            conn.putrequest('PATCH', '/method/request_body', skip_host=True)
-            conn.putheader('Host', self.HOST)
-            conn.putheader('Content-Length', str(len(b)))
-            conn.endheaders()
-            conn.send(b)
-            response = conn.response_class(conn.sock, method='PATCH')
-            response.begin()
-            self.assertEqual(response.status, 200)
-            self.body = response.read()
-            self.assertBody(b)
-        finally:
-            self.persistent = False
-
-        # Request a PATCH method with no body whatsoever (not an empty one).
-        # See https://github.com/cherrypy/cherrypy/issues/650.
-        # Provide a C-T or webtest will provide one (and a C-L) for us.
-        h = [('Content-Type', 'text/plain')]
-        self.getPage('/method/reachable', headers=h, method='PATCH')
-        self.assertStatus(411)
-
-        # HTTP PUT tests
-        # Request a PUT method with a form-urlencoded body
-        self.getPage('/method/parameterized', method='PUT',
-                     body='data=on+top+of+other+things')
-        self.assertBody('on top of other things')
-
-        # Request a PUT method with a file body
-        b = 'one thing on top of another'
-        h = [('Content-Type', 'text/plain'),
-             ('Content-Length', str(len(b)))]
-        self.getPage('/method/request_body', headers=h, method='PUT', body=b)
-        self.assertStatus(200)
-        self.assertBody(b)
-
-        # Request a PUT method with a file body but no Content-Type.
-        # See https://github.com/cherrypy/cherrypy/issues/790.
-        b = b'one thing on top of another'
-        self.persistent = True
-        try:
-            conn = self.HTTP_CONN
-            conn.putrequest('PUT', '/method/request_body', skip_host=True)
-            conn.putheader('Host', self.HOST)
-            conn.putheader('Content-Length', str(len(b)))
-            conn.endheaders()
-            conn.send(b)
-            response = conn.response_class(conn.sock, method='PUT')
-            response.begin()
-            self.assertEqual(response.status, 200)
-            self.body = response.read()
-            self.assertBody(b)
-        finally:
-            self.persistent = False
-
-        # Request a PUT method with no body whatsoever (not an empty one).
-        # See https://github.com/cherrypy/cherrypy/issues/650.
-        # Provide a C-T or webtest will provide one (and a C-L) for us.
-        h = [('Content-Type', 'text/plain')]
-        self.getPage('/method/reachable', headers=h, method='PUT')
-        self.assertStatus(411)
-
-        # Request a custom method with a request body
-        b = ('<?xml version="1.0" encoding="utf-8" ?>\n\n'
-             '<propfind xmlns="DAV:"><prop><getlastmodified/>'
-             '</prop></propfind>')
-        h = [('Content-Type', 'text/xml'),
-             ('Content-Length', str(len(b)))]
-        self.getPage('/method/request_body', headers=h,
-                     method='PROPFIND', body=b)
-        self.assertStatus(200)
-        self.assertBody(b)
-
-        # Request a disallowed method
-        self.getPage('/method/', method='LINK')
-        self.assertStatus(405)
-
-        # Request an unknown method
-        self.getPage('/method/', method='SEARCH')
-        self.assertStatus(501)
-
-        # For method dispatchers: make sure that an HTTP method doesn't
-        # collide with a virtual path atom. If you build HTTP-method
-        # dispatching into the core, rewrite these handlers to use
-        # your dispatch idioms.
-        self.getPage('/divorce/get?ID=13')
-        self.assertBody('Divorce document 13: empty')
-        self.assertStatus(200)
-        self.getPage('/divorce/', method='GET')
-        self.assertBody('<h1>Choose your document</h1>\n<ul>\n</ul>')
-        self.assertStatus(200)
-
-    def test_CONNECT_method(self):
-        self.persistent = True
-        try:
-            conn = self.HTTP_CONN
-            conn.request('CONNECT', 'created.example.com:3128')
-            response = conn.response_class(conn.sock, method='CONNECT')
-            response.begin()
-            self.assertEqual(response.status, 204)
-        finally:
-            self.persistent = False
-
-        self.persistent = True
-        try:
-            conn = self.HTTP_CONN
-            conn.request('CONNECT', 'body.example.com:3128')
-            response = conn.response_class(conn.sock, method='CONNECT')
-            response.begin()
-            self.assertEqual(response.status, 200)
-            self.body = response.read()
-            self.assertBody(b'CONNECTed to /body.example.com:3128')
-        finally:
-            self.persistent = False
-
-    def test_CONNECT_method_invalid_authority(self):
-        for request_target in ['example.com', 'http://example.com:33',
-                               '/path/', 'path/', '/?q=f', '#f']:
-            self.persistent = True
-            try:
-                conn = self.HTTP_CONN
-                conn.request('CONNECT', request_target)
-                response = conn.response_class(conn.sock, method='CONNECT')
-                response.begin()
-                self.assertEqual(response.status, 400)
-                self.body = response.read()
-                self.assertBody(b'Invalid path in Request-URI: request-target '
-                                b'must match authority-form.')
-            finally:
-                self.persistent = False
-
-    def testEmptyThreadlocals(self):
-        results = []
-        for x in range(20):
-            self.getPage('/threadlocal/')
-            results.append(self.body)
-        self.assertEqual(results, [b'None'] * 20)
diff --git a/libraries/cherrypy/test/test_routes.py b/libraries/cherrypy/test/test_routes.py
deleted file mode 100644
index cc714765..00000000
--- a/libraries/cherrypy/test/test_routes.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""Test Routes dispatcher."""
-import os
-import importlib
-
-import pytest
-
-import cherrypy
-from cherrypy.test import helper
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-class RoutesDispatchTest(helper.CPWebCase):
-    """Routes dispatcher test suite."""
-
-    @staticmethod
-    def setup_server():
-        """Set up cherrypy test instance."""
-        try:
-            importlib.import_module('routes')
-        except ImportError:
-            pytest.skip('Install routes to test RoutesDispatcher code')
-
-        class Dummy:
-
-            def index(self):
-                return 'I said good day!'
-
-        class City:
-
-            def __init__(self, name):
-                self.name = name
-                self.population = 10000
-
-            @cherrypy.config(**{
-                'tools.response_headers.on': True,
-                'tools.response_headers.headers': [
-                    ('Content-Language', 'en-GB'),
-                ],
-            })
-            def index(self, **kwargs):
-                return 'Welcome to %s, pop. %s' % (self.name, self.population)
-
-            def update(self, **kwargs):
-                self.population = kwargs['pop']
-                return 'OK'
-
-        d = cherrypy.dispatch.RoutesDispatcher()
-        d.connect(action='index', name='hounslow', route='/hounslow',
-                  controller=City('Hounslow'))
-        d.connect(
-            name='surbiton', route='/surbiton', controller=City('Surbiton'),
-            action='index', conditions=dict(method=['GET']))
-        d.mapper.connect('/surbiton', controller='surbiton',
-                         action='update', conditions=dict(method=['POST']))
-        d.connect('main', ':action', controller=Dummy())
-
-        conf = {'/': {'request.dispatch': d}}
-        cherrypy.tree.mount(root=None, config=conf)
-
-    def test_Routes_Dispatch(self):
-        """Check that routes package based URI dispatching works correctly."""
-        self.getPage('/hounslow')
-        self.assertStatus('200 OK')
-        self.assertBody('Welcome to Hounslow, pop. 10000')
-
-        self.getPage('/foo')
-        self.assertStatus('404 Not Found')
-
-        self.getPage('/surbiton')
-        self.assertStatus('200 OK')
-        self.assertBody('Welcome to Surbiton, pop. 10000')
-
-        self.getPage('/surbiton', method='POST', body='pop=1327')
-        self.assertStatus('200 OK')
-        self.assertBody('OK')
-        self.getPage('/surbiton')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Language', 'en-GB')
-        self.assertBody('Welcome to Surbiton, pop. 1327')
diff --git a/libraries/cherrypy/test/test_session.py b/libraries/cherrypy/test/test_session.py
deleted file mode 100644
index 0083c97c..00000000
--- a/libraries/cherrypy/test/test_session.py
+++ /dev/null
@@ -1,512 +0,0 @@
-import os
-import threading
-import time
-import socket
-import importlib
-
-from six.moves.http_client import HTTPConnection
-
-import pytest
-from path import Path
-
-import cherrypy
-from cherrypy._cpcompat import (
-    json_decode,
-    HTTPSConnection,
-)
-from cherrypy.lib import sessions
-from cherrypy.lib import reprconf
-from cherrypy.lib.httputil import response_codes
-from cherrypy.test import helper
-
-localDir = os.path.dirname(__file__)
-
-
-def http_methods_allowed(methods=['GET', 'HEAD']):
-    method = cherrypy.request.method.upper()
-    if method not in methods:
-        cherrypy.response.headers['Allow'] = ', '.join(methods)
-        raise cherrypy.HTTPError(405)
-
-
-cherrypy.tools.allow = cherrypy.Tool('on_start_resource', http_methods_allowed)
-
-
-def setup_server():
-
-    @cherrypy.config(**{
-        'tools.sessions.on': True,
-        'tools.sessions.storage_class': sessions.RamSession,
-        'tools.sessions.storage_path': localDir,
-        'tools.sessions.timeout': (1.0 / 60),
-        'tools.sessions.clean_freq': (1.0 / 60),
-    })
-    class Root:
-
-        @cherrypy.expose
-        def clear(self):
-            cherrypy.session.cache.clear()
-
-        @cherrypy.expose
-        def data(self):
-            cherrypy.session['aha'] = 'foo'
-            return repr(cherrypy.session._data)
-
-        @cherrypy.expose
-        def testGen(self):
-            counter = cherrypy.session.get('counter', 0) + 1
-            cherrypy.session['counter'] = counter
-            yield str(counter)
-
-        @cherrypy.expose
-        def testStr(self):
-            counter = cherrypy.session.get('counter', 0) + 1
-            cherrypy.session['counter'] = counter
-            return str(counter)
-
-        @cherrypy.expose
-        @cherrypy.config(**{'tools.sessions.on': False})
-        def set_session_cls(self, new_cls_name):
-            new_cls = reprconf.attributes(new_cls_name)
-            cfg = {'tools.sessions.storage_class': new_cls}
-            self.__class__._cp_config.update(cfg)
-            if hasattr(cherrypy, 'session'):
-                del cherrypy.session
-            if new_cls.clean_thread:
-                new_cls.clean_thread.stop()
-                new_cls.clean_thread.unsubscribe()
-                del new_cls.clean_thread
-
-        @cherrypy.expose
-        def index(self):
-            sess = cherrypy.session
-            c = sess.get('counter', 0) + 1
-            time.sleep(0.01)
-            sess['counter'] = c
-            return str(c)
-
-        @cherrypy.expose
-        def keyin(self, key):
-            return str(key in cherrypy.session)
-
-        @cherrypy.expose
-        def delete(self):
-            cherrypy.session.delete()
-            sessions.expire()
-            return 'done'
-
-        @cherrypy.expose
-        def delkey(self, key):
-            del cherrypy.session[key]
-            return 'OK'
-
-        @cherrypy.expose
-        def redir_target(self):
-            return self._cp_config['tools.sessions.storage_class'].__name__
-
-        @cherrypy.expose
-        def iredir(self):
-            raise cherrypy.InternalRedirect('/redir_target')
-
-        @cherrypy.expose
-        @cherrypy.config(**{
-            'tools.allow.on': True,
-            'tools.allow.methods': ['GET'],
-        })
-        def restricted(self):
-            return cherrypy.request.method
-
-        @cherrypy.expose
-        def regen(self):
-            cherrypy.tools.sessions.regenerate()
-            return 'logged in'
-
-        @cherrypy.expose
-        def length(self):
-            return str(len(cherrypy.session))
-
-        @cherrypy.expose
-        @cherrypy.config(**{
-            'tools.sessions.path': '/session_cookie',
-            'tools.sessions.name': 'temp',
-            'tools.sessions.persistent': False,
-        })
-        def session_cookie(self):
-            # Must load() to start the clean thread.
-            cherrypy.session.load()
-            return cherrypy.session.id
-
-    cherrypy.tree.mount(Root())
-
-
-class SessionTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def tearDown(self):
-        # Clean up sessions.
-        for fname in os.listdir(localDir):
-            if fname.startswith(sessions.FileSession.SESSION_PREFIX):
-                path = Path(localDir) / fname
-                path.remove_p()
-
-    @pytest.mark.xfail(reason='#1534')
-    def test_0_Session(self):
-        self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
-        self.getPage('/clear')
-
-        # Test that a normal request gets the same id in the cookies.
-        # Note: this wouldn't work if /data didn't load the session.
-        self.getPage('/data')
-        self.assertBody("{'aha': 'foo'}")
-        c = self.cookies[0]
-        self.getPage('/data', self.cookies)
-        self.assertEqual(self.cookies[0], c)
-
-        self.getPage('/testStr')
-        self.assertBody('1')
-        cookie_parts = dict([p.strip().split('=')
-                             for p in self.cookies[0][1].split(';')])
-        # Assert there is an 'expires' param
-        self.assertEqual(set(cookie_parts.keys()),
-                         set(['session_id', 'expires', 'Path']))
-        self.getPage('/testGen', self.cookies)
-        self.assertBody('2')
-        self.getPage('/testStr', self.cookies)
-        self.assertBody('3')
-        self.getPage('/data', self.cookies)
-        self.assertDictEqual(json_decode(self.body),
-                             {'counter': 3, 'aha': 'foo'})
-        self.getPage('/length', self.cookies)
-        self.assertBody('2')
-        self.getPage('/delkey?key=counter', self.cookies)
-        self.assertStatus(200)
-
-        self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
-        self.getPage('/testStr')
-        self.assertBody('1')
-        self.getPage('/testGen', self.cookies)
-        self.assertBody('2')
-        self.getPage('/testStr', self.cookies)
-        self.assertBody('3')
-        self.getPage('/delkey?key=counter', self.cookies)
-        self.assertStatus(200)
-
-        # Wait for the session.timeout (1 second)
-        time.sleep(2)
-        self.getPage('/')
-        self.assertBody('1')
-        self.getPage('/length', self.cookies)
-        self.assertBody('1')
-
-        # Test session __contains__
-        self.getPage('/keyin?key=counter', self.cookies)
-        self.assertBody('True')
-        cookieset1 = self.cookies
-
-        # Make a new session and test __len__ again
-        self.getPage('/')
-        self.getPage('/length', self.cookies)
-        self.assertBody('2')
-
-        # Test session delete
-        self.getPage('/delete', self.cookies)
-        self.assertBody('done')
-        self.getPage('/delete', cookieset1)
-        self.assertBody('done')
-
-        def f():
-            return [
-                x
-                for x in os.listdir(localDir)
-                if x.startswith('session-')
-            ]
-        self.assertEqual(f(), [])
-
-        # Wait for the cleanup thread to delete remaining session files
-        self.getPage('/')
-        self.assertNotEqual(f(), [])
-        time.sleep(2)
-        self.assertEqual(f(), [])
-
-    def test_1_Ram_Concurrency(self):
-        self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
-        self._test_Concurrency()
-
-    @pytest.mark.xfail(reason='#1306')
-    def test_2_File_Concurrency(self):
-        self.getPage('/set_session_cls/cherrypy.lib.sessions.FileSession')
-        self._test_Concurrency()
-
-    def _test_Concurrency(self):
-        client_thread_count = 5
-        request_count = 30
-
-        # Get initial cookie
-        self.getPage('/')
-        self.assertBody('1')
-        cookies = self.cookies
-
-        data_dict = {}
-        errors = []
-
-        def request(index):
-            if self.scheme == 'https':
-                c = HTTPSConnection('%s:%s' % (self.interface(), self.PORT))
-            else:
-                c = HTTPConnection('%s:%s' % (self.interface(), self.PORT))
-            for i in range(request_count):
-                c.putrequest('GET', '/')
-                for k, v in cookies:
-                    c.putheader(k, v)
-                c.endheaders()
-                response = c.getresponse()
-                body = response.read()
-                if response.status != 200 or not body.isdigit():
-                    errors.append((response.status, body))
-                else:
-                    data_dict[index] = max(data_dict[index], int(body))
-                # Uncomment the following line to prove threads overlap.
-                # sys.stdout.write("%d " % index)
-
-        # Start <request_count> requests from each of
-        # <client_thread_count> concurrent clients
-        ts = []
-        for c in range(client_thread_count):
-            data_dict[c] = 0
-            t = threading.Thread(target=request, args=(c,))
-            ts.append(t)
-            t.start()
-
-        for t in ts:
-            t.join()
-
-        hitcount = max(data_dict.values())
-        expected = 1 + (client_thread_count * request_count)
-
-        for e in errors:
-            print(e)
-        self.assertEqual(hitcount, expected)
-
-    def test_3_Redirect(self):
-        # Start a new session
-        self.getPage('/testStr')
-        self.getPage('/iredir', self.cookies)
-        self.assertBody('FileSession')
-
-    def test_4_File_deletion(self):
-        # Start a new session
-        self.getPage('/testStr')
-        # Delete the session file manually and retry.
-        id = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
-        path = os.path.join(localDir, 'session-' + id)
-        os.unlink(path)
-        self.getPage('/testStr', self.cookies)
-
-    def test_5_Error_paths(self):
-        self.getPage('/unknown/page')
-        self.assertErrorPage(404, "The path '/unknown/page' was not found.")
-
-        # Note: this path is *not* the same as above. The above
-        # takes a normal route through the session code; this one
-        # skips the session code's before_handler and only calls
-        # before_finalize (save) and on_end (close). So the session
-        # code has to survive calling save/close without init.
-        self.getPage('/restricted', self.cookies, method='POST')
-        self.assertErrorPage(405, response_codes[405][1])
-
-    def test_6_regenerate(self):
-        self.getPage('/testStr')
-        # grab the cookie ID
-        id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
-        self.getPage('/regen')
-        self.assertBody('logged in')
-        id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
-        self.assertNotEqual(id1, id2)
-
-        self.getPage('/testStr')
-        # grab the cookie ID
-        id1 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
-        self.getPage('/testStr',
-                     headers=[
-                         ('Cookie',
-                          'session_id=maliciousid; '
-                          'expires=Sat, 27 Oct 2017 04:18:28 GMT; Path=/;')])
-        id2 = self.cookies[0][1].split(';', 1)[0].split('=', 1)[1]
-        self.assertNotEqual(id1, id2)
-        self.assertNotEqual(id2, 'maliciousid')
-
-    def test_7_session_cookies(self):
-        self.getPage('/set_session_cls/cherrypy.lib.sessions.RamSession')
-        self.getPage('/clear')
-        self.getPage('/session_cookie')
-        # grab the cookie ID
-        cookie_parts = dict([p.strip().split('=')
-                            for p in self.cookies[0][1].split(';')])
-        # Assert there is no 'expires' param
-        self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
-        id1 = cookie_parts['temp']
-        self.assertEqual(list(sessions.RamSession.cache), [id1])
-
-        # Send another request in the same "browser session".
-        self.getPage('/session_cookie', self.cookies)
-        cookie_parts = dict([p.strip().split('=')
-                            for p in self.cookies[0][1].split(';')])
-        # Assert there is no 'expires' param
-        self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
-        self.assertBody(id1)
-        self.assertEqual(list(sessions.RamSession.cache), [id1])
-
-        # Simulate a browser close by just not sending the cookies
-        self.getPage('/session_cookie')
-        # grab the cookie ID
-        cookie_parts = dict([p.strip().split('=')
-                            for p in self.cookies[0][1].split(';')])
-        # Assert there is no 'expires' param
-        self.assertEqual(set(cookie_parts.keys()), set(['temp', 'Path']))
-        # Assert a new id has been generated...
-        id2 = cookie_parts['temp']
-        self.assertNotEqual(id1, id2)
-        self.assertEqual(set(sessions.RamSession.cache.keys()),
-                         set([id1, id2]))
-
-        # Wait for the session.timeout on both sessions
-        time.sleep(2.5)
-        cache = list(sessions.RamSession.cache)
-        if cache:
-            if cache == [id2]:
-                self.fail('The second session did not time out.')
-            else:
-                self.fail('Unknown session id in cache: %r', cache)
-
-    def test_8_Ram_Cleanup(self):
-        def lock():
-            s1 = sessions.RamSession()
-            s1.acquire_lock()
-            time.sleep(1)
-            s1.release_lock()
-
-        t = threading.Thread(target=lock)
-        t.start()
-        start = time.time()
-        while not sessions.RamSession.locks and time.time() - start < 5:
-            time.sleep(0.01)
-        assert len(sessions.RamSession.locks) == 1, 'Lock not acquired'
-        s2 = sessions.RamSession()
-        s2.clean_up()
-        msg = 'Clean up should not remove active lock'
-        assert len(sessions.RamSession.locks) == 1, msg
-        t.join()
-
-
-try:
-    importlib.import_module('memcache')
-
-    host, port = '127.0.0.1', 11211
-    for res in socket.getaddrinfo(host, port, socket.AF_UNSPEC,
-                                  socket.SOCK_STREAM):
-        af, socktype, proto, canonname, sa = res
-        s = None
-        try:
-            s = socket.socket(af, socktype, proto)
-            # See http://groups.google.com/group/cherrypy-users/
-            #        browse_frm/thread/bbfe5eb39c904fe0
-            s.settimeout(1.0)
-            s.connect((host, port))
-            s.close()
-        except socket.error:
-            if s:
-                s.close()
-            raise
-        break
-except (ImportError, socket.error):
-    class MemcachedSessionTest(helper.CPWebCase):
-        setup_server = staticmethod(setup_server)
-
-        def test(self):
-            return self.skip('memcached not reachable ')
-else:
-    class MemcachedSessionTest(helper.CPWebCase):
-        setup_server = staticmethod(setup_server)
-
-        def test_0_Session(self):
-            self.getPage('/set_session_cls/cherrypy.Sessions.MemcachedSession')
-
-            self.getPage('/testStr')
-            self.assertBody('1')
-            self.getPage('/testGen', self.cookies)
-            self.assertBody('2')
-            self.getPage('/testStr', self.cookies)
-            self.assertBody('3')
-            self.getPage('/length', self.cookies)
-            self.assertErrorPage(500)
-            self.assertInBody('NotImplementedError')
-            self.getPage('/delkey?key=counter', self.cookies)
-            self.assertStatus(200)
-
-            # Wait for the session.timeout (1 second)
-            time.sleep(1.25)
-            self.getPage('/')
-            self.assertBody('1')
-
-            # Test session __contains__
-            self.getPage('/keyin?key=counter', self.cookies)
-            self.assertBody('True')
-
-            # Test session delete
-            self.getPage('/delete', self.cookies)
-            self.assertBody('done')
-
-        def test_1_Concurrency(self):
-            client_thread_count = 5
-            request_count = 30
-
-            # Get initial cookie
-            self.getPage('/')
-            self.assertBody('1')
-            cookies = self.cookies
-
-            data_dict = {}
-
-            def request(index):
-                for i in range(request_count):
-                    self.getPage('/', cookies)
-                    # Uncomment the following line to prove threads overlap.
-                    # sys.stdout.write("%d " % index)
-                if not self.body.isdigit():
-                    self.fail(self.body)
-                data_dict[index] = int(self.body)
-
-            # Start <request_count> concurrent requests from
-            # each of <client_thread_count> clients
-            ts = []
-            for c in range(client_thread_count):
-                data_dict[c] = 0
-                t = threading.Thread(target=request, args=(c,))
-                ts.append(t)
-                t.start()
-
-            for t in ts:
-                t.join()
-
-            hitcount = max(data_dict.values())
-            expected = 1 + (client_thread_count * request_count)
-            self.assertEqual(hitcount, expected)
-
-        def test_3_Redirect(self):
-            # Start a new session
-            self.getPage('/testStr')
-            self.getPage('/iredir', self.cookies)
-            self.assertBody('memcached')
-
-        def test_5_Error_paths(self):
-            self.getPage('/unknown/page')
-            self.assertErrorPage(
-                404, "The path '/unknown/page' was not found.")
-
-            # Note: this path is *not* the same as above. The above
-            # takes a normal route through the session code; this one
-            # skips the session code's before_handler and only calls
-            # before_finalize (save) and on_end (close). So the session
-            # code has to survive calling save/close without init.
-            self.getPage('/restricted', self.cookies, method='POST')
-            self.assertErrorPage(405, response_codes[405][1])
diff --git a/libraries/cherrypy/test/test_sessionauthenticate.py b/libraries/cherrypy/test/test_sessionauthenticate.py
deleted file mode 100644
index 63053fcb..00000000
--- a/libraries/cherrypy/test/test_sessionauthenticate.py
+++ /dev/null
@@ -1,61 +0,0 @@
-import cherrypy
-from cherrypy.test import helper
-
-
-class SessionAuthenticateTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        def check(username, password):
-            # Dummy check_username_and_password function
-            if username != 'test' or password != 'password':
-                return 'Wrong login/password'
-
-        def augment_params():
-            # A simple tool to add some things to request.params
-            # This is to check to make sure that session_auth can handle
-            # request params (ticket #780)
-            cherrypy.request.params['test'] = 'test'
-
-        cherrypy.tools.augment_params = cherrypy.Tool(
-            'before_handler', augment_params, None, priority=30)
-
-        class Test:
-
-            _cp_config = {
-                'tools.sessions.on': True,
-                'tools.session_auth.on': True,
-                'tools.session_auth.check_username_and_password': check,
-                'tools.augment_params.on': True,
-            }
-
-            @cherrypy.expose
-            def index(self, **kwargs):
-                return 'Hi %s, you are logged in' % cherrypy.request.login
-
-        cherrypy.tree.mount(Test())
-
-    def testSessionAuthenticate(self):
-        # request a page and check for login form
-        self.getPage('/')
-        self.assertInBody('<form method="post" action="do_login">')
-
-        # setup credentials
-        login_body = 'username=test&password=password&from_page=/'
-
-        # attempt a login
-        self.getPage('/do_login', method='POST', body=login_body)
-        self.assertStatus((302, 303))
-
-        # get the page now that we are logged in
-        self.getPage('/', self.cookies)
-        self.assertBody('Hi test, you are logged in')
-
-        # do a logout
-        self.getPage('/do_logout', self.cookies, method='POST')
-        self.assertStatus((302, 303))
-
-        # verify we are logged out
-        self.getPage('/', self.cookies)
-        self.assertInBody('<form method="post" action="do_login">')
diff --git a/libraries/cherrypy/test/test_states.py b/libraries/cherrypy/test/test_states.py
deleted file mode 100644
index 606ca4f6..00000000
--- a/libraries/cherrypy/test/test_states.py
+++ /dev/null
@@ -1,473 +0,0 @@
-import os
-import signal
-import time
-import unittest
-import warnings
-
-from six.moves.http_client import BadStatusLine
-
-import pytest
-import portend
-
-import cherrypy
-import cherrypy.process.servers
-from cherrypy.test import helper
-
-engine = cherrypy.engine
-thisdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-class Dependency:
-
-    def __init__(self, bus):
-        self.bus = bus
-        self.running = False
-        self.startcount = 0
-        self.gracecount = 0
-        self.threads = {}
-
-    def subscribe(self):
-        self.bus.subscribe('start', self.start)
-        self.bus.subscribe('stop', self.stop)
-        self.bus.subscribe('graceful', self.graceful)
-        self.bus.subscribe('start_thread', self.startthread)
-        self.bus.subscribe('stop_thread', self.stopthread)
-
-    def start(self):
-        self.running = True
-        self.startcount += 1
-
-    def stop(self):
-        self.running = False
-
-    def graceful(self):
-        self.gracecount += 1
-
-    def startthread(self, thread_id):
-        self.threads[thread_id] = None
-
-    def stopthread(self, thread_id):
-        del self.threads[thread_id]
-
-
-db_connection = Dependency(engine)
-
-
-def setup_server():
-    class Root:
-
-        @cherrypy.expose
-        def index(self):
-            return 'Hello World'
-
-        @cherrypy.expose
-        def ctrlc(self):
-            raise KeyboardInterrupt()
-
-        @cherrypy.expose
-        def graceful(self):
-            engine.graceful()
-            return 'app was (gracefully) restarted succesfully'
-
-    cherrypy.tree.mount(Root())
-    cherrypy.config.update({
-        'environment': 'test_suite',
-    })
-
-    db_connection.subscribe()
-
-# ------------ Enough helpers. Time for real live test cases. ------------ #
-
-
-class ServerStateTests(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def setUp(self):
-        cherrypy.server.socket_timeout = 0.1
-        self.do_gc_test = False
-
-    def test_0_NormalStateFlow(self):
-        engine.stop()
-        # Our db_connection should not be running
-        self.assertEqual(db_connection.running, False)
-        self.assertEqual(db_connection.startcount, 1)
-        self.assertEqual(len(db_connection.threads), 0)
-
-        # Test server start
-        engine.start()
-        self.assertEqual(engine.state, engine.states.STARTED)
-
-        host = cherrypy.server.socket_host
-        port = cherrypy.server.socket_port
-        portend.occupied(host, port, timeout=0.1)
-
-        # The db_connection should be running now
-        self.assertEqual(db_connection.running, True)
-        self.assertEqual(db_connection.startcount, 2)
-        self.assertEqual(len(db_connection.threads), 0)
-
-        self.getPage('/')
-        self.assertBody('Hello World')
-        self.assertEqual(len(db_connection.threads), 1)
-
-        # Test engine stop. This will also stop the HTTP server.
-        engine.stop()
-        self.assertEqual(engine.state, engine.states.STOPPED)
-
-        # Verify that our custom stop function was called
-        self.assertEqual(db_connection.running, False)
-        self.assertEqual(len(db_connection.threads), 0)
-
-        # Block the main thread now and verify that exit() works.
-        def exittest():
-            self.getPage('/')
-            self.assertBody('Hello World')
-            engine.exit()
-        cherrypy.server.start()
-        engine.start_with_callback(exittest)
-        engine.block()
-        self.assertEqual(engine.state, engine.states.EXITING)
-
-    def test_1_Restart(self):
-        cherrypy.server.start()
-        engine.start()
-
-        # The db_connection should be running now
-        self.assertEqual(db_connection.running, True)
-        grace = db_connection.gracecount
-
-        self.getPage('/')
-        self.assertBody('Hello World')
-        self.assertEqual(len(db_connection.threads), 1)
-
-        # Test server restart from this thread
-        engine.graceful()
-        self.assertEqual(engine.state, engine.states.STARTED)
-        self.getPage('/')
-        self.assertBody('Hello World')
-        self.assertEqual(db_connection.running, True)
-        self.assertEqual(db_connection.gracecount, grace + 1)
-        self.assertEqual(len(db_connection.threads), 1)
-
-        # Test server restart from inside a page handler
-        self.getPage('/graceful')
-        self.assertEqual(engine.state, engine.states.STARTED)
-        self.assertBody('app was (gracefully) restarted succesfully')
-        self.assertEqual(db_connection.running, True)
-        self.assertEqual(db_connection.gracecount, grace + 2)
-        # Since we are requesting synchronously, is only one thread used?
-        # Note that the "/graceful" request has been flushed.
-        self.assertEqual(len(db_connection.threads), 0)
-
-        engine.stop()
-        self.assertEqual(engine.state, engine.states.STOPPED)
-        self.assertEqual(db_connection.running, False)
-        self.assertEqual(len(db_connection.threads), 0)
-
-    def test_2_KeyboardInterrupt(self):
-        # Raise a keyboard interrupt in the HTTP server's main thread.
-        # We must start the server in this, the main thread
-        engine.start()
-        cherrypy.server.start()
-
-        self.persistent = True
-        try:
-            # Make the first request and assert there's no "Connection: close".
-            self.getPage('/')
-            self.assertStatus('200 OK')
-            self.assertBody('Hello World')
-            self.assertNoHeader('Connection')
-
-            cherrypy.server.httpserver.interrupt = KeyboardInterrupt
-            engine.block()
-
-            self.assertEqual(db_connection.running, False)
-            self.assertEqual(len(db_connection.threads), 0)
-            self.assertEqual(engine.state, engine.states.EXITING)
-        finally:
-            self.persistent = False
-
-        # Raise a keyboard interrupt in a page handler; on multithreaded
-        # servers, this should occur in one of the worker threads.
-        # This should raise a BadStatusLine error, since the worker
-        # thread will just die without writing a response.
-        engine.start()
-        cherrypy.server.start()
-        # From python3.5 a new exception is retuned when the connection
-        # ends abruptly:
-        #   http.client.RemoteDisconnected
-        # RemoteDisconnected is a subclass of:
-        #   (ConnectionResetError, http.client.BadStatusLine)
-        # and ConnectionResetError is an indirect subclass of:
-        #    OSError
-        # From python 3.3 an up socket.error is an alias to OSError
-        # following PEP-3151, therefore http.client.RemoteDisconnected
-        # is considered a socket.error.
-        #
-        # raise_subcls specifies the classes that are not going
-        # to be considered as a socket.error for the retries.
-        # Given that RemoteDisconnected is part BadStatusLine
-        # we can use the same call for all py3 versions without
-        # sideffects. python < 3.5 will raise directly BadStatusLine
-        # which is not a subclass for socket.error/OSError.
-        try:
-            self.getPage('/ctrlc', raise_subcls=BadStatusLine)
-        except BadStatusLine:
-            pass
-        else:
-            print(self.body)
-            self.fail('AssertionError: BadStatusLine not raised')
-
-        engine.block()
-        self.assertEqual(db_connection.running, False)
-        self.assertEqual(len(db_connection.threads), 0)
-
-    @pytest.mark.xfail(
-        'sys.platform == "Darwin" '
-        'and sys.version_info > (3, 7) '
-        'and os.environ["TRAVIS"]',
-        reason='https://github.com/cherrypy/cherrypy/issues/1693',
-    )
-    def test_4_Autoreload(self):
-        # If test_3 has not been executed, the server won't be stopped,
-        # so we'll have to do it.
-        if engine.state != engine.states.EXITING:
-            engine.exit()
-
-        # Start the demo script in a new process
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
-        p.write_conf(extra='test_case_name: "test_4_Autoreload"')
-        p.start(imports='cherrypy.test._test_states_demo')
-        try:
-            self.getPage('/start')
-            start = float(self.body)
-
-            # Give the autoreloader time to cache the file time.
-            time.sleep(2)
-
-            # Touch the file
-            os.utime(os.path.join(thisdir, '_test_states_demo.py'), None)
-
-            # Give the autoreloader time to re-exec the process
-            time.sleep(2)
-            host = cherrypy.server.socket_host
-            port = cherrypy.server.socket_port
-            portend.occupied(host, port, timeout=5)
-
-            self.getPage('/start')
-            if not (float(self.body) > start):
-                raise AssertionError('start time %s not greater than %s' %
-                                     (float(self.body), start))
-        finally:
-            # Shut down the spawned process
-            self.getPage('/exit')
-        p.join()
-
-    def test_5_Start_Error(self):
-        # If test_3 has not been executed, the server won't be stopped,
-        # so we'll have to do it.
-        if engine.state != engine.states.EXITING:
-            engine.exit()
-
-        # If a process errors during start, it should stop the engine
-        # and exit with a non-zero exit code.
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
-                             wait=True)
-        p.write_conf(
-            extra="""starterror: True
-test_case_name: "test_5_Start_Error"
-"""
-        )
-        p.start(imports='cherrypy.test._test_states_demo')
-        if p.exit_code == 0:
-            self.fail('Process failed to return nonzero exit code.')
-
-
-class PluginTests(helper.CPWebCase):
-
-    def test_daemonize(self):
-        if os.name not in ['posix']:
-            return self.skip('skipped (not on posix) ')
-        self.HOST = '127.0.0.1'
-        self.PORT = 8081
-        # Spawn the process and wait, when this returns, the original process
-        # is finished.  If it daemonized properly, we should still be able
-        # to access pages.
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
-                             wait=True, daemonize=True,
-                             socket_host='127.0.0.1',
-                             socket_port=8081)
-        p.write_conf(
-            extra='test_case_name: "test_daemonize"')
-        p.start(imports='cherrypy.test._test_states_demo')
-        try:
-            # Just get the pid of the daemonization process.
-            self.getPage('/pid')
-            self.assertStatus(200)
-            page_pid = int(self.body)
-            self.assertEqual(page_pid, p.get_pid())
-        finally:
-            # Shut down the spawned process
-            self.getPage('/exit')
-        p.join()
-
-        # Wait until here to test the exit code because we want to ensure
-        # that we wait for the daemon to finish running before we fail.
-        if p.exit_code != 0:
-            self.fail('Daemonized parent process failed to exit cleanly.')
-
-
-class SignalHandlingTests(helper.CPWebCase):
-
-    def test_SIGHUP_tty(self):
-        # When not daemonized, SIGHUP should shut down the server.
-        try:
-            from signal import SIGHUP
-        except ImportError:
-            return self.skip('skipped (no SIGHUP) ')
-
-        # Spawn the process.
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
-        p.write_conf(
-            extra='test_case_name: "test_SIGHUP_tty"')
-        p.start(imports='cherrypy.test._test_states_demo')
-        # Send a SIGHUP
-        os.kill(p.get_pid(), SIGHUP)
-        # This might hang if things aren't working right, but meh.
-        p.join()
-
-    def test_SIGHUP_daemonized(self):
-        # When daemonized, SIGHUP should restart the server.
-        try:
-            from signal import SIGHUP
-        except ImportError:
-            return self.skip('skipped (no SIGHUP) ')
-
-        if os.name not in ['posix']:
-            return self.skip('skipped (not on posix) ')
-
-        # Spawn the process and wait, when this returns, the original process
-        # is finished.  If it daemonized properly, we should still be able
-        # to access pages.
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
-                             wait=True, daemonize=True)
-        p.write_conf(
-            extra='test_case_name: "test_SIGHUP_daemonized"')
-        p.start(imports='cherrypy.test._test_states_demo')
-
-        pid = p.get_pid()
-        try:
-            # Send a SIGHUP
-            os.kill(pid, SIGHUP)
-            # Give the server some time to restart
-            time.sleep(2)
-            self.getPage('/pid')
-            self.assertStatus(200)
-            new_pid = int(self.body)
-            self.assertNotEqual(new_pid, pid)
-        finally:
-            # Shut down the spawned process
-            self.getPage('/exit')
-        p.join()
-
-    def _require_signal_and_kill(self, signal_name):
-        if not hasattr(signal, signal_name):
-            self.skip('skipped (no %(signal_name)s)' % vars())
-
-        if not hasattr(os, 'kill'):
-            self.skip('skipped (no os.kill)')
-
-    def test_SIGTERM(self):
-        'SIGTERM should shut down the server whether daemonized or not.'
-        self._require_signal_and_kill('SIGTERM')
-
-        # Spawn a normal, undaemonized process.
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
-        p.write_conf(
-            extra='test_case_name: "test_SIGTERM"')
-        p.start(imports='cherrypy.test._test_states_demo')
-        # Send a SIGTERM
-        os.kill(p.get_pid(), signal.SIGTERM)
-        # This might hang if things aren't working right, but meh.
-        p.join()
-
-        if os.name in ['posix']:
-            # Spawn a daemonized process and test again.
-            p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'),
-                                 wait=True, daemonize=True)
-            p.write_conf(
-                extra='test_case_name: "test_SIGTERM_2"')
-            p.start(imports='cherrypy.test._test_states_demo')
-            # Send a SIGTERM
-            os.kill(p.get_pid(), signal.SIGTERM)
-            # This might hang if things aren't working right, but meh.
-            p.join()
-
-    def test_signal_handler_unsubscribe(self):
-        self._require_signal_and_kill('SIGTERM')
-
-        # Although Windows has `os.kill` and SIGTERM is defined, the
-        #  platform does not implement signals and sending SIGTERM
-        #  will result in a forced termination of the process.
-        #  Therefore, this test is not suitable for Windows.
-        if os.name == 'nt':
-            self.skip('SIGTERM not available')
-
-        # Spawn a normal, undaemonized process.
-        p = helper.CPProcess(ssl=(self.scheme.lower() == 'https'))
-        p.write_conf(
-            extra="""unsubsig: True
-test_case_name: "test_signal_handler_unsubscribe"
-""")
-        p.start(imports='cherrypy.test._test_states_demo')
-        # Ask the process to quit
-        os.kill(p.get_pid(), signal.SIGTERM)
-        # This might hang if things aren't working right, but meh.
-        p.join()
-
-        # Assert the old handler ran.
-        log_lines = list(open(p.error_log, 'rb'))
-        assert any(
-            line.endswith(b'I am an old SIGTERM handler.\n')
-            for line in log_lines
-        )
-
-
-class WaitTests(unittest.TestCase):
-
-    def test_safe_wait_INADDR_ANY(self):
-        """
-        Wait on INADDR_ANY should not raise IOError
-
-        In cases where the loopback interface does not exist, CherryPy cannot
-        effectively determine if a port binding to INADDR_ANY was effected.
-        In this situation, CherryPy should assume that it failed to detect
-        the binding (not that the binding failed) and only warn that it could
-        not verify it.
-        """
-        # At such a time that CherryPy can reliably determine one or more
-        #  viable IP addresses of the host, this test may be removed.
-
-        # Simulate the behavior we observe when no loopback interface is
-        #  present by: finding a port that's not occupied, then wait on it.
-
-        free_port = portend.find_available_local_port()
-
-        servers = cherrypy.process.servers
-
-        inaddr_any = '0.0.0.0'
-
-        # Wait on the free port that's unbound
-        with warnings.catch_warnings(record=True) as w:
-            with servers._safe_wait(inaddr_any, free_port):
-                portend.occupied(inaddr_any, free_port, timeout=1)
-            self.assertEqual(len(w), 1)
-            self.assertTrue(isinstance(w[0], warnings.WarningMessage))
-            self.assertTrue(
-                'Unable to verify that the server is bound on ' in str(w[0]))
-
-        # The wait should still raise an IO error if INADDR_ANY was
-        #  not supplied.
-        with pytest.raises(IOError):
-            with servers._safe_wait('127.0.0.1', free_port):
-                portend.occupied('127.0.0.1', free_port, timeout=1)
diff --git a/libraries/cherrypy/test/test_static.py b/libraries/cherrypy/test/test_static.py
deleted file mode 100644
index 5dc5a144..00000000
--- a/libraries/cherrypy/test/test_static.py
+++ /dev/null
@@ -1,434 +0,0 @@
-# -*- coding: utf-8 -*-
-import contextlib
-import io
-import os
-import sys
-import platform
-import tempfile
-
-from six import text_type as str
-from six.moves import urllib
-from six.moves.http_client import HTTPConnection
-
-import pytest
-import py.path
-
-import cherrypy
-from cherrypy.lib import static
-from cherrypy._cpcompat import HTTPSConnection, ntou, tonative
-from cherrypy.test import helper
-
-
-@pytest.fixture
-def unicode_filesystem(tmpdir):
-    filename = tmpdir / ntou('☃', 'utf-8')
-    tmpl = 'File system encoding ({encoding}) cannot support unicode filenames'
-    msg = tmpl.format(encoding=sys.getfilesystemencoding())
-    try:
-        io.open(str(filename), 'w').close()
-    except UnicodeEncodeError:
-        pytest.skip(msg)
-
-
-def ensure_unicode_filesystem():
-    """
-    TODO: replace with simply pytest fixtures once webtest.TestCase
-    no longer implies unittest.
-    """
-    tmpdir = py.path.local(tempfile.mkdtemp())
-    try:
-        unicode_filesystem(tmpdir)
-    finally:
-        tmpdir.remove()
-
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-has_space_filepath = os.path.join(curdir, 'static', 'has space.html')
-bigfile_filepath = os.path.join(curdir, 'static', 'bigfile.log')
-
-# The file size needs to be big enough such that half the size of it
-# won't be socket-buffered (or server-buffered) all in one go. See
-# test_file_stream.
-MB = 2 ** 20
-BIGFILE_SIZE = 32 * MB
-
-
-class StaticTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        if not os.path.exists(has_space_filepath):
-            with open(has_space_filepath, 'wb') as f:
-                f.write(b'Hello, world\r\n')
-        needs_bigfile = (
-            not os.path.exists(bigfile_filepath) or
-            os.path.getsize(bigfile_filepath) != BIGFILE_SIZE
-        )
-        if needs_bigfile:
-            with open(bigfile_filepath, 'wb') as f:
-                f.write(b'x' * BIGFILE_SIZE)
-
-        class Root:
-
-            @cherrypy.expose
-            @cherrypy.config(**{'response.stream': True})
-            def bigfile(self):
-                self.f = static.serve_file(bigfile_filepath)
-                return self.f
-
-            @cherrypy.expose
-            def tell(self):
-                if self.f.input.closed:
-                    return ''
-                return repr(self.f.input.tell()).rstrip('L')
-
-            @cherrypy.expose
-            def fileobj(self):
-                f = open(os.path.join(curdir, 'style.css'), 'rb')
-                return static.serve_fileobj(f, content_type='text/css')
-
-            @cherrypy.expose
-            def bytesio(self):
-                f = io.BytesIO(b'Fee\nfie\nfo\nfum')
-                return static.serve_fileobj(f, content_type='text/plain')
-
-        class Static:
-
-            @cherrypy.expose
-            def index(self):
-                return 'You want the Baron? You can have the Baron!'
-
-            @cherrypy.expose
-            def dynamic(self):
-                return 'This is a DYNAMIC page'
-
-        root = Root()
-        root.static = Static()
-
-        rootconf = {
-            '/static': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.dir': 'static',
-                'tools.staticdir.root': curdir,
-            },
-            '/static-long': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.dir': r'\\?\%s' % curdir,
-            },
-            '/style.css': {
-                'tools.staticfile.on': True,
-                'tools.staticfile.filename': os.path.join(curdir, 'style.css'),
-            },
-            '/docroot': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.root': curdir,
-                'tools.staticdir.dir': 'static',
-                'tools.staticdir.index': 'index.html',
-            },
-            '/error': {
-                'tools.staticdir.on': True,
-                'request.show_tracebacks': True,
-            },
-            '/404test': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.root': curdir,
-                'tools.staticdir.dir': 'static',
-                'error_page.404': error_page_404,
-            }
-        }
-        rootApp = cherrypy.Application(root)
-        rootApp.merge(rootconf)
-
-        test_app_conf = {
-            '/test': {
-                'tools.staticdir.index': 'index.html',
-                'tools.staticdir.on': True,
-                'tools.staticdir.root': curdir,
-                'tools.staticdir.dir': 'static',
-            },
-        }
-        testApp = cherrypy.Application(Static())
-        testApp.merge(test_app_conf)
-
-        vhost = cherrypy._cpwsgi.VirtualHost(rootApp, {'virt.net': testApp})
-        cherrypy.tree.graft(vhost)
-
-    @staticmethod
-    def teardown_server():
-        for f in (has_space_filepath, bigfile_filepath):
-            if os.path.exists(f):
-                try:
-                    os.unlink(f)
-                except Exception:
-                    pass
-
-    def test_static(self):
-        self.getPage('/static/index.html')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html')
-        self.assertBody('Hello, world\r\n')
-
-        # Using a staticdir.root value in a subdir...
-        self.getPage('/docroot/index.html')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html')
-        self.assertBody('Hello, world\r\n')
-
-        # Check a filename with spaces in it
-        self.getPage('/static/has%20space.html')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html')
-        self.assertBody('Hello, world\r\n')
-
-        self.getPage('/style.css')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/css')
-        # Note: The body should be exactly 'Dummy stylesheet\n', but
-        #   unfortunately some tools such as WinZip sometimes turn \n
-        #   into \r\n on Windows when extracting the CherryPy tarball so
-        #   we just check the content
-        self.assertMatchesBody('^Dummy stylesheet')
-
-    @pytest.mark.skipif(platform.system() != 'Windows', reason='Windows only')
-    def test_static_longpath(self):
-        """Test serving of a file in subdir of a Windows long-path
-        staticdir."""
-        self.getPage('/static-long/static/index.html')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html')
-        self.assertBody('Hello, world\r\n')
-
-    def test_fallthrough(self):
-        # Test that NotFound will then try dynamic handlers (see [878]).
-        self.getPage('/static/dynamic')
-        self.assertBody('This is a DYNAMIC page')
-
-        # Check a directory via fall-through to dynamic handler.
-        self.getPage('/static/')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html;charset=utf-8')
-        self.assertBody('You want the Baron? You can have the Baron!')
-
-    def test_index(self):
-        # Check a directory via "staticdir.index".
-        self.getPage('/docroot/')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/html')
-        self.assertBody('Hello, world\r\n')
-        # The same page should be returned even if redirected.
-        self.getPage('/docroot')
-        self.assertStatus(301)
-        self.assertHeader('Location', '%s/docroot/' % self.base())
-        self.assertMatchesBody(
-            "This resource .* <a href=(['\"])%s/docroot/\\1>"
-            '%s/docroot/</a>.'
-            % (self.base(), self.base())
-        )
-
-    def test_config_errors(self):
-        # Check that we get an error if no .file or .dir
-        self.getPage('/error/thing.html')
-        self.assertErrorPage(500)
-        if sys.version_info >= (3, 3):
-            errmsg = (
-                r'TypeError: staticdir\(\) missing 2 '
-                'required positional arguments'
-            )
-        else:
-            errmsg = (
-                r'TypeError: staticdir\(\) takes at least 2 '
-                r'(positional )?arguments \(0 given\)'
-            )
-        self.assertMatchesBody(errmsg.encode('ascii'))
-
-    def test_security(self):
-        # Test up-level security
-        self.getPage('/static/../../test/style.css')
-        self.assertStatus((400, 403))
-
-    def test_modif(self):
-        # Test modified-since on a reasonably-large file
-        self.getPage('/static/dirback.jpg')
-        self.assertStatus('200 OK')
-        lastmod = ''
-        for k, v in self.headers:
-            if k == 'Last-Modified':
-                lastmod = v
-        ims = ('If-Modified-Since', lastmod)
-        self.getPage('/static/dirback.jpg', headers=[ims])
-        self.assertStatus(304)
-        self.assertNoHeader('Content-Type')
-        self.assertNoHeader('Content-Length')
-        self.assertNoHeader('Content-Disposition')
-        self.assertBody('')
-
-    def test_755_vhost(self):
-        self.getPage('/test/', [('Host', 'virt.net')])
-        self.assertStatus(200)
-        self.getPage('/test', [('Host', 'virt.net')])
-        self.assertStatus(301)
-        self.assertHeader('Location', self.scheme + '://virt.net/test/')
-
-    def test_serve_fileobj(self):
-        self.getPage('/fileobj')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/css;charset=utf-8')
-        self.assertMatchesBody('^Dummy stylesheet')
-
-    def test_serve_bytesio(self):
-        self.getPage('/bytesio')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/plain;charset=utf-8')
-        self.assertHeader('Content-Length', 14)
-        self.assertMatchesBody('Fee\nfie\nfo\nfum')
-
-    @pytest.mark.xfail(reason='#1475')
-    def test_file_stream(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Make an initial request
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('GET', '/bigfile', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 200)
-
-        body = b''
-        remaining = BIGFILE_SIZE
-        while remaining > 0:
-            data = response.fp.read(65536)
-            if not data:
-                break
-            body += data
-            remaining -= len(data)
-
-            if self.scheme == 'https':
-                newconn = HTTPSConnection
-            else:
-                newconn = HTTPConnection
-            s, h, b = helper.webtest.openURL(
-                b'/tell', headers=[], host=self.HOST, port=self.PORT,
-                http_conn=newconn)
-            if not b:
-                # The file was closed on the server.
-                tell_position = BIGFILE_SIZE
-            else:
-                tell_position = int(b)
-
-            read_so_far = len(body)
-
-            # It is difficult for us to force the server to only read
-            # the bytes that we ask for - there are going to be buffers
-            # inbetween.
-            #
-            # CherryPy will attempt to write as much data as it can to
-            # the socket, and we don't have a way to determine what that
-            # size will be. So we make the following assumption - by
-            # the time we have read in the entire file on the server,
-            # we will have at least received half of it. If this is not
-            # the case, then this is an indicator that either:
-            #   - machines that are running this test are using buffer
-            #     sizes greater than half of BIGFILE_SIZE; or
-            #   - streaming is broken.
-            #
-            # At the time of writing, we seem to have encountered
-            # buffer sizes bigger than 512K, so we've increased
-            # BIGFILE_SIZE to 4MB and in 2016 to 20MB and then 32MB.
-            # This test is going to keep failing according to the
-            # improvements in hardware and OS buffers.
-            if tell_position >= BIGFILE_SIZE:
-                if read_so_far < (BIGFILE_SIZE / 2):
-                    self.fail(
-                        'The file should have advanced to position %r, but '
-                        'has already advanced to the end of the file. It '
-                        'may not be streamed as intended, or at the wrong '
-                        'chunk size (64k)' % read_so_far)
-            elif tell_position < read_so_far:
-                self.fail(
-                    'The file should have advanced to position %r, but has '
-                    'only advanced to position %r. It may not be streamed '
-                    'as intended, or at the wrong chunk size (64k)' %
-                    (read_so_far, tell_position))
-
-        if body != b'x' * BIGFILE_SIZE:
-            self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
-                      (BIGFILE_SIZE, body[:50], len(body)))
-        conn.close()
-
-    def test_file_stream_deadlock(self):
-        if cherrypy.server.protocol_version != 'HTTP/1.1':
-            return self.skip()
-
-        self.PROTOCOL = 'HTTP/1.1'
-
-        # Make an initial request but abort early.
-        self.persistent = True
-        conn = self.HTTP_CONN
-        conn.putrequest('GET', '/bigfile', skip_host=True)
-        conn.putheader('Host', self.HOST)
-        conn.endheaders()
-        response = conn.response_class(conn.sock, method='GET')
-        response.begin()
-        self.assertEqual(response.status, 200)
-        body = response.fp.read(65536)
-        if body != b'x' * len(body):
-            self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
-                      (65536, body[:50], len(body)))
-        response.close()
-        conn.close()
-
-        # Make a second request, which should fetch the whole file.
-        self.persistent = False
-        self.getPage('/bigfile')
-        if self.body != b'x' * BIGFILE_SIZE:
-            self.fail("Body != 'x' * %d. Got %r instead (%d bytes)." %
-                      (BIGFILE_SIZE, self.body[:50], len(body)))
-
-    def test_error_page_with_serve_file(self):
-        self.getPage('/404test/yunyeen')
-        self.assertStatus(404)
-        self.assertInBody("I couldn't find that thing")
-
-    def test_null_bytes(self):
-        self.getPage('/static/\x00')
-        self.assertStatus('404 Not Found')
-
-    @staticmethod
-    @contextlib.contextmanager
-    def unicode_file():
-        filename = ntou('Слава Україні.html', 'utf-8')
-        filepath = os.path.join(curdir, 'static', filename)
-        with io.open(filepath, 'w', encoding='utf-8') as strm:
-            strm.write(ntou('Героям Слава!', 'utf-8'))
-        try:
-            yield
-        finally:
-            os.remove(filepath)
-
-    py27_on_windows = (
-        platform.system() == 'Windows' and
-        sys.version_info < (3,)
-    )
-    @pytest.mark.xfail(py27_on_windows, reason='#1544')  # noqa: E301
-    def test_unicode(self):
-        ensure_unicode_filesystem()
-        with self.unicode_file():
-            url = ntou('/static/Слава Україні.html', 'utf-8')
-            # quote function requires str
-            url = tonative(url, 'utf-8')
-            url = urllib.parse.quote(url)
-            self.getPage(url)
-
-            expected = ntou('Героям Слава!', 'utf-8')
-            self.assertInBody(expected)
-
-
-def error_page_404(status, message, traceback, version):
-    path = os.path.join(curdir, 'static', '404.html')
-    return static.serve_file(path, content_type='text/html')
diff --git a/libraries/cherrypy/test/test_tools.py b/libraries/cherrypy/test/test_tools.py
deleted file mode 100644
index a73a3898..00000000
--- a/libraries/cherrypy/test/test_tools.py
+++ /dev/null
@@ -1,468 +0,0 @@
-"""Test the various means of instantiating and invoking tools."""
-
-import gzip
-import io
-import sys
-import time
-import types
-import unittest
-import operator
-
-import six
-from six.moves import range, map
-from six.moves.http_client import IncompleteRead
-
-import cherrypy
-from cherrypy import tools
-from cherrypy._cpcompat import ntou
-from cherrypy.test import helper, _test_decorators
-
-
-timeout = 0.2
-europoundUnicode = ntou('\x80\xa3')
-
-
-#                             Client-side code                             #
-
-
-class ToolTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        # Put check_access in a custom toolbox with its own namespace
-        myauthtools = cherrypy._cptools.Toolbox('myauth')
-
-        def check_access(default=False):
-            if not getattr(cherrypy.request, 'userid', default):
-                raise cherrypy.HTTPError(401)
-        myauthtools.check_access = cherrypy.Tool(
-            'before_request_body', check_access)
-
-        def numerify():
-            def number_it(body):
-                for chunk in body:
-                    for k, v in cherrypy.request.numerify_map:
-                        chunk = chunk.replace(k, v)
-                    yield chunk
-            cherrypy.response.body = number_it(cherrypy.response.body)
-
-        class NumTool(cherrypy.Tool):
-
-            def _setup(self):
-                def makemap():
-                    m = self._merged_args().get('map', {})
-                    cherrypy.request.numerify_map = list(six.iteritems(m))
-                cherrypy.request.hooks.attach('on_start_resource', makemap)
-
-                def critical():
-                    cherrypy.request.error_response = cherrypy.HTTPError(
-                        502).set_response
-                critical.failsafe = True
-
-                cherrypy.request.hooks.attach('on_start_resource', critical)
-                cherrypy.request.hooks.attach(self._point, self.callable)
-
-        tools.numerify = NumTool('before_finalize', numerify)
-
-        # It's not mandatory to inherit from cherrypy.Tool.
-        class NadsatTool:
-
-            def __init__(self):
-                self.ended = {}
-                self._name = 'nadsat'
-
-            def nadsat(self):
-                def nadsat_it_up(body):
-                    for chunk in body:
-                        chunk = chunk.replace(b'good', b'horrorshow')
-                        chunk = chunk.replace(b'piece', b'lomtick')
-                        yield chunk
-                cherrypy.response.body = nadsat_it_up(cherrypy.response.body)
-            nadsat.priority = 0
-
-            def cleanup(self):
-                # This runs after the request has been completely written out.
-                cherrypy.response.body = [b'razdrez']
-                id = cherrypy.request.params.get('id')
-                if id:
-                    self.ended[id] = True
-            cleanup.failsafe = True
-
-            def _setup(self):
-                cherrypy.request.hooks.attach('before_finalize', self.nadsat)
-                cherrypy.request.hooks.attach('on_end_request', self.cleanup)
-        tools.nadsat = NadsatTool()
-
-        def pipe_body():
-            cherrypy.request.process_request_body = False
-            clen = int(cherrypy.request.headers['Content-Length'])
-            cherrypy.request.body = cherrypy.request.rfile.read(clen)
-
-        # Assert that we can use a callable object instead of a function.
-        class Rotator(object):
-
-            def __call__(self, scale):
-                r = cherrypy.response
-                r.collapse_body()
-                if six.PY3:
-                    r.body = [bytes([(x + scale) % 256 for x in r.body[0]])]
-                else:
-                    r.body = [chr((ord(x) + scale) % 256) for x in r.body[0]]
-        cherrypy.tools.rotator = cherrypy.Tool('before_finalize', Rotator())
-
-        def stream_handler(next_handler, *args, **kwargs):
-            actual = cherrypy.request.config.get('tools.streamer.arg')
-            assert actual == 'arg value'
-            cherrypy.response.output = o = io.BytesIO()
-            try:
-                next_handler(*args, **kwargs)
-                # Ignore the response and return our accumulated output
-                # instead.
-                return o.getvalue()
-            finally:
-                o.close()
-        cherrypy.tools.streamer = cherrypy._cptools.HandlerWrapperTool(
-            stream_handler)
-
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'Howdy earth!'
-
-            @cherrypy.expose
-            @cherrypy.config(**{
-                'tools.streamer.on': True,
-                'tools.streamer.arg': 'arg value',
-            })
-            def tarfile(self):
-                actual = cherrypy.request.config.get('tools.streamer.arg')
-                assert actual == 'arg value'
-                cherrypy.response.output.write(b'I am ')
-                cherrypy.response.output.write(b'a tarfile')
-
-            @cherrypy.expose
-            def euro(self):
-                hooks = list(cherrypy.request.hooks['before_finalize'])
-                hooks.sort()
-                cbnames = [x.callback.__name__ for x in hooks]
-                assert cbnames == ['gzip'], cbnames
-                priorities = [x.priority for x in hooks]
-                assert priorities == [80], priorities
-                yield ntou('Hello,')
-                yield ntou('world')
-                yield europoundUnicode
-
-            # Bare hooks
-            @cherrypy.expose
-            @cherrypy.config(**{'hooks.before_request_body': pipe_body})
-            def pipe(self):
-                return cherrypy.request.body
-
-            # Multiple decorators; include kwargs just for fun.
-            # Note that rotator must run before gzip.
-            @cherrypy.expose
-            def decorated_euro(self, *vpath):
-                yield ntou('Hello,')
-                yield ntou('world')
-                yield europoundUnicode
-            decorated_euro = tools.gzip(compress_level=6)(decorated_euro)
-            decorated_euro = tools.rotator(scale=3)(decorated_euro)
-
-        root = Root()
-
-        class TestType(type):
-            """Metaclass which automatically exposes all functions in each
-            subclass, and adds an instance of the subclass as an attribute
-            of root.
-            """
-            def __init__(cls, name, bases, dct):
-                type.__init__(cls, name, bases, dct)
-                for value in six.itervalues(dct):
-                    if isinstance(value, types.FunctionType):
-                        cherrypy.expose(value)
-                setattr(root, name.lower(), cls())
-        Test = TestType('Test', (object,), {})
-
-        # METHOD ONE:
-        # Declare Tools in _cp_config
-        @cherrypy.config(**{'tools.nadsat.on': True})
-        class Demo(Test):
-
-            def index(self, id=None):
-                return 'A good piece of cherry pie'
-
-            def ended(self, id):
-                return repr(tools.nadsat.ended[id])
-
-            def err(self, id=None):
-                raise ValueError()
-
-            def errinstream(self, id=None):
-                yield 'nonconfidential'
-                raise ValueError()
-                yield 'confidential'
-
-            # METHOD TWO: decorator using Tool()
-            # We support Python 2.3, but the @-deco syntax would look like
-            # this:
-            # @tools.check_access()
-            def restricted(self):
-                return 'Welcome!'
-            restricted = myauthtools.check_access()(restricted)
-            userid = restricted
-
-            def err_in_onstart(self):
-                return 'success!'
-
-            @cherrypy.config(**{'response.stream': True})
-            def stream(self, id=None):
-                for x in range(100000000):
-                    yield str(x)
-
-        conf = {
-            # METHOD THREE:
-            # Declare Tools in detached config
-            '/demo': {
-                'tools.numerify.on': True,
-                'tools.numerify.map': {b'pie': b'3.14159'},
-            },
-            '/demo/restricted': {
-                'request.show_tracebacks': False,
-            },
-            '/demo/userid': {
-                'request.show_tracebacks': False,
-                'myauth.check_access.default': True,
-            },
-            '/demo/errinstream': {
-                'response.stream': True,
-            },
-            '/demo/err_in_onstart': {
-                # Because this isn't a dict, on_start_resource will error.
-                'tools.numerify.map': 'pie->3.14159'
-            },
-            # Combined tools
-            '/euro': {
-                'tools.gzip.on': True,
-                'tools.encode.on': True,
-            },
-            # Priority specified in config
-            '/decorated_euro/subpath': {
-                'tools.gzip.priority': 10,
-            },
-            # Handler wrappers
-            '/tarfile': {'tools.streamer.on': True}
-        }
-        app = cherrypy.tree.mount(root, config=conf)
-        app.request_class.namespaces['myauth'] = myauthtools
-
-        root.tooldecs = _test_decorators.ToolExamples()
-
-    def testHookErrors(self):
-        self.getPage('/demo/?id=1')
-        # If body is "razdrez", then on_end_request is being called too early.
-        self.assertBody('A horrorshow lomtick of cherry 3.14159')
-        # If this fails, then on_end_request isn't being called at all.
-        time.sleep(0.1)
-        self.getPage('/demo/ended/1')
-        self.assertBody('True')
-
-        valerr = '\n    raise ValueError()\nValueError'
-        self.getPage('/demo/err?id=3')
-        # If body is "razdrez", then on_end_request is being called too early.
-        self.assertErrorPage(502, pattern=valerr)
-        # If this fails, then on_end_request isn't being called at all.
-        time.sleep(0.1)
-        self.getPage('/demo/ended/3')
-        self.assertBody('True')
-
-        # If body is "razdrez", then on_end_request is being called too early.
-        if (cherrypy.server.protocol_version == 'HTTP/1.0' or
-                getattr(cherrypy.server, 'using_apache', False)):
-            self.getPage('/demo/errinstream?id=5')
-            # Because this error is raised after the response body has
-            # started, the status should not change to an error status.
-            self.assertStatus('200 OK')
-            self.assertBody('nonconfidential')
-        else:
-            # Because this error is raised after the response body has
-            # started, and because it's chunked output, an error is raised by
-            # the HTTP client when it encounters incomplete output.
-            self.assertRaises((ValueError, IncompleteRead), self.getPage,
-                              '/demo/errinstream?id=5')
-        # If this fails, then on_end_request isn't being called at all.
-        time.sleep(0.1)
-        self.getPage('/demo/ended/5')
-        self.assertBody('True')
-
-        # Test the "__call__" technique (compile-time decorator).
-        self.getPage('/demo/restricted')
-        self.assertErrorPage(401)
-
-        # Test compile-time decorator with kwargs from config.
-        self.getPage('/demo/userid')
-        self.assertBody('Welcome!')
-
-    def testEndRequestOnDrop(self):
-        old_timeout = None
-        try:
-            httpserver = cherrypy.server.httpserver
-            old_timeout = httpserver.timeout
-        except (AttributeError, IndexError):
-            return self.skip()
-
-        try:
-            httpserver.timeout = timeout
-
-            # Test that on_end_request is called even if the client drops.
-            self.persistent = True
-            try:
-                conn = self.HTTP_CONN
-                conn.putrequest('GET', '/demo/stream?id=9', skip_host=True)
-                conn.putheader('Host', self.HOST)
-                conn.endheaders()
-                # Skip the rest of the request and close the conn. This will
-                # cause the server's active socket to error, which *should*
-                # result in the request being aborted, and request.close being
-                # called all the way up the stack (including WSGI middleware),
-                # eventually calling our on_end_request hook.
-            finally:
-                self.persistent = False
-            time.sleep(timeout * 2)
-            # Test that the on_end_request hook was called.
-            self.getPage('/demo/ended/9')
-            self.assertBody('True')
-        finally:
-            if old_timeout is not None:
-                httpserver.timeout = old_timeout
-
-    def testGuaranteedHooks(self):
-        # The 'critical' on_start_resource hook is 'failsafe' (guaranteed
-        # to run even if there are failures in other on_start methods).
-        # This is NOT true of the other hooks.
-        # Here, we have set up a failure in NumerifyTool.numerify_map,
-        # but our 'critical' hook should run and set the error to 502.
-        self.getPage('/demo/err_in_onstart')
-        self.assertErrorPage(502)
-        tmpl = "AttributeError: 'str' object has no attribute '{attr}'"
-        expected_msg = tmpl.format(attr='items' if six.PY3 else 'iteritems')
-        self.assertInBody(expected_msg)
-
-    def testCombinedTools(self):
-        expectedResult = (ntou('Hello,world') +
-                          europoundUnicode).encode('utf-8')
-        zbuf = io.BytesIO()
-        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=9)
-        zfile.write(expectedResult)
-        zfile.close()
-
-        self.getPage('/euro',
-                     headers=[
-                         ('Accept-Encoding', 'gzip'),
-                         ('Accept-Charset', 'ISO-8859-1,utf-8;q=0.7,*;q=0.7')])
-        self.assertInBody(zbuf.getvalue()[:3])
-
-        zbuf = io.BytesIO()
-        zfile = gzip.GzipFile(mode='wb', fileobj=zbuf, compresslevel=6)
-        zfile.write(expectedResult)
-        zfile.close()
-
-        self.getPage('/decorated_euro', headers=[('Accept-Encoding', 'gzip')])
-        self.assertInBody(zbuf.getvalue()[:3])
-
-        # This returns a different value because gzip's priority was
-        # lowered in conf, allowing the rotator to run after gzip.
-        # Of course, we don't want breakage in production apps,
-        # but it proves the priority was changed.
-        self.getPage('/decorated_euro/subpath',
-                     headers=[('Accept-Encoding', 'gzip')])
-        if six.PY3:
-            self.assertInBody(bytes([(x + 3) % 256 for x in zbuf.getvalue()]))
-        else:
-            self.assertInBody(''.join([chr((ord(x) + 3) % 256)
-                              for x in zbuf.getvalue()]))
-
-    def testBareHooks(self):
-        content = 'bit of a pain in me gulliver'
-        self.getPage('/pipe',
-                     headers=[('Content-Length', str(len(content))),
-                              ('Content-Type', 'text/plain')],
-                     method='POST', body=content)
-        self.assertBody(content)
-
-    def testHandlerWrapperTool(self):
-        self.getPage('/tarfile')
-        self.assertBody('I am a tarfile')
-
-    def testToolWithConfig(self):
-        if not sys.version_info >= (2, 5):
-            return self.skip('skipped (Python 2.5+ only)')
-
-        self.getPage('/tooldecs/blah')
-        self.assertHeader('Content-Type', 'application/data')
-
-    def testWarnToolOn(self):
-        # get
-        try:
-            cherrypy.tools.numerify.on
-        except AttributeError:
-            pass
-        else:
-            raise AssertionError('Tool.on did not error as it should have.')
-
-        # set
-        try:
-            cherrypy.tools.numerify.on = True
-        except AttributeError:
-            pass
-        else:
-            raise AssertionError('Tool.on did not error as it should have.')
-
-    def testDecorator(self):
-        @cherrypy.tools.register('on_start_resource')
-        def example():
-            pass
-        self.assertTrue(isinstance(cherrypy.tools.example, cherrypy.Tool))
-        self.assertEqual(cherrypy.tools.example._point, 'on_start_resource')
-
-        @cherrypy.tools.register(  # noqa: F811
-            'before_finalize', name='renamed', priority=60,
-        )
-        def example():
-            pass
-        self.assertTrue(isinstance(cherrypy.tools.renamed, cherrypy.Tool))
-        self.assertEqual(cherrypy.tools.renamed._point, 'before_finalize')
-        self.assertEqual(cherrypy.tools.renamed._name, 'renamed')
-        self.assertEqual(cherrypy.tools.renamed._priority, 60)
-
-
-class SessionAuthTest(unittest.TestCase):
-
-    def test_login_screen_returns_bytes(self):
-        """
-        login_screen must return bytes even if unicode parameters are passed.
-        Issue 1132 revealed that login_screen would return unicode if the
-        username and password were unicode.
-        """
-        sa = cherrypy.lib.cptools.SessionAuth()
-        res = sa.login_screen(None, username=six.text_type('nobody'),
-                              password=six.text_type('anypass'))
-        self.assertTrue(isinstance(res, bytes))
-
-
-class TestHooks:
-    def test_priorities(self):
-        """
-        Hooks should sort by priority order.
-        """
-        Hook = cherrypy._cprequest.Hook
-        hooks = [
-            Hook(None, priority=48),
-            Hook(None),
-            Hook(None, priority=49),
-        ]
-        hooks.sort()
-        by_priority = operator.attrgetter('priority')
-        priorities = list(map(by_priority, hooks))
-        assert priorities == [48, 49, 50]
diff --git a/libraries/cherrypy/test/test_tutorials.py b/libraries/cherrypy/test/test_tutorials.py
deleted file mode 100644
index efa35b99..00000000
--- a/libraries/cherrypy/test/test_tutorials.py
+++ /dev/null
@@ -1,210 +0,0 @@
-import sys
-import imp
-import types
-import importlib
-
-import six
-
-import cherrypy
-from cherrypy.test import helper
-
-
-class TutorialTest(helper.CPWebCase):
-
-    @classmethod
-    def setup_server(cls):
-        """
-        Mount something so the engine starts.
-        """
-        class Dummy:
-            pass
-        cherrypy.tree.mount(Dummy())
-
-    @staticmethod
-    def load_module(name):
-        """
-        Import or reload tutorial module as needed.
-        """
-        target = 'cherrypy.tutorial.' + name
-        if target in sys.modules:
-            module = imp.reload(sys.modules[target])
-        else:
-            module = importlib.import_module(target)
-        return module
-
-    @classmethod
-    def setup_tutorial(cls, name, root_name, config={}):
-        cherrypy.config.reset()
-        module = cls.load_module(name)
-        root = getattr(module, root_name)
-        conf = getattr(module, 'tutconf')
-        class_types = type,
-        if six.PY2:
-            class_types += types.ClassType,
-        if isinstance(root, class_types):
-            root = root()
-        cherrypy.tree.mount(root, config=conf)
-        cherrypy.config.update(config)
-
-    def test01HelloWorld(self):
-        self.setup_tutorial('tut01_helloworld', 'HelloWorld')
-        self.getPage('/')
-        self.assertBody('Hello world!')
-
-    def test02ExposeMethods(self):
-        self.setup_tutorial('tut02_expose_methods', 'HelloWorld')
-        self.getPage('/show_msg')
-        self.assertBody('Hello world!')
-
-    def test03GetAndPost(self):
-        self.setup_tutorial('tut03_get_and_post', 'WelcomePage')
-
-        # Try different GET queries
-        self.getPage('/greetUser?name=Bob')
-        self.assertBody("Hey Bob, what's up?")
-
-        self.getPage('/greetUser')
-        self.assertBody('Please enter your name <a href="./">here</a>.')
-
-        self.getPage('/greetUser?name=')
-        self.assertBody('No, really, enter your name <a href="./">here</a>.')
-
-        # Try the same with POST
-        self.getPage('/greetUser', method='POST', body='name=Bob')
-        self.assertBody("Hey Bob, what's up?")
-
-        self.getPage('/greetUser', method='POST', body='name=')
-        self.assertBody('No, really, enter your name <a href="./">here</a>.')
-
-    def test04ComplexSite(self):
-        self.setup_tutorial('tut04_complex_site', 'root')
-
-        msg = '''
-            <p>Here are some extra useful links:</p>
-
-            <ul>
-                <li><a href="http://del.icio.us">del.icio.us</a></li>
-                <li><a href="http://www.cherrypy.org">CherryPy</a></li>
-            </ul>
-
-            <p>[<a href="../">Return to links page</a>]</p>'''
-        self.getPage('/links/extra/')
-        self.assertBody(msg)
-
-    def test05DerivedObjects(self):
-        self.setup_tutorial('tut05_derived_objects', 'HomePage')
-        msg = '''
-            <html>
-            <head>
-                <title>Another Page</title>
-            <head>
-            <body>
-            <h2>Another Page</h2>
-
-            <p>
-            And this is the amazing second page!
-            </p>
-
-            </body>
-            </html>
-        '''
-        # the tutorial has some annoying spaces in otherwise blank lines
-        msg = msg.replace('</h2>\n\n', '</h2>\n        \n')
-        msg = msg.replace('</p>\n\n', '</p>\n        \n')
-        self.getPage('/another/')
-        self.assertBody(msg)
-
-    def test06DefaultMethod(self):
-        self.setup_tutorial('tut06_default_method', 'UsersPage')
-        self.getPage('/hendrik')
-        self.assertBody('Hendrik Mans, CherryPy co-developer & crazy German '
-                        '(<a href="./">back</a>)')
-
-    def test07Sessions(self):
-        self.setup_tutorial('tut07_sessions', 'HitCounter')
-
-        self.getPage('/')
-        self.assertBody(
-            "\n            During your current session, you've viewed this"
-            '\n            page 1 times! Your life is a patio of fun!'
-            '\n        ')
-
-        self.getPage('/', self.cookies)
-        self.assertBody(
-            "\n            During your current session, you've viewed this"
-            '\n            page 2 times! Your life is a patio of fun!'
-            '\n        ')
-
-    def test08GeneratorsAndYield(self):
-        self.setup_tutorial('tut08_generators_and_yield', 'GeneratorDemo')
-        self.getPage('/')
-        self.assertBody('<html><body><h2>Generators rule!</h2>'
-                        '<h3>List of users:</h3>'
-                        'Remi<br/>Carlos<br/>Hendrik<br/>Lorenzo Lamas<br/>'
-                        '</body></html>')
-
-    def test09Files(self):
-        self.setup_tutorial('tut09_files', 'FileDemo')
-
-        # Test upload
-        filesize = 5
-        h = [('Content-type', 'multipart/form-data; boundary=x'),
-             ('Content-Length', str(105 + filesize))]
-        b = ('--x\n'
-             'Content-Disposition: form-data; name="myFile"; '
-             'filename="hello.txt"\r\n'
-             'Content-Type: text/plain\r\n'
-             '\r\n')
-        b += 'a' * filesize + '\n' + '--x--\n'
-        self.getPage('/upload', h, 'POST', b)
-        self.assertBody('''<html>
-        <body>
-            myFile length: %d<br />
-            myFile filename: hello.txt<br />
-            myFile mime-type: text/plain
-        </body>
-        </html>''' % filesize)
-
-        # Test download
-        self.getPage('/download')
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'application/x-download')
-        self.assertHeader('Content-Disposition',
-                          # Make sure the filename is quoted.
-                          'attachment; filename="pdf_file.pdf"')
-        self.assertEqual(len(self.body), 85698)
-
-    def test10HTTPErrors(self):
-        self.setup_tutorial('tut10_http_errors', 'HTTPErrorDemo')
-
-        @cherrypy.expose
-        def traceback_setting():
-            return repr(cherrypy.request.show_tracebacks)
-        cherrypy.tree.mount(traceback_setting, '/traceback_setting')
-
-        self.getPage('/')
-        self.assertInBody("""<a href="toggleTracebacks">""")
-        self.assertInBody("""<a href="/doesNotExist">""")
-        self.assertInBody("""<a href="/error?code=403">""")
-        self.assertInBody("""<a href="/error?code=500">""")
-        self.assertInBody("""<a href="/messageArg">""")
-
-        self.getPage('/traceback_setting')
-        setting = self.body
-        self.getPage('/toggleTracebacks')
-        self.assertStatus((302, 303))
-        self.getPage('/traceback_setting')
-        self.assertBody(str(not eval(setting)))
-
-        self.getPage('/error?code=500')
-        self.assertStatus(500)
-        self.assertInBody('The server encountered an unexpected condition '
-                          'which prevented it from fulfilling the request.')
-
-        self.getPage('/error?code=403')
-        self.assertStatus(403)
-        self.assertInBody("<h2>You can't do that!</h2>")
-
-        self.getPage('/messageArg')
-        self.assertStatus(500)
-        self.assertInBody("If you construct an HTTPError with a 'message'")
diff --git a/libraries/cherrypy/test/test_virtualhost.py b/libraries/cherrypy/test/test_virtualhost.py
deleted file mode 100644
index de88f927..00000000
--- a/libraries/cherrypy/test/test_virtualhost.py
+++ /dev/null
@@ -1,113 +0,0 @@
-import os
-
-import cherrypy
-from cherrypy.test import helper
-
-curdir = os.path.join(os.getcwd(), os.path.dirname(__file__))
-
-
-class VirtualHostTest(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return 'Hello, world'
-
-            @cherrypy.expose
-            def dom4(self):
-                return 'Under construction'
-
-            @cherrypy.expose
-            def method(self, value):
-                return 'You sent %s' % value
-
-        class VHost:
-
-            def __init__(self, sitename):
-                self.sitename = sitename
-
-            @cherrypy.expose
-            def index(self):
-                return 'Welcome to %s' % self.sitename
-
-            @cherrypy.expose
-            def vmethod(self, value):
-                return 'You sent %s' % value
-
-            @cherrypy.expose
-            def url(self):
-                return cherrypy.url('nextpage')
-
-            # Test static as a handler (section must NOT include vhost prefix)
-            static = cherrypy.tools.staticdir.handler(
-                section='/static', dir=curdir)
-
-        root = Root()
-        root.mydom2 = VHost('Domain 2')
-        root.mydom3 = VHost('Domain 3')
-        hostmap = {'www.mydom2.com': '/mydom2',
-                   'www.mydom3.com': '/mydom3',
-                   'www.mydom4.com': '/dom4',
-                   }
-        cherrypy.tree.mount(root, config={
-            '/': {
-                'request.dispatch': cherrypy.dispatch.VirtualHost(**hostmap)
-            },
-            # Test static in config (section must include vhost prefix)
-            '/mydom2/static2': {
-                'tools.staticdir.on': True,
-                'tools.staticdir.root': curdir,
-                'tools.staticdir.dir': 'static',
-                'tools.staticdir.index': 'index.html',
-            },
-        })
-
-    def testVirtualHost(self):
-        self.getPage('/', [('Host', 'www.mydom1.com')])
-        self.assertBody('Hello, world')
-        self.getPage('/mydom2/', [('Host', 'www.mydom1.com')])
-        self.assertBody('Welcome to Domain 2')
-
-        self.getPage('/', [('Host', 'www.mydom2.com')])
-        self.assertBody('Welcome to Domain 2')
-        self.getPage('/', [('Host', 'www.mydom3.com')])
-        self.assertBody('Welcome to Domain 3')
-        self.getPage('/', [('Host', 'www.mydom4.com')])
-        self.assertBody('Under construction')
-
-        # Test GET, POST, and positional params
-        self.getPage('/method?value=root')
-        self.assertBody('You sent root')
-        self.getPage('/vmethod?value=dom2+GET', [('Host', 'www.mydom2.com')])
-        self.assertBody('You sent dom2 GET')
-        self.getPage('/vmethod', [('Host', 'www.mydom3.com')], method='POST',
-                     body='value=dom3+POST')
-        self.assertBody('You sent dom3 POST')
-        self.getPage('/vmethod/pos', [('Host', 'www.mydom3.com')])
-        self.assertBody('You sent pos')
-
-        # Test that cherrypy.url uses the browser url, not the virtual url
-        self.getPage('/url', [('Host', 'www.mydom2.com')])
-        self.assertBody('%s://www.mydom2.com/nextpage' % self.scheme)
-
-    def test_VHost_plus_Static(self):
-        # Test static as a handler
-        self.getPage('/static/style.css', [('Host', 'www.mydom2.com')])
-        self.assertStatus('200 OK')
-        self.assertHeader('Content-Type', 'text/css;charset=utf-8')
-
-        # Test static in config
-        self.getPage('/static2/dirback.jpg', [('Host', 'www.mydom2.com')])
-        self.assertStatus('200 OK')
-        self.assertHeaderIn('Content-Type', ['image/jpeg', 'image/pjpeg'])
-
-        # Test static config with "index" arg
-        self.getPage('/static2/', [('Host', 'www.mydom2.com')])
-        self.assertStatus('200 OK')
-        self.assertBody('Hello, world\r\n')
-        # Since tools.trailing_slash is on by default, this should redirect
-        self.getPage('/static2', [('Host', 'www.mydom2.com')])
-        self.assertStatus(301)
diff --git a/libraries/cherrypy/test/test_wsgi_ns.py b/libraries/cherrypy/test/test_wsgi_ns.py
deleted file mode 100644
index 3545724c..00000000
--- a/libraries/cherrypy/test/test_wsgi_ns.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import cherrypy
-from cherrypy.test import helper
-
-
-class WSGI_Namespace_Test(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        class WSGIResponse(object):
-
-            def __init__(self, appresults):
-                self.appresults = appresults
-                self.iter = iter(appresults)
-
-            def __iter__(self):
-                return self
-
-            def next(self):
-                return self.iter.next()
-
-            def __next__(self):
-                return next(self.iter)
-
-            def close(self):
-                if hasattr(self.appresults, 'close'):
-                    self.appresults.close()
-
-        class ChangeCase(object):
-
-            def __init__(self, app, to=None):
-                self.app = app
-                self.to = to
-
-            def __call__(self, environ, start_response):
-                res = self.app(environ, start_response)
-
-                class CaseResults(WSGIResponse):
-
-                    def next(this):
-                        return getattr(this.iter.next(), self.to)()
-
-                    def __next__(this):
-                        return getattr(next(this.iter), self.to)()
-                return CaseResults(res)
-
-        class Replacer(object):
-
-            def __init__(self, app, map={}):
-                self.app = app
-                self.map = map
-
-            def __call__(self, environ, start_response):
-                res = self.app(environ, start_response)
-
-                class ReplaceResults(WSGIResponse):
-
-                    def next(this):
-                        line = this.iter.next()
-                        for k, v in self.map.iteritems():
-                            line = line.replace(k, v)
-                        return line
-
-                    def __next__(this):
-                        line = next(this.iter)
-                        for k, v in self.map.items():
-                            line = line.replace(k, v)
-                        return line
-                return ReplaceResults(res)
-
-        class Root(object):
-
-            @cherrypy.expose
-            def index(self):
-                return 'HellO WoRlD!'
-
-        root_conf = {'wsgi.pipeline': [('replace', Replacer)],
-                     'wsgi.replace.map': {b'L': b'X',
-                                          b'l': b'r'},
-                     }
-
-        app = cherrypy.Application(Root())
-        app.wsgiapp.pipeline.append(('changecase', ChangeCase))
-        app.wsgiapp.config['changecase'] = {'to': 'upper'}
-        cherrypy.tree.mount(app, config={'/': root_conf})
-
-    def test_pipeline(self):
-        if not cherrypy.server.httpserver:
-            return self.skip()
-
-        self.getPage('/')
-        # If body is "HEXXO WORXD!", the middleware was applied out of order.
-        self.assertBody('HERRO WORRD!')
diff --git a/libraries/cherrypy/test/test_wsgi_unix_socket.py b/libraries/cherrypy/test/test_wsgi_unix_socket.py
deleted file mode 100644
index 8f1cc00b..00000000
--- a/libraries/cherrypy/test/test_wsgi_unix_socket.py
+++ /dev/null
@@ -1,93 +0,0 @@
-import os
-import socket
-import atexit
-import tempfile
-
-from six.moves.http_client import HTTPConnection
-
-import pytest
-
-import cherrypy
-from cherrypy.test import helper
-
-
-def usocket_path():
-    fd, path = tempfile.mkstemp('cp_test.sock')
-    os.close(fd)
-    os.remove(path)
-    return path
-
-
-USOCKET_PATH = usocket_path()
-
-
-class USocketHTTPConnection(HTTPConnection):
-    """
-    HTTPConnection over a unix socket.
-    """
-
-    def __init__(self, path):
-        HTTPConnection.__init__(self, 'localhost')
-        self.path = path
-
-    def __call__(self, *args, **kwargs):
-        """
-        Catch-all method just to present itself as a constructor for the
-        HTTPConnection.
-        """
-        return self
-
-    def connect(self):
-        """
-        Override the connect method and assign a unix socket as a transport.
-        """
-        sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
-        sock.connect(self.path)
-        self.sock = sock
-        atexit.register(lambda: os.remove(self.path))
-
-
-@pytest.mark.skipif("sys.platform == 'win32'")
-class WSGI_UnixSocket_Test(helper.CPWebCase):
-    """
-    Test basic behavior on a cherrypy wsgi server listening
-    on a unix socket.
-
-    It exercises the config option `server.socket_file`.
-    """
-    HTTP_CONN = USocketHTTPConnection(USOCKET_PATH)
-
-    @staticmethod
-    def setup_server():
-        class Root(object):
-
-            @cherrypy.expose
-            def index(self):
-                return 'Test OK'
-
-            @cherrypy.expose
-            def error(self):
-                raise Exception('Invalid page')
-
-        config = {
-            'server.socket_file': USOCKET_PATH
-        }
-        cherrypy.config.update(config)
-        cherrypy.tree.mount(Root())
-
-    def tearDown(self):
-        cherrypy.config.update({'server.socket_file': None})
-
-    def test_simple_request(self):
-        self.getPage('/')
-        self.assertStatus('200 OK')
-        self.assertInBody('Test OK')
-
-    def test_not_found(self):
-        self.getPage('/invalid_path')
-        self.assertStatus('404 Not Found')
-
-    def test_internal_error(self):
-        self.getPage('/error')
-        self.assertStatus('500 Internal Server Error')
-        self.assertInBody('Invalid page')
diff --git a/libraries/cherrypy/test/test_wsgi_vhost.py b/libraries/cherrypy/test/test_wsgi_vhost.py
deleted file mode 100644
index 2b6e5ba9..00000000
--- a/libraries/cherrypy/test/test_wsgi_vhost.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import cherrypy
-from cherrypy.test import helper
-
-
-class WSGI_VirtualHost_Test(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        class ClassOfRoot(object):
-
-            def __init__(self, name):
-                self.name = name
-
-            @cherrypy.expose
-            def index(self):
-                return 'Welcome to the %s website!' % self.name
-
-        default = cherrypy.Application(None)
-
-        domains = {}
-        for year in range(1997, 2008):
-            app = cherrypy.Application(ClassOfRoot('Class of %s' % year))
-            domains['www.classof%s.example' % year] = app
-
-        cherrypy.tree.graft(cherrypy._cpwsgi.VirtualHost(default, domains))
-
-    def test_welcome(self):
-        if not cherrypy.server.using_wsgi:
-            return self.skip('skipped (not using WSGI)... ')
-
-        for year in range(1997, 2008):
-            self.getPage(
-                '/', headers=[('Host', 'www.classof%s.example' % year)])
-            self.assertBody('Welcome to the Class of %s website!' % year)
diff --git a/libraries/cherrypy/test/test_wsgiapps.py b/libraries/cherrypy/test/test_wsgiapps.py
deleted file mode 100644
index 1b3bf28f..00000000
--- a/libraries/cherrypy/test/test_wsgiapps.py
+++ /dev/null
@@ -1,120 +0,0 @@
-import sys
-
-import cherrypy
-from cherrypy._cpcompat import ntob
-from cherrypy.test import helper
-
-
-class WSGIGraftTests(helper.CPWebCase):
-
-    @staticmethod
-    def setup_server():
-
-        def test_app(environ, start_response):
-            status = '200 OK'
-            response_headers = [('Content-type', 'text/plain')]
-            start_response(status, response_headers)
-            output = ['Hello, world!\n',
-                      'This is a wsgi app running within CherryPy!\n\n']
-            keys = list(environ.keys())
-            keys.sort()
-            for k in keys:
-                output.append('%s: %s\n' % (k, environ[k]))
-            return [ntob(x, 'utf-8') for x in output]
-
-        def test_empty_string_app(environ, start_response):
-            status = '200 OK'
-            response_headers = [('Content-type', 'text/plain')]
-            start_response(status, response_headers)
-            return [
-                b'Hello', b'', b' ', b'', b'world',
-            ]
-
-        class WSGIResponse(object):
-
-            def __init__(self, appresults):
-                self.appresults = appresults
-                self.iter = iter(appresults)
-
-            def __iter__(self):
-                return self
-
-            if sys.version_info >= (3, 0):
-                def __next__(self):
-                    return next(self.iter)
-            else:
-                def next(self):
-                    return self.iter.next()
-
-            def close(self):
-                if hasattr(self.appresults, 'close'):
-                    self.appresults.close()
-
-        class ReversingMiddleware(object):
-
-            def __init__(self, app):
-                self.app = app
-
-            def __call__(self, environ, start_response):
-                results = app(environ, start_response)
-
-                class Reverser(WSGIResponse):
-
-                    if sys.version_info >= (3, 0):
-                        def __next__(this):
-                            line = list(next(this.iter))
-                            line.reverse()
-                            return bytes(line)
-                    else:
-                        def next(this):
-                            line = list(this.iter.next())
-                            line.reverse()
-                            return ''.join(line)
-
-                return Reverser(results)
-
-        class Root:
-
-            @cherrypy.expose
-            def index(self):
-                return ntob("I'm a regular CherryPy page handler!")
-
-        cherrypy.tree.mount(Root())
-
-        cherrypy.tree.graft(test_app, '/hosted/app1')
-        cherrypy.tree.graft(test_empty_string_app, '/hosted/app3')
-
-        # Set script_name explicitly to None to signal CP that it should
-        # be pulled from the WSGI environ each time.
-        app = cherrypy.Application(Root(), script_name=None)
-        cherrypy.tree.graft(ReversingMiddleware(app), '/hosted/app2')
-
-    wsgi_output = '''Hello, world!
-This is a wsgi app running within CherryPy!'''
-
-    def test_01_standard_app(self):
-        self.getPage('/')
-        self.assertBody("I'm a regular CherryPy page handler!")
-
-    def test_04_pure_wsgi(self):
-        if not cherrypy.server.using_wsgi:
-            return self.skip('skipped (not using WSGI)... ')
-        self.getPage('/hosted/app1')
-        self.assertHeader('Content-Type', 'text/plain')
-        self.assertInBody(self.wsgi_output)
-
-    def test_05_wrapped_cp_app(self):
-        if not cherrypy.server.using_wsgi:
-            return self.skip('skipped (not using WSGI)... ')
-        self.getPage('/hosted/app2/')
-        body = list("I'm a regular CherryPy page handler!")
-        body.reverse()
-        body = ''.join(body)
-        self.assertInBody(body)
-
-    def test_06_empty_string_app(self):
-        if not cherrypy.server.using_wsgi:
-            return self.skip('skipped (not using WSGI)... ')
-        self.getPage('/hosted/app3')
-        self.assertHeader('Content-Type', 'text/plain')
-        self.assertInBody('Hello world')
diff --git a/libraries/cherrypy/test/test_xmlrpc.py b/libraries/cherrypy/test/test_xmlrpc.py
deleted file mode 100644
index ad93b821..00000000
--- a/libraries/cherrypy/test/test_xmlrpc.py
+++ /dev/null
@@ -1,183 +0,0 @@
-import sys
-
-import six
-
-from six.moves.xmlrpc_client import (
-    DateTime, Fault,
-    ProtocolError, ServerProxy, SafeTransport
-)
-
-import cherrypy
-from cherrypy import _cptools
-from cherrypy.test import helper
-
-if six.PY3:
-    HTTPSTransport = SafeTransport
-
-    # Python 3.0's SafeTransport still mistakenly checks for socket.ssl
-    import socket
-    if not hasattr(socket, 'ssl'):
-        socket.ssl = True
-else:
-    class HTTPSTransport(SafeTransport):
-
-        """Subclass of SafeTransport to fix sock.recv errors (by using file).
-        """
-
-        def request(self, host, handler, request_body, verbose=0):
-            # issue XML-RPC request
-            h = self.make_connection(host)
-            if verbose:
-                h.set_debuglevel(1)
-
-            self.send_request(h, handler, request_body)
-            self.send_host(h, host)
-            self.send_user_agent(h)
-            self.send_content(h, request_body)
-
-            errcode, errmsg, headers = h.getreply()
-            if errcode != 200:
-                raise ProtocolError(host + handler, errcode, errmsg, headers)
-
-            self.verbose = verbose
-
-            # Here's where we differ from the superclass. It says:
-            # try:
-            #     sock = h._conn.sock
-            # except AttributeError:
-            #     sock = None
-            # return self._parse_response(h.getfile(), sock)
-
-            return self.parse_response(h.getfile())
-
-
-def setup_server():
-
-    class Root:
-
-        @cherrypy.expose
-        def index(self):
-            return "I'm a standard index!"
-
-    class XmlRpc(_cptools.XMLRPCController):
-
-        @cherrypy.expose
-        def foo(self):
-            return 'Hello world!'
-
-        @cherrypy.expose
-        def return_single_item_list(self):
-            return [42]
-
-        @cherrypy.expose
-        def return_string(self):
-            return 'here is a string'
-
-        @cherrypy.expose
-        def return_tuple(self):
-            return ('here', 'is', 1, 'tuple')
-
-        @cherrypy.expose
-        def return_dict(self):
-            return dict(a=1, b=2, c=3)
-
-        @cherrypy.expose
-        def return_composite(self):
-            return dict(a=1, z=26), 'hi', ['welcome', 'friend']
-
-        @cherrypy.expose
-        def return_int(self):
-            return 42
-
-        @cherrypy.expose
-        def return_float(self):
-            return 3.14
-
-        @cherrypy.expose
-        def return_datetime(self):
-            return DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1))
-
-        @cherrypy.expose
-        def return_boolean(self):
-            return True
-
-        @cherrypy.expose
-        def test_argument_passing(self, num):
-            return num * 2
-
-        @cherrypy.expose
-        def test_returning_Fault(self):
-            return Fault(1, 'custom Fault response')
-
-    root = Root()
-    root.xmlrpc = XmlRpc()
-    cherrypy.tree.mount(root, config={'/': {
-        'request.dispatch': cherrypy.dispatch.XMLRPCDispatcher(),
-        'tools.xmlrpc.allow_none': 0,
-    }})
-
-
-class XmlRpcTest(helper.CPWebCase):
-    setup_server = staticmethod(setup_server)
-
-    def testXmlRpc(self):
-
-        scheme = self.scheme
-        if scheme == 'https':
-            url = 'https://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
-            proxy = ServerProxy(url, transport=HTTPSTransport())
-        else:
-            url = 'http://%s:%s/xmlrpc/' % (self.interface(), self.PORT)
-            proxy = ServerProxy(url)
-
-        # begin the tests ...
-        self.getPage('/xmlrpc/foo')
-        self.assertBody('Hello world!')
-
-        self.assertEqual(proxy.return_single_item_list(), [42])
-        self.assertNotEqual(proxy.return_single_item_list(), 'one bazillion')
-        self.assertEqual(proxy.return_string(), 'here is a string')
-        self.assertEqual(proxy.return_tuple(),
-                         list(('here', 'is', 1, 'tuple')))
-        self.assertEqual(proxy.return_dict(), {'a': 1, 'c': 3, 'b': 2})
-        self.assertEqual(proxy.return_composite(),
-                         [{'a': 1, 'z': 26}, 'hi', ['welcome', 'friend']])
-        self.assertEqual(proxy.return_int(), 42)
-        self.assertEqual(proxy.return_float(), 3.14)
-        self.assertEqual(proxy.return_datetime(),
-                         DateTime((2003, 10, 7, 8, 1, 0, 1, 280, -1)))
-        self.assertEqual(proxy.return_boolean(), True)
-        self.assertEqual(proxy.test_argument_passing(22), 22 * 2)
-
-        # Test an error in the page handler (should raise an xmlrpclib.Fault)
-        try:
-            proxy.test_argument_passing({})
-        except Exception:
-            x = sys.exc_info()[1]
-            self.assertEqual(x.__class__, Fault)
-            self.assertEqual(x.faultString, ('unsupported operand type(s) '
-                                             "for *: 'dict' and 'int'"))
-        else:
-            self.fail('Expected xmlrpclib.Fault')
-
-        # https://github.com/cherrypy/cherrypy/issues/533
-        # if a method is not found, an xmlrpclib.Fault should be raised
-        try:
-            proxy.non_method()
-        except Exception:
-            x = sys.exc_info()[1]
-            self.assertEqual(x.__class__, Fault)
-            self.assertEqual(x.faultString,
-                             'method "non_method" is not supported')
-        else:
-            self.fail('Expected xmlrpclib.Fault')
-
-        # Test returning a Fault from the page handler.
-        try:
-            proxy.test_returning_Fault()
-        except Exception:
-            x = sys.exc_info()[1]
-            self.assertEqual(x.__class__, Fault)
-            self.assertEqual(x.faultString, ('custom Fault response'))
-        else:
-            self.fail('Expected xmlrpclib.Fault')
diff --git a/libraries/cherrypy/test/webtest.py b/libraries/cherrypy/test/webtest.py
deleted file mode 100644
index 9fb6ce62..00000000
--- a/libraries/cherrypy/test/webtest.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# for compatibility, expose cheroot webtest here
-import warnings
-
-from cheroot.test.webtest import (  # noqa
-    interface,
-    WebCase, cleanHeaders, shb, openURL,
-    ServerError, server_error,
-)
-
-
-warnings.warn('Use cheroot.test.webtest', DeprecationWarning)
diff --git a/libraries/cherrypy/tutorial/README.rst b/libraries/cherrypy/tutorial/README.rst
deleted file mode 100644
index c47e7d32..00000000
--- a/libraries/cherrypy/tutorial/README.rst
+++ /dev/null
@@ -1,16 +0,0 @@
-CherryPy Tutorials
-------------------
-
-This is a series of tutorials explaining how to develop dynamic web
-applications using CherryPy. A couple of notes:
-
-
-- Each of these tutorials builds on the ones before it. If you're
-  new to CherryPy, we recommend you start with 01_helloworld.py and
-  work your way upwards. :)
-
-- In most of these tutorials, you will notice that all output is done
-  by returning normal Python strings, often using simple Python
-  variable substitution. In most real-world applications, you will
-  probably want to use a separate template package (like Cheetah,
-  CherryTemplate or XML/XSL).
diff --git a/libraries/cherrypy/tutorial/__init__.py b/libraries/cherrypy/tutorial/__init__.py
deleted file mode 100644
index 08c142c5..00000000
--- a/libraries/cherrypy/tutorial/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-
-# This is used in test_config to test unrepr of "from A import B"
-thing2 = object()
diff --git a/libraries/cherrypy/tutorial/custom_error.html b/libraries/cherrypy/tutorial/custom_error.html
deleted file mode 100644
index d0f30c8a..00000000
--- a/libraries/cherrypy/tutorial/custom_error.html
+++ /dev/null
@@ -1,14 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN"
-  "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html>
-<head>
-    <title>403 Unauthorized</title>
-</head>
-    <body>
-        <h2>You can't do that!</h2>
-        <p>%(message)s</p>
-        <p>This is a custom error page that is read from a file.<p>
-        <pre>%(traceback)s</pre>
-    </body>
-</html>
diff --git a/libraries/cherrypy/tutorial/pdf_file.pdf b/libraries/cherrypy/tutorial/pdf_file.pdf
deleted file mode 100644
index 38b4f15eabdd65d4a674cb32034361245aa7b97e..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001

literal 85698
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

diff --git a/libraries/cherrypy/tutorial/tut01_helloworld.py b/libraries/cherrypy/tutorial/tut01_helloworld.py
deleted file mode 100644
index e86793c8..00000000
--- a/libraries/cherrypy/tutorial/tut01_helloworld.py
+++ /dev/null
@@ -1,34 +0,0 @@
-"""
-Tutorial - Hello World
-
-The most basic (working) CherryPy application possible.
-"""
-
-import os.path
-
-# Import CherryPy global namespace
-import cherrypy
-
-
-class HelloWorld:
-
-    """ Sample request handler class. """
-
-    # Expose the index method through the web. CherryPy will never
-    # publish methods that don't have the exposed attribute set to True.
-    @cherrypy.expose
-    def index(self):
-        # CherryPy will call this method for the root URI ("/") and send
-        # its return value to the client. Because this is tutorial
-        # lesson number 01, we'll just send something really simple.
-        # How about...
-        return 'Hello world!'
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(HelloWorld(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut02_expose_methods.py b/libraries/cherrypy/tutorial/tut02_expose_methods.py
deleted file mode 100644
index 8afbf7d8..00000000
--- a/libraries/cherrypy/tutorial/tut02_expose_methods.py
+++ /dev/null
@@ -1,32 +0,0 @@
-"""
-Tutorial - Multiple methods
-
-This tutorial shows you how to link to other methods of your request
-handler.
-"""
-
-import os.path
-
-import cherrypy
-
-
-class HelloWorld:
-
-    @cherrypy.expose
-    def index(self):
-        # Let's link to another method here.
-        return 'We have an <a href="show_msg">important message</a> for you!'
-
-    @cherrypy.expose
-    def show_msg(self):
-        # Here's the important message!
-        return 'Hello world!'
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(HelloWorld(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut03_get_and_post.py b/libraries/cherrypy/tutorial/tut03_get_and_post.py
deleted file mode 100644
index 0b3d4613..00000000
--- a/libraries/cherrypy/tutorial/tut03_get_and_post.py
+++ /dev/null
@@ -1,51 +0,0 @@
-"""
-Tutorial - Passing variables
-
-This tutorial shows you how to pass GET/POST variables to methods.
-"""
-
-import os.path
-
-import cherrypy
-
-
-class WelcomePage:
-
-    @cherrypy.expose
-    def index(self):
-        # Ask for the user's name.
-        return '''
-            <form action="greetUser" method="GET">
-            What is your name?
-            <input type="text" name="name" />
-            <input type="submit" />
-            </form>'''
-
-    @cherrypy.expose
-    def greetUser(self, name=None):
-        # CherryPy passes all GET and POST variables as method parameters.
-        # It doesn't make a difference where the variables come from, how
-        # large their contents are, and so on.
-        #
-        # You can define default parameter values as usual. In this
-        # example, the "name" parameter defaults to None so we can check
-        # if a name was actually specified.
-
-        if name:
-            # Greet the user!
-            return "Hey %s, what's up?" % name
-        else:
-            if name is None:
-                # No name was specified
-                return 'Please enter your name <a href="./">here</a>.'
-            else:
-                return 'No, really, enter your name <a href="./">here</a>.'
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(WelcomePage(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut04_complex_site.py b/libraries/cherrypy/tutorial/tut04_complex_site.py
deleted file mode 100644
index 3caa1775..00000000
--- a/libraries/cherrypy/tutorial/tut04_complex_site.py
+++ /dev/null
@@ -1,103 +0,0 @@
-"""
-Tutorial - Multiple objects
-
-This tutorial shows you how to create a site structure through multiple
-possibly nested request handler objects.
-"""
-
-import os.path
-
-import cherrypy
-
-
-class HomePage:
-
-    @cherrypy.expose
-    def index(self):
-        return '''
-            <p>Hi, this is the home page! Check out the other
-            fun stuff on this site:</p>
-
-            <ul>
-                <li><a href="/joke/">A silly joke</a></li>
-                <li><a href="/links/">Useful links</a></li>
-            </ul>'''
-
-
-class JokePage:
-
-    @cherrypy.expose
-    def index(self):
-        return '''
-            <p>"In Python, how do you create a string of random
-            characters?" -- "Read a Perl file!"</p>
-            <p>[<a href="../">Return</a>]</p>'''
-
-
-class LinksPage:
-
-    def __init__(self):
-        # Request handler objects can create their own nested request
-        # handler objects. Simply create them inside their __init__
-        # methods!
-        self.extra = ExtraLinksPage()
-
-    @cherrypy.expose
-    def index(self):
-        # Note the way we link to the extra links page (and back).
-        # As you can see, this object doesn't really care about its
-        # absolute position in the site tree, since we use relative
-        # links exclusively.
-        return '''
-            <p>Here are some useful links:</p>
-
-            <ul>
-                <li>
-                    <a href="http://www.cherrypy.org">The CherryPy Homepage</a>
-                </li>
-                <li>
-                    <a href="http://www.python.org">The Python Homepage</a>
-                </li>
-            </ul>
-
-            <p>You can check out some extra useful
-            links <a href="./extra/">here</a>.</p>
-
-            <p>[<a href="../">Return</a>]</p>
-        '''
-
-
-class ExtraLinksPage:
-
-    @cherrypy.expose
-    def index(self):
-        # Note the relative link back to the Links page!
-        return '''
-            <p>Here are some extra useful links:</p>
-
-            <ul>
-                <li><a href="http://del.icio.us">del.icio.us</a></li>
-                <li><a href="http://www.cherrypy.org">CherryPy</a></li>
-            </ul>
-
-            <p>[<a href="../">Return to links page</a>]</p>'''
-
-
-# Of course we can also mount request handler objects right here!
-root = HomePage()
-root.joke = JokePage()
-root.links = LinksPage()
-
-# Remember, we don't need to mount ExtraLinksPage here, because
-# LinksPage does that itself on initialization. In fact, there is
-# no reason why you shouldn't let your root object take care of
-# creating all contained request handler objects.
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(root, config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut05_derived_objects.py b/libraries/cherrypy/tutorial/tut05_derived_objects.py
deleted file mode 100644
index f626e03f..00000000
--- a/libraries/cherrypy/tutorial/tut05_derived_objects.py
+++ /dev/null
@@ -1,80 +0,0 @@
-"""
-Tutorial - Object inheritance
-
-You are free to derive your request handler classes from any base
-class you wish. In most real-world applications, you will probably
-want to create a central base class used for all your pages, which takes
-care of things like printing a common page header and footer.
-"""
-
-import os.path
-
-import cherrypy
-
-
-class Page:
-    # Store the page title in a class attribute
-    title = 'Untitled Page'
-
-    def header(self):
-        return '''
-            <html>
-            <head>
-                <title>%s</title>
-            <head>
-            <body>
-            <h2>%s</h2>
-        ''' % (self.title, self.title)
-
-    def footer(self):
-        return '''
-            </body>
-            </html>
-        '''
-
-    # Note that header and footer don't get their exposed attributes
-    # set to True. This isn't necessary since the user isn't supposed
-    # to call header or footer directly; instead, we'll call them from
-    # within the actually exposed handler methods defined in this
-    # class' subclasses.
-
-
-class HomePage(Page):
-    # Different title for this page
-    title = 'Tutorial 5'
-
-    def __init__(self):
-        # create a subpage
-        self.another = AnotherPage()
-
-    @cherrypy.expose
-    def index(self):
-        # Note that we call the header and footer methods inherited
-        # from the Page class!
-        return self.header() + '''
-            <p>
-            Isn't this exciting? There's
-            <a href="./another/">another page</a>, too!
-            </p>
-        ''' + self.footer()
-
-
-class AnotherPage(Page):
-    title = 'Another Page'
-
-    @cherrypy.expose
-    def index(self):
-        return self.header() + '''
-            <p>
-            And this is the amazing second page!
-            </p>
-        ''' + self.footer()
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(HomePage(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut06_default_method.py b/libraries/cherrypy/tutorial/tut06_default_method.py
deleted file mode 100644
index 0ce4cabe..00000000
--- a/libraries/cherrypy/tutorial/tut06_default_method.py
+++ /dev/null
@@ -1,61 +0,0 @@
-"""
-Tutorial - The default method
-
-Request handler objects can implement a method called "default" that
-is called when no other suitable method/object could be found.
-Essentially, if CherryPy2 can't find a matching request handler object
-for the given request URI, it will use the default method of the object
-located deepest on the URI path.
-
-Using this mechanism you can easily simulate virtual URI structures
-by parsing the extra URI string, which you can access through
-cherrypy.request.virtualPath.
-
-The application in this tutorial simulates an URI structure looking
-like /users/<username>. Since the <username> bit will not be found (as
-there are no matching methods), it is handled by the default method.
-"""
-
-import os.path
-
-import cherrypy
-
-
-class UsersPage:
-
-    @cherrypy.expose
-    def index(self):
-        # Since this is just a stupid little example, we'll simply
-        # display a list of links to random, made-up users. In a real
-        # application, this could be generated from a database result set.
-        return '''
-            <a href="./remi">Remi Delon</a><br/>
-            <a href="./hendrik">Hendrik Mans</a><br/>
-            <a href="./lorenzo">Lorenzo Lamas</a><br/>
-        '''
-
-    @cherrypy.expose
-    def default(self, user):
-        # Here we react depending on the virtualPath -- the part of the
-        # path that could not be mapped to an object method. In a real
-        # application, we would probably do some database lookups here
-        # instead of the silly if/elif/else construct.
-        if user == 'remi':
-            out = 'Remi Delon, CherryPy lead developer'
-        elif user == 'hendrik':
-            out = 'Hendrik Mans, CherryPy co-developer & crazy German'
-        elif user == 'lorenzo':
-            out = 'Lorenzo Lamas, famous actor and singer!'
-        else:
-            out = 'Unknown user. :-('
-
-        return '%s (<a href="./">back</a>)' % out
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(UsersPage(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut07_sessions.py b/libraries/cherrypy/tutorial/tut07_sessions.py
deleted file mode 100644
index 204322b5..00000000
--- a/libraries/cherrypy/tutorial/tut07_sessions.py
+++ /dev/null
@@ -1,41 +0,0 @@
-"""
-Tutorial - Sessions
-
-Storing session data in CherryPy applications is very easy: cherrypy
-provides a dictionary called "session" that represents the session
-data for the current user. If you use RAM based sessions, you can store
-any kind of object into that dictionary; otherwise, you are limited to
-objects that can be pickled.
-"""
-
-import os.path
-
-import cherrypy
-
-
-class HitCounter:
-
-    _cp_config = {'tools.sessions.on': True}
-
-    @cherrypy.expose
-    def index(self):
-        # Increase the silly hit counter
-        count = cherrypy.session.get('count', 0) + 1
-
-        # Store the new value in the session dictionary
-        cherrypy.session['count'] = count
-
-        # And display a silly hit count message!
-        return '''
-            During your current session, you've viewed this
-            page %s times! Your life is a patio of fun!
-        ''' % count
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(HitCounter(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut08_generators_and_yield.py b/libraries/cherrypy/tutorial/tut08_generators_and_yield.py
deleted file mode 100644
index 18f42f93..00000000
--- a/libraries/cherrypy/tutorial/tut08_generators_and_yield.py
+++ /dev/null
@@ -1,44 +0,0 @@
-"""
-Bonus Tutorial: Using generators to return result bodies
-
-Instead of returning a complete result string, you can use the yield
-statement to return one result part after another. This may be convenient
-in situations where using a template package like CherryPy or Cheetah
-would be overkill, and messy string concatenation too uncool. ;-)
-"""
-
-import os.path
-
-import cherrypy
-
-
-class GeneratorDemo:
-
-    def header(self):
-        return '<html><body><h2>Generators rule!</h2>'
-
-    def footer(self):
-        return '</body></html>'
-
-    @cherrypy.expose
-    def index(self):
-        # Let's make up a list of users for presentation purposes
-        users = ['Remi', 'Carlos', 'Hendrik', 'Lorenzo Lamas']
-
-        # Every yield line adds one part to the total result body.
-        yield self.header()
-        yield '<h3>List of users:</h3>'
-
-        for user in users:
-            yield '%s<br/>' % user
-
-        yield self.footer()
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(GeneratorDemo(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut09_files.py b/libraries/cherrypy/tutorial/tut09_files.py
deleted file mode 100644
index 48585cbe..00000000
--- a/libraries/cherrypy/tutorial/tut09_files.py
+++ /dev/null
@@ -1,105 +0,0 @@
-"""
-
-Tutorial: File upload and download
-
-Uploads
--------
-
-When a client uploads a file to a CherryPy application, it's placed
-on disk immediately. CherryPy will pass it to your exposed method
-as an argument (see "myFile" below); that arg will have a "file"
-attribute, which is a handle to the temporary uploaded file.
-If you wish to permanently save the file, you need to read()
-from myFile.file and write() somewhere else.
-
-Note the use of 'enctype="multipart/form-data"' and 'input type="file"'
-in the HTML which the client uses to upload the file.
-
-
-Downloads
----------
-
-If you wish to send a file to the client, you have two options:
-First, you can simply return a file-like object from your page handler.
-CherryPy will read the file and serve it as the content (HTTP body)
-of the response. However, that doesn't tell the client that
-the response is a file to be saved, rather than displayed.
-Use cherrypy.lib.static.serve_file for that; it takes four
-arguments:
-
-serve_file(path, content_type=None, disposition=None, name=None)
-
-Set "name" to the filename that you expect clients to use when they save
-your file. Note that the "name" argument is ignored if you don't also
-provide a "disposition" (usually "attachement"). You can manually set
-"content_type", but be aware that if you also use the encoding tool, it
-may choke if the file extension is not recognized as belonging to a known
-Content-Type. Setting the content_type to "application/x-download" works
-in most cases, and should prompt the user with an Open/Save dialog in
-popular browsers.
-
-"""
-
-import os
-import os.path
-
-import cherrypy
-from cherrypy.lib import static
-
-localDir = os.path.dirname(__file__)
-absDir = os.path.join(os.getcwd(), localDir)
-
-
-class FileDemo(object):
-
-    @cherrypy.expose
-    def index(self):
-        return """
-        <html><body>
-            <h2>Upload a file</h2>
-            <form action="upload" method="post" enctype="multipart/form-data">
-            filename: <input type="file" name="myFile" /><br />
-            <input type="submit" />
-            </form>
-            <h2>Download a file</h2>
-            <a href='download'>This one</a>
-        </body></html>
-        """
-
-    @cherrypy.expose
-    def upload(self, myFile):
-        out = """<html>
-        <body>
-            myFile length: %s<br />
-            myFile filename: %s<br />
-            myFile mime-type: %s
-        </body>
-        </html>"""
-
-        # Although this just counts the file length, it demonstrates
-        # how to read large files in chunks instead of all at once.
-        # CherryPy reads the uploaded file into a temporary file;
-        # myFile.file.read reads from that.
-        size = 0
-        while True:
-            data = myFile.file.read(8192)
-            if not data:
-                break
-            size += len(data)
-
-        return out % (size, myFile.filename, myFile.content_type)
-
-    @cherrypy.expose
-    def download(self):
-        path = os.path.join(absDir, 'pdf_file.pdf')
-        return static.serve_file(path, 'application/x-download',
-                                 'attachment', os.path.basename(path))
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(FileDemo(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tut10_http_errors.py b/libraries/cherrypy/tutorial/tut10_http_errors.py
deleted file mode 100644
index 18f02fd0..00000000
--- a/libraries/cherrypy/tutorial/tut10_http_errors.py
+++ /dev/null
@@ -1,84 +0,0 @@
-"""
-
-Tutorial: HTTP errors
-
-HTTPError is used to return an error response to the client.
-CherryPy has lots of options regarding how such errors are
-logged, displayed, and formatted.
-
-"""
-
-import os
-import os.path
-
-import cherrypy
-
-localDir = os.path.dirname(__file__)
-curpath = os.path.normpath(os.path.join(os.getcwd(), localDir))
-
-
-class HTTPErrorDemo(object):
-
-    # Set a custom response for 403 errors.
-    _cp_config = {'error_page.403':
-                  os.path.join(curpath, 'custom_error.html')}
-
-    @cherrypy.expose
-    def index(self):
-        # display some links that will result in errors
-        tracebacks = cherrypy.request.show_tracebacks
-        if tracebacks:
-            trace = 'off'
-        else:
-            trace = 'on'
-
-        return """
-        <html><body>
-            <p>Toggle tracebacks <a href="toggleTracebacks">%s</a></p>
-            <p><a href="/doesNotExist">Click me; I'm a broken link!</a></p>
-            <p>
-              <a href="/error?code=403">
-                Use a custom error page from a file.
-              </a>
-            </p>
-            <p>These errors are explicitly raised by the application:</p>
-            <ul>
-                <li><a href="/error?code=400">400</a></li>
-                <li><a href="/error?code=401">401</a></li>
-                <li><a href="/error?code=402">402</a></li>
-                <li><a href="/error?code=500">500</a></li>
-            </ul>
-            <p><a href="/messageArg">You can also set the response body
-            when you raise an error.</a></p>
-        </body></html>
-        """ % trace
-
-    @cherrypy.expose
-    def toggleTracebacks(self):
-        # simple function to toggle tracebacks on and off
-        tracebacks = cherrypy.request.show_tracebacks
-        cherrypy.config.update({'request.show_tracebacks': not tracebacks})
-
-        # redirect back to the index
-        raise cherrypy.HTTPRedirect('/')
-
-    @cherrypy.expose
-    def error(self, code):
-        # raise an error based on the get query
-        raise cherrypy.HTTPError(status=code)
-
-    @cherrypy.expose
-    def messageArg(self):
-        message = ("If you construct an HTTPError with a 'message' "
-                   'argument, it wil be placed on the error page '
-                   '(underneath the status line by default).')
-        raise cherrypy.HTTPError(500, message=message)
-
-
-tutconf = os.path.join(os.path.dirname(__file__), 'tutorial.conf')
-
-if __name__ == '__main__':
-    # CherryPy always starts with app.root when trying to map request URIs
-    # to objects, so we need to mount a request handler root. A request
-    # to '/' will be mapped to HelloWorld().index().
-    cherrypy.quickstart(HTTPErrorDemo(), config=tutconf)
diff --git a/libraries/cherrypy/tutorial/tutorial.conf b/libraries/cherrypy/tutorial/tutorial.conf
deleted file mode 100644
index 43dfa60f..00000000
--- a/libraries/cherrypy/tutorial/tutorial.conf
+++ /dev/null
@@ -1,4 +0,0 @@
-[global]
-server.socket_host = "127.0.0.1"
-server.socket_port = 8080
-server.thread_pool = 10
diff --git a/libraries/contextlib2.py b/libraries/contextlib2.py
deleted file mode 100644
index f08df14c..00000000
--- a/libraries/contextlib2.py
+++ /dev/null
@@ -1,436 +0,0 @@
-"""contextlib2 - backports and enhancements to the contextlib module"""
-
-import sys
-import warnings
-from collections import deque
-from functools import wraps
-
-__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack",
-           "redirect_stdout", "redirect_stderr", "suppress"]
-
-# Backwards compatibility
-__all__ += ["ContextStack"]
-
-class ContextDecorator(object):
-    "A base class or mixin that enables context managers to work as decorators."
-
-    def refresh_cm(self):
-        """Returns the context manager used to actually wrap the call to the
-        decorated function.
-
-        The default implementation just returns *self*.
-
-        Overriding this method allows otherwise one-shot context managers
-        like _GeneratorContextManager to support use as decorators via
-        implicit recreation.
-
-        DEPRECATED: refresh_cm was never added to the standard library's
-                    ContextDecorator API
-        """
-        warnings.warn("refresh_cm was never added to the standard library",
-                      DeprecationWarning)
-        return self._recreate_cm()
-
-    def _recreate_cm(self):
-        """Return a recreated instance of self.
-
-        Allows an otherwise one-shot context manager like
-        _GeneratorContextManager to support use as
-        a decorator via implicit recreation.
-
-        This is a private interface just for _GeneratorContextManager.
-        See issue #11647 for details.
-        """
-        return self
-
-    def __call__(self, func):
-        @wraps(func)
-        def inner(*args, **kwds):
-            with self._recreate_cm():
-                return func(*args, **kwds)
-        return inner
-
-
-class _GeneratorContextManager(ContextDecorator):
-    """Helper for @contextmanager decorator."""
-
-    def __init__(self, func, args, kwds):
-        self.gen = func(*args, **kwds)
-        self.func, self.args, self.kwds = func, args, kwds
-        # Issue 19330: ensure context manager instances have good docstrings
-        doc = getattr(func, "__doc__", None)
-        if doc is None:
-            doc = type(self).__doc__
-        self.__doc__ = doc
-        # Unfortunately, this still doesn't provide good help output when
-        # inspecting the created context manager instances, since pydoc
-        # currently bypasses the instance docstring and shows the docstring
-        # for the class instead.
-        # See http://bugs.python.org/issue19404 for more details.
-
-    def _recreate_cm(self):
-        # _GCM instances are one-shot context managers, so the
-        # CM must be recreated each time a decorated function is
-        # called
-        return self.__class__(self.func, self.args, self.kwds)
-
-    def __enter__(self):
-        try:
-            return next(self.gen)
-        except StopIteration:
-            raise RuntimeError("generator didn't yield")
-
-    def __exit__(self, type, value, traceback):
-        if type is None:
-            try:
-                next(self.gen)
-            except StopIteration:
-                return
-            else:
-                raise RuntimeError("generator didn't stop")
-        else:
-            if value is None:
-                # Need to force instantiation so we can reliably
-                # tell if we get the same exception back
-                value = type()
-            try:
-                self.gen.throw(type, value, traceback)
-                raise RuntimeError("generator didn't stop after throw()")
-            except StopIteration as exc:
-                # Suppress StopIteration *unless* it's the same exception that
-                # was passed to throw().  This prevents a StopIteration
-                # raised inside the "with" statement from being suppressed.
-                return exc is not value
-            except RuntimeError as exc:
-                # Don't re-raise the passed in exception
-                if exc is value:
-                    return False
-                # Likewise, avoid suppressing if a StopIteration exception
-                # was passed to throw() and later wrapped into a RuntimeError
-                # (see PEP 479).
-                if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value:
-                    return False
-                raise
-            except:
-                # only re-raise if it's *not* the exception that was
-                # passed to throw(), because __exit__() must not raise
-                # an exception unless __exit__() itself failed.  But throw()
-                # has to raise the exception to signal propagation, so this
-                # fixes the impedance mismatch between the throw() protocol
-                # and the __exit__() protocol.
-                #
-                if sys.exc_info()[1] is not value:
-                    raise
-
-
-def contextmanager(func):
-    """@contextmanager decorator.
-
-    Typical usage:
-
-        @contextmanager
-        def some_generator(<arguments>):
-            <setup>
-            try:
-                yield <value>
-            finally:
-                <cleanup>
-
-    This makes this:
-
-        with some_generator(<arguments>) as <variable>:
-            <body>
-
-    equivalent to this:
-
-        <setup>
-        try:
-            <variable> = <value>
-            <body>
-        finally:
-            <cleanup>
-
-    """
-    @wraps(func)
-    def helper(*args, **kwds):
-        return _GeneratorContextManager(func, args, kwds)
-    return helper
-
-
-class closing(object):
-    """Context to automatically close something at the end of a block.
-
-    Code like this:
-
-        with closing(<module>.open(<arguments>)) as f:
-            <block>
-
-    is equivalent to this:
-
-        f = <module>.open(<arguments>)
-        try:
-            <block>
-        finally:
-            f.close()
-
-    """
-    def __init__(self, thing):
-        self.thing = thing
-    def __enter__(self):
-        return self.thing
-    def __exit__(self, *exc_info):
-        self.thing.close()
-
-
-class _RedirectStream(object):
-
-    _stream = None
-
-    def __init__(self, new_target):
-        self._new_target = new_target
-        # We use a list of old targets to make this CM re-entrant
-        self._old_targets = []
-
-    def __enter__(self):
-        self._old_targets.append(getattr(sys, self._stream))
-        setattr(sys, self._stream, self._new_target)
-        return self._new_target
-
-    def __exit__(self, exctype, excinst, exctb):
-        setattr(sys, self._stream, self._old_targets.pop())
-
-
-class redirect_stdout(_RedirectStream):
-    """Context manager for temporarily redirecting stdout to another file.
-
-        # How to send help() to stderr
-        with redirect_stdout(sys.stderr):
-            help(dir)
-
-        # How to write help() to a file
-        with open('help.txt', 'w') as f:
-            with redirect_stdout(f):
-                help(pow)
-    """
-
-    _stream = "stdout"
-
-
-class redirect_stderr(_RedirectStream):
-    """Context manager for temporarily redirecting stderr to another file."""
-
-    _stream = "stderr"
-
-
-class suppress(object):
-    """Context manager to suppress specified exceptions
-
-    After the exception is suppressed, execution proceeds with the next
-    statement following the with statement.
-
-         with suppress(FileNotFoundError):
-             os.remove(somefile)
-         # Execution still resumes here if the file was already removed
-    """
-
-    def __init__(self, *exceptions):
-        self._exceptions = exceptions
-
-    def __enter__(self):
-        pass
-
-    def __exit__(self, exctype, excinst, exctb):
-        # Unlike isinstance and issubclass, CPython exception handling
-        # currently only looks at the concrete type hierarchy (ignoring
-        # the instance and subclass checking hooks). While Guido considers
-        # that a bug rather than a feature, it's a fairly hard one to fix
-        # due to various internal implementation details. suppress provides
-        # the simpler issubclass based semantics, rather than trying to
-        # exactly reproduce the limitations of the CPython interpreter.
-        #
-        # See http://bugs.python.org/issue12029 for more details
-        return exctype is not None and issubclass(exctype, self._exceptions)
-
-
-# Context manipulation is Python 3 only
-_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3
-if _HAVE_EXCEPTION_CHAINING:
-    def _make_context_fixer(frame_exc):
-        def _fix_exception_context(new_exc, old_exc):
-            # Context may not be correct, so find the end of the chain
-            while 1:
-                exc_context = new_exc.__context__
-                if exc_context is old_exc:
-                    # Context is already set correctly (see issue 20317)
-                    return
-                if exc_context is None or exc_context is frame_exc:
-                    break
-                new_exc = exc_context
-            # Change the end of the chain to point to the exception
-            # we expect it to reference
-            new_exc.__context__ = old_exc
-        return _fix_exception_context
-
-    def _reraise_with_existing_context(exc_details):
-        try:
-            # bare "raise exc_details[1]" replaces our carefully
-            # set-up context
-            fixed_ctx = exc_details[1].__context__
-            raise exc_details[1]
-        except BaseException:
-            exc_details[1].__context__ = fixed_ctx
-            raise
-else:
-    # No exception context in Python 2
-    def _make_context_fixer(frame_exc):
-        return lambda new_exc, old_exc: None
-
-    # Use 3 argument raise in Python 2,
-    # but use exec to avoid SyntaxError in Python 3
-    def _reraise_with_existing_context(exc_details):
-        exc_type, exc_value, exc_tb = exc_details
-        exec ("raise exc_type, exc_value, exc_tb")
-
-# Handle old-style classes if they exist
-try:
-    from types import InstanceType
-except ImportError:
-    # Python 3 doesn't have old-style classes
-    _get_type = type
-else:
-    # Need to handle old-style context managers on Python 2
-    def _get_type(obj):
-        obj_type = type(obj)
-        if obj_type is InstanceType:
-            return obj.__class__ # Old-style class
-        return obj_type # New-style class
-
-# Inspired by discussions on http://bugs.python.org/issue13585
-class ExitStack(object):
-    """Context manager for dynamic management of a stack of exit callbacks
-
-    For example:
-
-        with ExitStack() as stack:
-            files = [stack.enter_context(open(fname)) for fname in filenames]
-            # All opened files will automatically be closed at the end of
-            # the with statement, even if attempts to open files later
-            # in the list raise an exception
-
-    """
-    def __init__(self):
-        self._exit_callbacks = deque()
-
-    def pop_all(self):
-        """Preserve the context stack by transferring it to a new instance"""
-        new_stack = type(self)()
-        new_stack._exit_callbacks = self._exit_callbacks
-        self._exit_callbacks = deque()
-        return new_stack
-
-    def _push_cm_exit(self, cm, cm_exit):
-        """Helper to correctly register callbacks to __exit__ methods"""
-        def _exit_wrapper(*exc_details):
-            return cm_exit(cm, *exc_details)
-        _exit_wrapper.__self__ = cm
-        self.push(_exit_wrapper)
-
-    def push(self, exit):
-        """Registers a callback with the standard __exit__ method signature
-
-        Can suppress exceptions the same way __exit__ methods can.
-
-        Also accepts any object with an __exit__ method (registering a call
-        to the method instead of the object itself)
-        """
-        # We use an unbound method rather than a bound method to follow
-        # the standard lookup behaviour for special methods
-        _cb_type = _get_type(exit)
-        try:
-            exit_method = _cb_type.__exit__
-        except AttributeError:
-            # Not a context manager, so assume its a callable
-            self._exit_callbacks.append(exit)
-        else:
-            self._push_cm_exit(exit, exit_method)
-        return exit # Allow use as a decorator
-
-    def callback(self, callback, *args, **kwds):
-        """Registers an arbitrary callback and arguments.
-
-        Cannot suppress exceptions.
-        """
-        def _exit_wrapper(exc_type, exc, tb):
-            callback(*args, **kwds)
-        # We changed the signature, so using @wraps is not appropriate, but
-        # setting __wrapped__ may still help with introspection
-        _exit_wrapper.__wrapped__ = callback
-        self.push(_exit_wrapper)
-        return callback # Allow use as a decorator
-
-    def enter_context(self, cm):
-        """Enters the supplied context manager
-
-        If successful, also pushes its __exit__ method as a callback and
-        returns the result of the __enter__ method.
-        """
-        # We look up the special methods on the type to match the with statement
-        _cm_type = _get_type(cm)
-        _exit = _cm_type.__exit__
-        result = _cm_type.__enter__(cm)
-        self._push_cm_exit(cm, _exit)
-        return result
-
-    def close(self):
-        """Immediately unwind the context stack"""
-        self.__exit__(None, None, None)
-
-    def __enter__(self):
-        return self
-
-    def __exit__(self, *exc_details):
-        received_exc = exc_details[0] is not None
-
-        # We manipulate the exception state so it behaves as though
-        # we were actually nesting multiple with statements
-        frame_exc = sys.exc_info()[1]
-        _fix_exception_context = _make_context_fixer(frame_exc)
-
-        # Callbacks are invoked in LIFO order to match the behaviour of
-        # nested context managers
-        suppressed_exc = False
-        pending_raise = False
-        while self._exit_callbacks:
-            cb = self._exit_callbacks.pop()
-            try:
-                if cb(*exc_details):
-                    suppressed_exc = True
-                    pending_raise = False
-                    exc_details = (None, None, None)
-            except:
-                new_exc_details = sys.exc_info()
-                # simulate the stack of exceptions by setting the context
-                _fix_exception_context(new_exc_details[1], exc_details[1])
-                pending_raise = True
-                exc_details = new_exc_details
-        if pending_raise:
-            _reraise_with_existing_context(exc_details)
-        return received_exc and suppressed_exc
-
-# Preserve backwards compatibility
-class ContextStack(ExitStack):
-    """Backwards compatibility alias for ExitStack"""
-
-    def __init__(self):
-        warnings.warn("ContextStack has been renamed to ExitStack",
-                      DeprecationWarning)
-        super(ContextStack, self).__init__()
-
-    def register_exit(self, callback):
-        return self.push(callback)
-
-    def register(self, callback, *args, **kwds):
-        return self.callback(callback, *args, **kwds)
-
-    def preserve(self):
-        return self.pop_all()
diff --git a/libraries/more_itertools/__init__.py b/libraries/more_itertools/__init__.py
deleted file mode 100644
index bba462c3..00000000
--- a/libraries/more_itertools/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from more_itertools.more import *  # noqa
-from more_itertools.recipes import *  # noqa
diff --git a/libraries/more_itertools/more.py b/libraries/more_itertools/more.py
deleted file mode 100644
index 05e851ee..00000000
--- a/libraries/more_itertools/more.py
+++ /dev/null
@@ -1,2211 +0,0 @@
-from __future__ import print_function
-
-from collections import Counter, defaultdict, deque
-from functools import partial, wraps
-from heapq import merge
-from itertools import (
-    chain,
-    compress,
-    count,
-    cycle,
-    dropwhile,
-    groupby,
-    islice,
-    repeat,
-    starmap,
-    takewhile,
-    tee
-)
-from operator import itemgetter, lt, gt, sub
-from sys import maxsize, version_info
-try:
-    from collections.abc import Sequence
-except ImportError:
-    from collections import Sequence
-
-from six import binary_type, string_types, text_type
-from six.moves import filter, map, range, zip, zip_longest
-
-from .recipes import consume, flatten, take
-
-__all__ = [
-    'adjacent',
-    'always_iterable',
-    'always_reversible',
-    'bucket',
-    'chunked',
-    'circular_shifts',
-    'collapse',
-    'collate',
-    'consecutive_groups',
-    'consumer',
-    'count_cycle',
-    'difference',
-    'distinct_permutations',
-    'distribute',
-    'divide',
-    'exactly_n',
-    'first',
-    'groupby_transform',
-    'ilen',
-    'interleave_longest',
-    'interleave',
-    'intersperse',
-    'islice_extended',
-    'iterate',
-    'last',
-    'locate',
-    'lstrip',
-    'make_decorator',
-    'map_reduce',
-    'numeric_range',
-    'one',
-    'padded',
-    'peekable',
-    'replace',
-    'rlocate',
-    'rstrip',
-    'run_length',
-    'seekable',
-    'SequenceView',
-    'side_effect',
-    'sliced',
-    'sort_together',
-    'split_at',
-    'split_after',
-    'split_before',
-    'spy',
-    'stagger',
-    'strip',
-    'unique_to_each',
-    'windowed',
-    'with_iter',
-    'zip_offset',
-]
-
-_marker = object()
-
-
-def chunked(iterable, n):
-    """Break *iterable* into lists of length *n*:
-
-        >>> list(chunked([1, 2, 3, 4, 5, 6], 3))
-        [[1, 2, 3], [4, 5, 6]]
-
-    If the length of *iterable* is not evenly divisible by *n*, the last
-    returned list will be shorter:
-
-        >>> list(chunked([1, 2, 3, 4, 5, 6, 7, 8], 3))
-        [[1, 2, 3], [4, 5, 6], [7, 8]]
-
-    To use a fill-in value instead, see the :func:`grouper` recipe.
-
-    :func:`chunked` is useful for splitting up a computation on a large number
-    of keys into batches, to be pickled and sent off to worker processes. One
-    example is operations on rows in MySQL, which does not implement
-    server-side cursors properly and would otherwise load the entire dataset
-    into RAM on the client.
-
-    """
-    return iter(partial(take, n, iter(iterable)), [])
-
-
-def first(iterable, default=_marker):
-    """Return the first item of *iterable*, or *default* if *iterable* is
-    empty.
-
-        >>> first([0, 1, 2, 3])
-        0
-        >>> first([], 'some default')
-        'some default'
-
-    If *default* is not provided and there are no items in the iterable,
-    raise ``ValueError``.
-
-    :func:`first` is useful when you have a generator of expensive-to-retrieve
-    values and want any arbitrary one. It is marginally shorter than
-    ``next(iter(iterable), default)``.
-
-    """
-    try:
-        return next(iter(iterable))
-    except StopIteration:
-        # I'm on the edge about raising ValueError instead of StopIteration. At
-        # the moment, ValueError wins, because the caller could conceivably
-        # want to do something different with flow control when I raise the
-        # exception, and it's weird to explicitly catch StopIteration.
-        if default is _marker:
-            raise ValueError('first() was called on an empty iterable, and no '
-                             'default value was provided.')
-        return default
-
-
-def last(iterable, default=_marker):
-    """Return the last item of *iterable*, or *default* if *iterable* is
-    empty.
-
-        >>> last([0, 1, 2, 3])
-        3
-        >>> last([], 'some default')
-        'some default'
-
-    If *default* is not provided and there are no items in the iterable,
-    raise ``ValueError``.
-    """
-    try:
-        try:
-            # Try to access the last item directly
-            return iterable[-1]
-        except (TypeError, AttributeError, KeyError):
-            # If not slice-able, iterate entirely using length-1 deque
-            return deque(iterable, maxlen=1)[0]
-    except IndexError:  # If the iterable was empty
-        if default is _marker:
-            raise ValueError('last() was called on an empty iterable, and no '
-                             'default value was provided.')
-        return default
-
-
-class peekable(object):
-    """Wrap an iterator to allow lookahead and prepending elements.
-
-    Call :meth:`peek` on the result to get the value that will be returned
-    by :func:`next`. This won't advance the iterator:
-
-        >>> p = peekable(['a', 'b'])
-        >>> p.peek()
-        'a'
-        >>> next(p)
-        'a'
-
-    Pass :meth:`peek` a default value to return that instead of raising
-    ``StopIteration`` when the iterator is exhausted.
-
-        >>> p = peekable([])
-        >>> p.peek('hi')
-        'hi'
-
-    peekables also offer a :meth:`prepend` method, which "inserts" items
-    at the head of the iterable:
-
-        >>> p = peekable([1, 2, 3])
-        >>> p.prepend(10, 11, 12)
-        >>> next(p)
-        10
-        >>> p.peek()
-        11
-        >>> list(p)
-        [11, 12, 1, 2, 3]
-
-    peekables can be indexed. Index 0 is the item that will be returned by
-    :func:`next`, index 1 is the item after that, and so on:
-    The values up to the given index will be cached.
-
-        >>> p = peekable(['a', 'b', 'c', 'd'])
-        >>> p[0]
-        'a'
-        >>> p[1]
-        'b'
-        >>> next(p)
-        'a'
-
-    Negative indexes are supported, but be aware that they will cache the
-    remaining items in the source iterator, which may require significant
-    storage.
-
-    To check whether a peekable is exhausted, check its truth value:
-
-        >>> p = peekable(['a', 'b'])
-        >>> if p:  # peekable has items
-        ...     list(p)
-        ['a', 'b']
-        >>> if not p:  # peekable is exhaused
-        ...     list(p)
-        []
-
-    """
-    def __init__(self, iterable):
-        self._it = iter(iterable)
-        self._cache = deque()
-
-    def __iter__(self):
-        return self
-
-    def __bool__(self):
-        try:
-            self.peek()
-        except StopIteration:
-            return False
-        return True
-
-    def __nonzero__(self):
-        # For Python 2 compatibility
-        return self.__bool__()
-
-    def peek(self, default=_marker):
-        """Return the item that will be next returned from ``next()``.
-
-        Return ``default`` if there are no items left. If ``default`` is not
-        provided, raise ``StopIteration``.
-
-        """
-        if not self._cache:
-            try:
-                self._cache.append(next(self._it))
-            except StopIteration:
-                if default is _marker:
-                    raise
-                return default
-        return self._cache[0]
-
-    def prepend(self, *items):
-        """Stack up items to be the next ones returned from ``next()`` or
-        ``self.peek()``. The items will be returned in
-        first in, first out order::
-
-            >>> p = peekable([1, 2, 3])
-            >>> p.prepend(10, 11, 12)
-            >>> next(p)
-            10
-            >>> list(p)
-            [11, 12, 1, 2, 3]
-
-        It is possible, by prepending items, to "resurrect" a peekable that
-        previously raised ``StopIteration``.
-
-            >>> p = peekable([])
-            >>> next(p)
-            Traceback (most recent call last):
-              ...
-            StopIteration
-            >>> p.prepend(1)
-            >>> next(p)
-            1
-            >>> next(p)
-            Traceback (most recent call last):
-              ...
-            StopIteration
-
-        """
-        self._cache.extendleft(reversed(items))
-
-    def __next__(self):
-        if self._cache:
-            return self._cache.popleft()
-
-        return next(self._it)
-
-    next = __next__  # For Python 2 compatibility
-
-    def _get_slice(self, index):
-        # Normalize the slice's arguments
-        step = 1 if (index.step is None) else index.step
-        if step > 0:
-            start = 0 if (index.start is None) else index.start
-            stop = maxsize if (index.stop is None) else index.stop
-        elif step < 0:
-            start = -1 if (index.start is None) else index.start
-            stop = (-maxsize - 1) if (index.stop is None) else index.stop
-        else:
-            raise ValueError('slice step cannot be zero')
-
-        # If either the start or stop index is negative, we'll need to cache
-        # the rest of the iterable in order to slice from the right side.
-        if (start < 0) or (stop < 0):
-            self._cache.extend(self._it)
-        # Otherwise we'll need to find the rightmost index and cache to that
-        # point.
-        else:
-            n = min(max(start, stop) + 1, maxsize)
-            cache_len = len(self._cache)
-            if n >= cache_len:
-                self._cache.extend(islice(self._it, n - cache_len))
-
-        return list(self._cache)[index]
-
-    def __getitem__(self, index):
-        if isinstance(index, slice):
-            return self._get_slice(index)
-
-        cache_len = len(self._cache)
-        if index < 0:
-            self._cache.extend(self._it)
-        elif index >= cache_len:
-            self._cache.extend(islice(self._it, index + 1 - cache_len))
-
-        return self._cache[index]
-
-
-def _collate(*iterables, **kwargs):
-    """Helper for ``collate()``, called when the user is using the ``reverse``
-    or ``key`` keyword arguments on Python versions below 3.5.
-
-    """
-    key = kwargs.pop('key', lambda a: a)
-    reverse = kwargs.pop('reverse', False)
-
-    min_or_max = partial(max if reverse else min, key=itemgetter(0))
-    peekables = [peekable(it) for it in iterables]
-    peekables = [p for p in peekables if p]  # Kill empties.
-    while peekables:
-        _, p = min_or_max((key(p.peek()), p) for p in peekables)
-        yield next(p)
-        peekables = [x for x in peekables if x]
-
-
-def collate(*iterables, **kwargs):
-    """Return a sorted merge of the items from each of several already-sorted
-    *iterables*.
-
-        >>> list(collate('ACDZ', 'AZ', 'JKL'))
-        ['A', 'A', 'C', 'D', 'J', 'K', 'L', 'Z', 'Z']
-
-    Works lazily, keeping only the next value from each iterable in memory. Use
-    :func:`collate` to, for example, perform a n-way mergesort of items that
-    don't fit in memory.
-
-    If a *key* function is specified, the iterables will be sorted according
-    to its result:
-
-        >>> key = lambda s: int(s)  # Sort by numeric value, not by string
-        >>> list(collate(['1', '10'], ['2', '11'], key=key))
-        ['1', '2', '10', '11']
-
-
-    If the *iterables* are sorted in descending order, set *reverse* to
-    ``True``:
-
-        >>> list(collate([5, 3, 1], [4, 2, 0], reverse=True))
-        [5, 4, 3, 2, 1, 0]
-
-    If the elements of the passed-in iterables are out of order, you might get
-    unexpected results.
-
-    On Python 2.7, this function delegates to :func:`heapq.merge` if neither
-    of the keyword arguments are specified. On Python 3.5+, this function
-    is an alias for :func:`heapq.merge`.
-
-    """
-    if not kwargs:
-        return merge(*iterables)
-
-    return _collate(*iterables, **kwargs)
-
-
-# If using Python version 3.5 or greater, heapq.merge() will be faster than
-# collate - use that instead.
-if version_info >= (3, 5, 0):
-    _collate_docstring = collate.__doc__
-    collate = partial(merge)
-    collate.__doc__ = _collate_docstring
-
-
-def consumer(func):
-    """Decorator that automatically advances a PEP-342-style "reverse iterator"
-    to its first yield point so you don't have to call ``next()`` on it
-    manually.
-
-        >>> @consumer
-        ... def tally():
-        ...     i = 0
-        ...     while True:
-        ...         print('Thing number %s is %s.' % (i, (yield)))
-        ...         i += 1
-        ...
-        >>> t = tally()
-        >>> t.send('red')
-        Thing number 0 is red.
-        >>> t.send('fish')
-        Thing number 1 is fish.
-
-    Without the decorator, you would have to call ``next(t)`` before
-    ``t.send()`` could be used.
-
-    """
-    @wraps(func)
-    def wrapper(*args, **kwargs):
-        gen = func(*args, **kwargs)
-        next(gen)
-        return gen
-    return wrapper
-
-
-def ilen(iterable):
-    """Return the number of items in *iterable*.
-
-        >>> ilen(x for x in range(1000000) if x % 3 == 0)
-        333334
-
-    This consumes the iterable, so handle with care.
-
-    """
-    # maxlen=1 only stores the last item in the deque
-    d = deque(enumerate(iterable, 1), maxlen=1)
-    # since we started enumerate at 1,
-    # the first item of the last pair will be the length of the iterable
-    # (assuming there were items)
-    return d[0][0] if d else 0
-
-
-def iterate(func, start):
-    """Return ``start``, ``func(start)``, ``func(func(start))``, ...
-
-        >>> from itertools import islice
-        >>> list(islice(iterate(lambda x: 2*x, 1), 10))
-        [1, 2, 4, 8, 16, 32, 64, 128, 256, 512]
-
-    """
-    while True:
-        yield start
-        start = func(start)
-
-
-def with_iter(context_manager):
-    """Wrap an iterable in a ``with`` statement, so it closes once exhausted.
-
-    For example, this will close the file when the iterator is exhausted::
-
-        upper_lines = (line.upper() for line in with_iter(open('foo')))
-
-    Any context manager which returns an iterable is a candidate for
-    ``with_iter``.
-
-    """
-    with context_manager as iterable:
-        for item in iterable:
-            yield item
-
-
-def one(iterable, too_short=None, too_long=None):
-    """Return the first item from *iterable*, which is expected to contain only
-    that item. Raise an exception if *iterable* is empty or has more than one
-    item.
-
-    :func:`one` is useful for ensuring that an iterable contains only one item.
-    For example, it can be used to retrieve the result of a database query
-    that is expected to return a single row.
-
-    If *iterable* is empty, ``ValueError`` will be raised. You may specify a
-    different exception with the *too_short* keyword:
-
-        >>> it = []
-        >>> one(it)  # doctest: +IGNORE_EXCEPTION_DETAIL
-        Traceback (most recent call last):
-        ...
-        ValueError: too many items in iterable (expected 1)'
-        >>> too_short = IndexError('too few items')
-        >>> one(it, too_short=too_short)  # doctest: +IGNORE_EXCEPTION_DETAIL
-        Traceback (most recent call last):
-        ...
-        IndexError: too few items
-
-    Similarly, if *iterable* contains more than one item, ``ValueError`` will
-    be raised. You may specify a different exception with the *too_long*
-    keyword:
-
-        >>> it = ['too', 'many']
-        >>> one(it)  # doctest: +IGNORE_EXCEPTION_DETAIL
-        Traceback (most recent call last):
-        ...
-        ValueError: too many items in iterable (expected 1)'
-        >>> too_long = RuntimeError
-        >>> one(it, too_long=too_long)  # doctest: +IGNORE_EXCEPTION_DETAIL
-        Traceback (most recent call last):
-        ...
-        RuntimeError
-
-    Note that :func:`one` attempts to advance *iterable* twice to ensure there
-    is only one item. If there is more than one, both items will be discarded.
-    See :func:`spy` or :func:`peekable` to check iterable contents less
-    destructively.
-
-    """
-    it = iter(iterable)
-
-    try:
-        value = next(it)
-    except StopIteration:
-        raise too_short or ValueError('too few items in iterable (expected 1)')
-
-    try:
-        next(it)
-    except StopIteration:
-        pass
-    else:
-        raise too_long or ValueError('too many items in iterable (expected 1)')
-
-    return value
-
-
-def distinct_permutations(iterable):
-    """Yield successive distinct permutations of the elements in *iterable*.
-
-        >>> sorted(distinct_permutations([1, 0, 1]))
-        [(0, 1, 1), (1, 0, 1), (1, 1, 0)]
-
-    Equivalent to ``set(permutations(iterable))``, except duplicates are not
-    generated and thrown away. For larger input sequences this is much more
-    efficient.
-
-    Duplicate permutations arise when there are duplicated elements in the
-    input iterable. The number of items returned is
-    `n! / (x_1! * x_2! * ... * x_n!)`, where `n` is the total number of
-    items input, and each `x_i` is the count of a distinct item in the input
-    sequence.
-
-    """
-    def perm_unique_helper(item_counts, perm, i):
-        """Internal helper function
-
-        :arg item_counts: Stores the unique items in ``iterable`` and how many
-            times they are repeated
-        :arg perm: The permutation that is being built for output
-        :arg i: The index of the permutation being modified
-
-        The output permutations are built up recursively; the distinct items
-        are placed until their repetitions are exhausted.
-        """
-        if i < 0:
-            yield tuple(perm)
-        else:
-            for item in item_counts:
-                if item_counts[item] <= 0:
-                    continue
-                perm[i] = item
-                item_counts[item] -= 1
-                for x in perm_unique_helper(item_counts, perm, i - 1):
-                    yield x
-                item_counts[item] += 1
-
-    item_counts = Counter(iterable)
-    length = sum(item_counts.values())
-
-    return perm_unique_helper(item_counts, [None] * length, length - 1)
-
-
-def intersperse(e, iterable, n=1):
-    """Intersperse filler element *e* among the items in *iterable*, leaving
-    *n* items between each filler element.
-
-        >>> list(intersperse('!', [1, 2, 3, 4, 5]))
-        [1, '!', 2, '!', 3, '!', 4, '!', 5]
-
-        >>> list(intersperse(None, [1, 2, 3, 4, 5], n=2))
-        [1, 2, None, 3, 4, None, 5]
-
-    """
-    if n == 0:
-        raise ValueError('n must be > 0')
-    elif n == 1:
-        # interleave(repeat(e), iterable) -> e, x_0, e, e, x_1, e, x_2...
-        # islice(..., 1, None) -> x_0, e, e, x_1, e, x_2...
-        return islice(interleave(repeat(e), iterable), 1, None)
-    else:
-        # interleave(filler, chunks) -> [e], [x_0, x_1], [e], [x_2, x_3]...
-        # islice(..., 1, None) -> [x_0, x_1], [e], [x_2, x_3]...
-        # flatten(...) -> x_0, x_1, e, x_2, x_3...
-        filler = repeat([e])
-        chunks = chunked(iterable, n)
-        return flatten(islice(interleave(filler, chunks), 1, None))
-
-
-def unique_to_each(*iterables):
-    """Return the elements from each of the input iterables that aren't in the
-    other input iterables.
-
-    For example, suppose you have a set of packages, each with a set of
-    dependencies::
-
-        {'pkg_1': {'A', 'B'}, 'pkg_2': {'B', 'C'}, 'pkg_3': {'B', 'D'}}
-
-    If you remove one package, which dependencies can also be removed?
-
-    If ``pkg_1`` is removed, then ``A`` is no longer necessary - it is not
-    associated with ``pkg_2`` or ``pkg_3``. Similarly, ``C`` is only needed for
-    ``pkg_2``, and ``D`` is only needed for ``pkg_3``::
-
-        >>> unique_to_each({'A', 'B'}, {'B', 'C'}, {'B', 'D'})
-        [['A'], ['C'], ['D']]
-
-    If there are duplicates in one input iterable that aren't in the others
-    they will be duplicated in the output. Input order is preserved::
-
-        >>> unique_to_each("mississippi", "missouri")
-        [['p', 'p'], ['o', 'u', 'r']]
-
-    It is assumed that the elements of each iterable are hashable.
-
-    """
-    pool = [list(it) for it in iterables]
-    counts = Counter(chain.from_iterable(map(set, pool)))
-    uniques = {element for element in counts if counts[element] == 1}
-    return [list(filter(uniques.__contains__, it)) for it in pool]
-
-
-def windowed(seq, n, fillvalue=None, step=1):
-    """Return a sliding window of width *n* over the given iterable.
-
-        >>> all_windows = windowed([1, 2, 3, 4, 5], 3)
-        >>> list(all_windows)
-        [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
-
-    When the window is larger than the iterable, *fillvalue* is used in place
-    of missing values::
-
-        >>> list(windowed([1, 2, 3], 4))
-        [(1, 2, 3, None)]
-
-    Each window will advance in increments of *step*:
-
-        >>> list(windowed([1, 2, 3, 4, 5, 6], 3, fillvalue='!', step=2))
-        [(1, 2, 3), (3, 4, 5), (5, 6, '!')]
-
-    """
-    if n < 0:
-        raise ValueError('n must be >= 0')
-    if n == 0:
-        yield tuple()
-        return
-    if step < 1:
-        raise ValueError('step must be >= 1')
-
-    it = iter(seq)
-    window = deque([], n)
-    append = window.append
-
-    # Initial deque fill
-    for _ in range(n):
-        append(next(it, fillvalue))
-    yield tuple(window)
-
-    # Appending new items to the right causes old items to fall off the left
-    i = 0
-    for item in it:
-        append(item)
-        i = (i + 1) % step
-        if i % step == 0:
-            yield tuple(window)
-
-    # If there are items from the iterable in the window, pad with the given
-    # value and emit them.
-    if (i % step) and (step - i < n):
-        for _ in range(step - i):
-            append(fillvalue)
-        yield tuple(window)
-
-
-class bucket(object):
-    """Wrap *iterable* and return an object that buckets it iterable into
-    child iterables based on a *key* function.
-
-        >>> iterable = ['a1', 'b1', 'c1', 'a2', 'b2', 'c2', 'b3']
-        >>> s = bucket(iterable, key=lambda x: x[0])
-        >>> a_iterable = s['a']
-        >>> next(a_iterable)
-        'a1'
-        >>> next(a_iterable)
-        'a2'
-        >>> list(s['b'])
-        ['b1', 'b2', 'b3']
-
-    The original iterable will be advanced and its items will be cached until
-    they are used by the child iterables. This may require significant storage.
-
-    By default, attempting to select a bucket to which no items belong  will
-    exhaust the iterable and cache all values.
-    If you specify a *validator* function, selected buckets will instead be
-    checked against it.
-
-        >>> from itertools import count
-        >>> it = count(1, 2)  # Infinite sequence of odd numbers
-        >>> key = lambda x: x % 10  # Bucket by last digit
-        >>> validator = lambda x: x in {1, 3, 5, 7, 9}  # Odd digits only
-        >>> s = bucket(it, key=key, validator=validator)
-        >>> 2 in s
-        False
-        >>> list(s[2])
-        []
-
-    """
-    def __init__(self, iterable, key, validator=None):
-        self._it = iter(iterable)
-        self._key = key
-        self._cache = defaultdict(deque)
-        self._validator = validator or (lambda x: True)
-
-    def __contains__(self, value):
-        if not self._validator(value):
-            return False
-
-        try:
-            item = next(self[value])
-        except StopIteration:
-            return False
-        else:
-            self._cache[value].appendleft(item)
-
-        return True
-
-    def _get_values(self, value):
-        """
-        Helper to yield items from the parent iterator that match *value*.
-        Items that don't match are stored in the local cache as they
-        are encountered.
-        """
-        while True:
-            # If we've cached some items that match the target value, emit
-            # the first one and evict it from the cache.
-            if self._cache[value]:
-                yield self._cache[value].popleft()
-            # Otherwise we need to advance the parent iterator to search for
-            # a matching item, caching the rest.
-            else:
-                while True:
-                    try:
-                        item = next(self._it)
-                    except StopIteration:
-                        return
-                    item_value = self._key(item)
-                    if item_value == value:
-                        yield item
-                        break
-                    elif self._validator(item_value):
-                        self._cache[item_value].append(item)
-
-    def __getitem__(self, value):
-        if not self._validator(value):
-            return iter(())
-
-        return self._get_values(value)
-
-
-def spy(iterable, n=1):
-    """Return a 2-tuple with a list containing the first *n* elements of
-    *iterable*, and an iterator with the same items as *iterable*.
-    This allows you to "look ahead" at the items in the iterable without
-    advancing it.
-
-    There is one item in the list by default:
-
-        >>> iterable = 'abcdefg'
-        >>> head, iterable = spy(iterable)
-        >>> head
-        ['a']
-        >>> list(iterable)
-        ['a', 'b', 'c', 'd', 'e', 'f', 'g']
-
-    You may use unpacking to retrieve items instead of lists:
-
-        >>> (head,), iterable = spy('abcdefg')
-        >>> head
-        'a'
-        >>> (first, second), iterable = spy('abcdefg', 2)
-        >>> first
-        'a'
-        >>> second
-        'b'
-
-    The number of items requested can be larger than the number of items in
-    the iterable:
-
-        >>> iterable = [1, 2, 3, 4, 5]
-        >>> head, iterable = spy(iterable, 10)
-        >>> head
-        [1, 2, 3, 4, 5]
-        >>> list(iterable)
-        [1, 2, 3, 4, 5]
-
-    """
-    it = iter(iterable)
-    head = take(n, it)
-
-    return head, chain(head, it)
-
-
-def interleave(*iterables):
-    """Return a new iterable yielding from each iterable in turn,
-    until the shortest is exhausted.
-
-        >>> list(interleave([1, 2, 3], [4, 5], [6, 7, 8]))
-        [1, 4, 6, 2, 5, 7]
-
-    For a version that doesn't terminate after the shortest iterable is
-    exhausted, see :func:`interleave_longest`.
-
-    """
-    return chain.from_iterable(zip(*iterables))
-
-
-def interleave_longest(*iterables):
-    """Return a new iterable yielding from each iterable in turn,
-    skipping any that are exhausted.
-
-        >>> list(interleave_longest([1, 2, 3], [4, 5], [6, 7, 8]))
-        [1, 4, 6, 2, 5, 7, 3, 8]
-
-    This function produces the same output as :func:`roundrobin`, but may
-    perform better for some inputs (in particular when the number of iterables
-    is large).
-
-    """
-    i = chain.from_iterable(zip_longest(*iterables, fillvalue=_marker))
-    return (x for x in i if x is not _marker)
-
-
-def collapse(iterable, base_type=None, levels=None):
-    """Flatten an iterable with multiple levels of nesting (e.g., a list of
-    lists of tuples) into non-iterable types.
-
-        >>> iterable = [(1, 2), ([3, 4], [[5], [6]])]
-        >>> list(collapse(iterable))
-        [1, 2, 3, 4, 5, 6]
-
-    String types are not considered iterable and will not be collapsed.
-    To avoid collapsing other types, specify *base_type*:
-
-        >>> iterable = ['ab', ('cd', 'ef'), ['gh', 'ij']]
-        >>> list(collapse(iterable, base_type=tuple))
-        ['ab', ('cd', 'ef'), 'gh', 'ij']
-
-    Specify *levels* to stop flattening after a certain level:
-
-    >>> iterable = [('a', ['b']), ('c', ['d'])]
-    >>> list(collapse(iterable))  # Fully flattened
-    ['a', 'b', 'c', 'd']
-    >>> list(collapse(iterable, levels=1))  # Only one level flattened
-    ['a', ['b'], 'c', ['d']]
-
-    """
-    def walk(node, level):
-        if (
-            ((levels is not None) and (level > levels)) or
-            isinstance(node, string_types) or
-            ((base_type is not None) and isinstance(node, base_type))
-        ):
-            yield node
-            return
-
-        try:
-            tree = iter(node)
-        except TypeError:
-            yield node
-            return
-        else:
-            for child in tree:
-                for x in walk(child, level + 1):
-                    yield x
-
-    for x in walk(iterable, 0):
-        yield x
-
-
-def side_effect(func, iterable, chunk_size=None, before=None, after=None):
-    """Invoke *func* on each item in *iterable* (or on each *chunk_size* group
-    of items) before yielding the item.
-
-    `func` must be a function that takes a single argument. Its return value
-    will be discarded.
-
-    *before* and *after* are optional functions that take no arguments. They
-    will be executed before iteration starts and after it ends, respectively.
-
-    `side_effect` can be used for logging, updating progress bars, or anything
-    that is not functionally "pure."
-
-    Emitting a status message:
-
-        >>> from more_itertools import consume
-        >>> func = lambda item: print('Received {}'.format(item))
-        >>> consume(side_effect(func, range(2)))
-        Received 0
-        Received 1
-
-    Operating on chunks of items:
-
-        >>> pair_sums = []
-        >>> func = lambda chunk: pair_sums.append(sum(chunk))
-        >>> list(side_effect(func, [0, 1, 2, 3, 4, 5], 2))
-        [0, 1, 2, 3, 4, 5]
-        >>> list(pair_sums)
-        [1, 5, 9]
-
-    Writing to a file-like object:
-
-        >>> from io import StringIO
-        >>> from more_itertools import consume
-        >>> f = StringIO()
-        >>> func = lambda x: print(x, file=f)
-        >>> before = lambda: print(u'HEADER', file=f)
-        >>> after = f.close
-        >>> it = [u'a', u'b', u'c']
-        >>> consume(side_effect(func, it, before=before, after=after))
-        >>> f.closed
-        True
-
-    """
-    try:
-        if before is not None:
-            before()
-
-        if chunk_size is None:
-            for item in iterable:
-                func(item)
-                yield item
-        else:
-            for chunk in chunked(iterable, chunk_size):
-                func(chunk)
-                for item in chunk:
-                    yield item
-    finally:
-        if after is not None:
-            after()
-
-
-def sliced(seq, n):
-    """Yield slices of length *n* from the sequence *seq*.
-
-        >>> list(sliced((1, 2, 3, 4, 5, 6), 3))
-        [(1, 2, 3), (4, 5, 6)]
-
-    If the length of the sequence is not divisible by the requested slice
-    length, the last slice will be shorter.
-
-        >>> list(sliced((1, 2, 3, 4, 5, 6, 7, 8), 3))
-        [(1, 2, 3), (4, 5, 6), (7, 8)]
-
-    This function will only work for iterables that support slicing.
-    For non-sliceable iterables, see :func:`chunked`.
-
-    """
-    return takewhile(bool, (seq[i: i + n] for i in count(0, n)))
-
-
-def split_at(iterable, pred):
-    """Yield lists of items from *iterable*, where each list is delimited by
-    an item where callable *pred* returns ``True``. The lists do not include
-    the delimiting items.
-
-        >>> list(split_at('abcdcba', lambda x: x == 'b'))
-        [['a'], ['c', 'd', 'c'], ['a']]
-
-        >>> list(split_at(range(10), lambda n: n % 2 == 1))
-        [[0], [2], [4], [6], [8], []]
-    """
-    buf = []
-    for item in iterable:
-        if pred(item):
-            yield buf
-            buf = []
-        else:
-            buf.append(item)
-    yield buf
-
-
-def split_before(iterable, pred):
-    """Yield lists of items from *iterable*, where each list starts with an
-    item where callable *pred* returns ``True``:
-
-        >>> list(split_before('OneTwo', lambda s: s.isupper()))
-        [['O', 'n', 'e'], ['T', 'w', 'o']]
-
-        >>> list(split_before(range(10), lambda n: n % 3 == 0))
-        [[0, 1, 2], [3, 4, 5], [6, 7, 8], [9]]
-
-    """
-    buf = []
-    for item in iterable:
-        if pred(item) and buf:
-            yield buf
-            buf = []
-        buf.append(item)
-    yield buf
-
-
-def split_after(iterable, pred):
-    """Yield lists of items from *iterable*, where each list ends with an
-    item where callable *pred* returns ``True``:
-
-        >>> list(split_after('one1two2', lambda s: s.isdigit()))
-        [['o', 'n', 'e', '1'], ['t', 'w', 'o', '2']]
-
-        >>> list(split_after(range(10), lambda n: n % 3 == 0))
-        [[0], [1, 2, 3], [4, 5, 6], [7, 8, 9]]
-
-    """
-    buf = []
-    for item in iterable:
-        buf.append(item)
-        if pred(item) and buf:
-            yield buf
-            buf = []
-    if buf:
-        yield buf
-
-
-def padded(iterable, fillvalue=None, n=None, next_multiple=False):
-    """Yield the elements from *iterable*, followed by *fillvalue*, such that
-    at least *n* items are emitted.
-
-        >>> list(padded([1, 2, 3], '?', 5))
-        [1, 2, 3, '?', '?']
-
-    If *next_multiple* is ``True``, *fillvalue* will be emitted until the
-    number of items emitted is a multiple of *n*::
-
-        >>> list(padded([1, 2, 3, 4], n=3, next_multiple=True))
-        [1, 2, 3, 4, None, None]
-
-    If *n* is ``None``, *fillvalue* will be emitted indefinitely.
-
-    """
-    it = iter(iterable)
-    if n is None:
-        for item in chain(it, repeat(fillvalue)):
-            yield item
-    elif n < 1:
-        raise ValueError('n must be at least 1')
-    else:
-        item_count = 0
-        for item in it:
-            yield item
-            item_count += 1
-
-        remaining = (n - item_count) % n if next_multiple else n - item_count
-        for _ in range(remaining):
-            yield fillvalue
-
-
-def distribute(n, iterable):
-    """Distribute the items from *iterable* among *n* smaller iterables.
-
-        >>> group_1, group_2 = distribute(2, [1, 2, 3, 4, 5, 6])
-        >>> list(group_1)
-        [1, 3, 5]
-        >>> list(group_2)
-        [2, 4, 6]
-
-    If the length of *iterable* is not evenly divisible by *n*, then the
-    length of the returned iterables will not be identical:
-
-        >>> children = distribute(3, [1, 2, 3, 4, 5, 6, 7])
-        >>> [list(c) for c in children]
-        [[1, 4, 7], [2, 5], [3, 6]]
-
-    If the length of *iterable* is smaller than *n*, then the last returned
-    iterables will be empty:
-
-        >>> children = distribute(5, [1, 2, 3])
-        >>> [list(c) for c in children]
-        [[1], [2], [3], [], []]
-
-    This function uses :func:`itertools.tee` and may require significant
-    storage. If you need the order items in the smaller iterables to match the
-    original iterable, see :func:`divide`.
-
-    """
-    if n < 1:
-        raise ValueError('n must be at least 1')
-
-    children = tee(iterable, n)
-    return [islice(it, index, None, n) for index, it in enumerate(children)]
-
-
-def stagger(iterable, offsets=(-1, 0, 1), longest=False, fillvalue=None):
-    """Yield tuples whose elements are offset from *iterable*.
-    The amount by which the `i`-th item in each tuple is offset is given by
-    the `i`-th item in *offsets*.
-
-        >>> list(stagger([0, 1, 2, 3]))
-        [(None, 0, 1), (0, 1, 2), (1, 2, 3)]
-        >>> list(stagger(range(8), offsets=(0, 2, 4)))
-        [(0, 2, 4), (1, 3, 5), (2, 4, 6), (3, 5, 7)]
-
-    By default, the sequence will end when the final element of a tuple is the
-    last item in the iterable. To continue until the first element of a tuple
-    is the last item in the iterable, set *longest* to ``True``::
-
-        >>> list(stagger([0, 1, 2, 3], longest=True))
-        [(None, 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, None), (3, None, None)]
-
-    By default, ``None`` will be used to replace offsets beyond the end of the
-    sequence. Specify *fillvalue* to use some other value.
-
-    """
-    children = tee(iterable, len(offsets))
-
-    return zip_offset(
-        *children, offsets=offsets, longest=longest, fillvalue=fillvalue
-    )
-
-
-def zip_offset(*iterables, **kwargs):
-    """``zip`` the input *iterables* together, but offset the `i`-th iterable
-    by the `i`-th item in *offsets*.
-
-        >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1)))
-        [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e')]
-
-    This can be used as a lightweight alternative to SciPy or pandas to analyze
-    data sets in which somes series have a lead or lag relationship.
-
-    By default, the sequence will end when the shortest iterable is exhausted.
-    To continue until the longest iterable is exhausted, set *longest* to
-    ``True``.
-
-        >>> list(zip_offset('0123', 'abcdef', offsets=(0, 1), longest=True))
-        [('0', 'b'), ('1', 'c'), ('2', 'd'), ('3', 'e'), (None, 'f')]
-
-    By default, ``None`` will be used to replace offsets beyond the end of the
-    sequence. Specify *fillvalue* to use some other value.
-
-    """
-    offsets = kwargs['offsets']
-    longest = kwargs.get('longest', False)
-    fillvalue = kwargs.get('fillvalue', None)
-
-    if len(iterables) != len(offsets):
-        raise ValueError("Number of iterables and offsets didn't match")
-
-    staggered = []
-    for it, n in zip(iterables, offsets):
-        if n < 0:
-            staggered.append(chain(repeat(fillvalue, -n), it))
-        elif n > 0:
-            staggered.append(islice(it, n, None))
-        else:
-            staggered.append(it)
-
-    if longest:
-        return zip_longest(*staggered, fillvalue=fillvalue)
-
-    return zip(*staggered)
-
-
-def sort_together(iterables, key_list=(0,), reverse=False):
-    """Return the input iterables sorted together, with *key_list* as the
-    priority for sorting. All iterables are trimmed to the length of the
-    shortest one.
-
-    This can be used like the sorting function in a spreadsheet. If each
-    iterable represents a column of data, the key list determines which
-    columns are used for sorting.
-
-    By default, all iterables are sorted using the ``0``-th iterable::
-
-        >>> iterables = [(4, 3, 2, 1), ('a', 'b', 'c', 'd')]
-        >>> sort_together(iterables)
-        [(1, 2, 3, 4), ('d', 'c', 'b', 'a')]
-
-    Set a different key list to sort according to another iterable.
-    Specifying mutliple keys dictates how ties are broken::
-
-        >>> iterables = [(3, 1, 2), (0, 1, 0), ('c', 'b', 'a')]
-        >>> sort_together(iterables, key_list=(1, 2))
-        [(2, 3, 1), (0, 0, 1), ('a', 'c', 'b')]
-
-    Set *reverse* to ``True`` to sort in descending order.
-
-        >>> sort_together([(1, 2, 3), ('c', 'b', 'a')], reverse=True)
-        [(3, 2, 1), ('a', 'b', 'c')]
-
-    """
-    return list(zip(*sorted(zip(*iterables),
-                            key=itemgetter(*key_list),
-                            reverse=reverse)))
-
-
-def divide(n, iterable):
-    """Divide the elements from *iterable* into *n* parts, maintaining
-    order.
-
-        >>> group_1, group_2 = divide(2, [1, 2, 3, 4, 5, 6])
-        >>> list(group_1)
-        [1, 2, 3]
-        >>> list(group_2)
-        [4, 5, 6]
-
-    If the length of *iterable* is not evenly divisible by *n*, then the
-    length of the returned iterables will not be identical:
-
-        >>> children = divide(3, [1, 2, 3, 4, 5, 6, 7])
-        >>> [list(c) for c in children]
-        [[1, 2, 3], [4, 5], [6, 7]]
-
-    If the length of the iterable is smaller than n, then the last returned
-    iterables will be empty:
-
-        >>> children = divide(5, [1, 2, 3])
-        >>> [list(c) for c in children]
-        [[1], [2], [3], [], []]
-
-    This function will exhaust the iterable before returning and may require
-    significant storage. If order is not important, see :func:`distribute`,
-    which does not first pull the iterable into memory.
-
-    """
-    if n < 1:
-        raise ValueError('n must be at least 1')
-
-    seq = tuple(iterable)
-    q, r = divmod(len(seq), n)
-
-    ret = []
-    for i in range(n):
-        start = (i * q) + (i if i < r else r)
-        stop = ((i + 1) * q) + (i + 1 if i + 1 < r else r)
-        ret.append(iter(seq[start:stop]))
-
-    return ret
-
-
-def always_iterable(obj, base_type=(text_type, binary_type)):
-    """If *obj* is iterable, return an iterator over its items::
-
-        >>> obj = (1, 2, 3)
-        >>> list(always_iterable(obj))
-        [1, 2, 3]
-
-    If *obj* is not iterable, return a one-item iterable containing *obj*::
-
-        >>> obj = 1
-        >>> list(always_iterable(obj))
-        [1]
-
-    If *obj* is ``None``, return an empty iterable:
-
-        >>> obj = None
-        >>> list(always_iterable(None))
-        []
-
-    By default, binary and text strings are not considered iterable::
-
-        >>> obj = 'foo'
-        >>> list(always_iterable(obj))
-        ['foo']
-
-    If *base_type* is set, objects for which ``isinstance(obj, base_type)``
-    returns ``True`` won't be considered iterable.
-
-        >>> obj = {'a': 1}
-        >>> list(always_iterable(obj))  # Iterate over the dict's keys
-        ['a']
-        >>> list(always_iterable(obj, base_type=dict))  # Treat dicts as a unit
-        [{'a': 1}]
-
-    Set *base_type* to ``None`` to avoid any special handling and treat objects
-    Python considers iterable as iterable:
-
-        >>> obj = 'foo'
-        >>> list(always_iterable(obj, base_type=None))
-        ['f', 'o', 'o']
-    """
-    if obj is None:
-        return iter(())
-
-    if (base_type is not None) and isinstance(obj, base_type):
-        return iter((obj,))
-
-    try:
-        return iter(obj)
-    except TypeError:
-        return iter((obj,))
-
-
-def adjacent(predicate, iterable, distance=1):
-    """Return an iterable over `(bool, item)` tuples where the `item` is
-    drawn from *iterable* and the `bool` indicates whether
-    that item satisfies the *predicate* or is adjacent to an item that does.
-
-    For example, to find whether items are adjacent to a ``3``::
-
-        >>> list(adjacent(lambda x: x == 3, range(6)))
-        [(False, 0), (False, 1), (True, 2), (True, 3), (True, 4), (False, 5)]
-
-    Set *distance* to change what counts as adjacent. For example, to find
-    whether items are two places away from a ``3``:
-
-        >>> list(adjacent(lambda x: x == 3, range(6), distance=2))
-        [(False, 0), (True, 1), (True, 2), (True, 3), (True, 4), (True, 5)]
-
-    This is useful for contextualizing the results of a search function.
-    For example, a code comparison tool might want to identify lines that
-    have changed, but also surrounding lines to give the viewer of the diff
-    context.
-
-    The predicate function will only be called once for each item in the
-    iterable.
-
-    See also :func:`groupby_transform`, which can be used with this function
-    to group ranges of items with the same `bool` value.
-
-    """
-    # Allow distance=0 mainly for testing that it reproduces results with map()
-    if distance < 0:
-        raise ValueError('distance must be at least 0')
-
-    i1, i2 = tee(iterable)
-    padding = [False] * distance
-    selected = chain(padding, map(predicate, i1), padding)
-    adjacent_to_selected = map(any, windowed(selected, 2 * distance + 1))
-    return zip(adjacent_to_selected, i2)
-
-
-def groupby_transform(iterable, keyfunc=None, valuefunc=None):
-    """An extension of :func:`itertools.groupby` that transforms the values of
-    *iterable* after grouping them.
-    *keyfunc* is a function used to compute a grouping key for each item.
-    *valuefunc* is a function for transforming the items after grouping.
-
-        >>> iterable = 'AaaABbBCcA'
-        >>> keyfunc = lambda x: x.upper()
-        >>> valuefunc = lambda x: x.lower()
-        >>> grouper = groupby_transform(iterable, keyfunc, valuefunc)
-        >>> [(k, ''.join(g)) for k, g in grouper]
-        [('A', 'aaaa'), ('B', 'bbb'), ('C', 'cc'), ('A', 'a')]
-
-    *keyfunc* and *valuefunc* default to identity functions if they are not
-    specified.
-
-    :func:`groupby_transform` is useful when grouping elements of an iterable
-    using a separate iterable as the key. To do this, :func:`zip` the iterables
-    and pass a *keyfunc* that extracts the first element and a *valuefunc*
-    that extracts the second element::
-
-        >>> from operator import itemgetter
-        >>> keys = [0, 0, 1, 1, 1, 2, 2, 2, 3]
-        >>> values = 'abcdefghi'
-        >>> iterable = zip(keys, values)
-        >>> grouper = groupby_transform(iterable, itemgetter(0), itemgetter(1))
-        >>> [(k, ''.join(g)) for k, g in grouper]
-        [(0, 'ab'), (1, 'cde'), (2, 'fgh'), (3, 'i')]
-
-    Note that the order of items in the iterable is significant.
-    Only adjacent items are grouped together, so if you don't want any
-    duplicate groups, you should sort the iterable by the key function.
-
-    """
-    valuefunc = (lambda x: x) if valuefunc is None else valuefunc
-    return ((k, map(valuefunc, g)) for k, g in groupby(iterable, keyfunc))
-
-
-def numeric_range(*args):
-    """An extension of the built-in ``range()`` function whose arguments can
-    be any orderable numeric type.
-
-    With only *stop* specified, *start* defaults to ``0`` and *step*
-    defaults to ``1``. The output items will match the type of *stop*:
-
-        >>> list(numeric_range(3.5))
-        [0.0, 1.0, 2.0, 3.0]
-
-    With only *start* and *stop* specified, *step* defaults to ``1``. The
-    output items will match the type of *start*:
-
-        >>> from decimal import Decimal
-        >>> start = Decimal('2.1')
-        >>> stop = Decimal('5.1')
-        >>> list(numeric_range(start, stop))
-        [Decimal('2.1'), Decimal('3.1'), Decimal('4.1')]
-
-    With *start*, *stop*, and *step*  specified the output items will match
-    the type of ``start + step``:
-
-        >>> from fractions import Fraction
-        >>> start = Fraction(1, 2)  # Start at 1/2
-        >>> stop = Fraction(5, 2)  # End at 5/2
-        >>> step = Fraction(1, 2)  # Count by 1/2
-        >>> list(numeric_range(start, stop, step))
-        [Fraction(1, 2), Fraction(1, 1), Fraction(3, 2), Fraction(2, 1)]
-
-    If *step* is zero, ``ValueError`` is raised. Negative steps are supported:
-
-        >>> list(numeric_range(3, -1, -1.0))
-        [3.0, 2.0, 1.0, 0.0]
-
-    Be aware of the limitations of floating point numbers; the representation
-    of the yielded numbers may be surprising.
-
-    """
-    argc = len(args)
-    if argc == 1:
-        stop, = args
-        start = type(stop)(0)
-        step = 1
-    elif argc == 2:
-        start, stop = args
-        step = 1
-    elif argc == 3:
-        start, stop, step = args
-    else:
-        err_msg = 'numeric_range takes at most 3 arguments, got {}'
-        raise TypeError(err_msg.format(argc))
-
-    values = (start + (step * n) for n in count())
-    if step > 0:
-        return takewhile(partial(gt, stop), values)
-    elif step < 0:
-        return takewhile(partial(lt, stop), values)
-    else:
-        raise ValueError('numeric_range arg 3 must not be zero')
-
-
-def count_cycle(iterable, n=None):
-    """Cycle through the items from *iterable* up to *n* times, yielding
-    the number of completed cycles along with each item. If *n* is omitted the
-    process repeats indefinitely.
-
-    >>> list(count_cycle('AB', 3))
-    [(0, 'A'), (0, 'B'), (1, 'A'), (1, 'B'), (2, 'A'), (2, 'B')]
-
-    """
-    iterable = tuple(iterable)
-    if not iterable:
-        return iter(())
-    counter = count() if n is None else range(n)
-    return ((i, item) for i in counter for item in iterable)
-
-
-def locate(iterable, pred=bool, window_size=None):
-    """Yield the index of each item in *iterable* for which *pred* returns
-    ``True``.
-
-    *pred* defaults to :func:`bool`, which will select truthy items:
-
-        >>> list(locate([0, 1, 1, 0, 1, 0, 0]))
-        [1, 2, 4]
-
-    Set *pred* to a custom function to, e.g., find the indexes for a particular
-    item.
-
-        >>> list(locate(['a', 'b', 'c', 'b'], lambda x: x == 'b'))
-        [1, 3]
-
-    If *window_size* is given, then the *pred* function will be called with
-    that many items. This enables searching for sub-sequences:
-
-        >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
-        >>> pred = lambda *args: args == (1, 2, 3)
-        >>> list(locate(iterable, pred=pred, window_size=3))
-        [1, 5, 9]
-
-    Use with :func:`seekable` to find indexes and then retrieve the associated
-    items:
-
-        >>> from itertools import count
-        >>> from more_itertools import seekable
-        >>> source = (3 * n + 1 if (n % 2) else n // 2 for n in count())
-        >>> it = seekable(source)
-        >>> pred = lambda x: x > 100
-        >>> indexes = locate(it, pred=pred)
-        >>> i = next(indexes)
-        >>> it.seek(i)
-        >>> next(it)
-        106
-
-    """
-    if window_size is None:
-        return compress(count(), map(pred, iterable))
-
-    if window_size < 1:
-        raise ValueError('window size must be at least 1')
-
-    it = windowed(iterable, window_size, fillvalue=_marker)
-    return compress(count(), starmap(pred, it))
-
-
-def lstrip(iterable, pred):
-    """Yield the items from *iterable*, but strip any from the beginning
-    for which *pred* returns ``True``.
-
-    For example, to remove a set of items from the start of an iterable:
-
-        >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
-        >>> pred = lambda x: x in {None, False, ''}
-        >>> list(lstrip(iterable, pred))
-        [1, 2, None, 3, False, None]
-
-    This function is analogous to to :func:`str.lstrip`, and is essentially
-    an wrapper for :func:`itertools.dropwhile`.
-
-    """
-    return dropwhile(pred, iterable)
-
-
-def rstrip(iterable, pred):
-    """Yield the items from *iterable*, but strip any from the end
-    for which *pred* returns ``True``.
-
-    For example, to remove a set of items from the end of an iterable:
-
-        >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
-        >>> pred = lambda x: x in {None, False, ''}
-        >>> list(rstrip(iterable, pred))
-        [None, False, None, 1, 2, None, 3]
-
-    This function is analogous to :func:`str.rstrip`.
-
-    """
-    cache = []
-    cache_append = cache.append
-    for x in iterable:
-        if pred(x):
-            cache_append(x)
-        else:
-            for y in cache:
-                yield y
-            del cache[:]
-            yield x
-
-
-def strip(iterable, pred):
-    """Yield the items from *iterable*, but strip any from the
-    beginning and end for which *pred* returns ``True``.
-
-    For example, to remove a set of items from both ends of an iterable:
-
-        >>> iterable = (None, False, None, 1, 2, None, 3, False, None)
-        >>> pred = lambda x: x in {None, False, ''}
-        >>> list(strip(iterable, pred))
-        [1, 2, None, 3]
-
-    This function is analogous to :func:`str.strip`.
-
-    """
-    return rstrip(lstrip(iterable, pred), pred)
-
-
-def islice_extended(iterable, *args):
-    """An extension of :func:`itertools.islice` that supports negative values
-    for *stop*, *start*, and *step*.
-
-        >>> iterable = iter('abcdefgh')
-        >>> list(islice_extended(iterable, -4, -1))
-        ['e', 'f', 'g']
-
-    Slices with negative values require some caching of *iterable*, but this
-    function takes care to minimize the amount of memory required.
-
-    For example, you can use a negative step with an infinite iterator:
-
-        >>> from itertools import count
-        >>> list(islice_extended(count(), 110, 99, -2))
-        [110, 108, 106, 104, 102, 100]
-
-    """
-    s = slice(*args)
-    start = s.start
-    stop = s.stop
-    if s.step == 0:
-        raise ValueError('step argument must be a non-zero integer or None.')
-    step = s.step or 1
-
-    it = iter(iterable)
-
-    if step > 0:
-        start = 0 if (start is None) else start
-
-        if (start < 0):
-            # Consume all but the last -start items
-            cache = deque(enumerate(it, 1), maxlen=-start)
-            len_iter = cache[-1][0] if cache else 0
-
-            # Adjust start to be positive
-            i = max(len_iter + start, 0)
-
-            # Adjust stop to be positive
-            if stop is None:
-                j = len_iter
-            elif stop >= 0:
-                j = min(stop, len_iter)
-            else:
-                j = max(len_iter + stop, 0)
-
-            # Slice the cache
-            n = j - i
-            if n <= 0:
-                return
-
-            for index, item in islice(cache, 0, n, step):
-                yield item
-        elif (stop is not None) and (stop < 0):
-            # Advance to the start position
-            next(islice(it, start, start), None)
-
-            # When stop is negative, we have to carry -stop items while
-            # iterating
-            cache = deque(islice(it, -stop), maxlen=-stop)
-
-            for index, item in enumerate(it):
-                cached_item = cache.popleft()
-                if index % step == 0:
-                    yield cached_item
-                cache.append(item)
-        else:
-            # When both start and stop are positive we have the normal case
-            for item in islice(it, start, stop, step):
-                yield item
-    else:
-        start = -1 if (start is None) else start
-
-        if (stop is not None) and (stop < 0):
-            # Consume all but the last items
-            n = -stop - 1
-            cache = deque(enumerate(it, 1), maxlen=n)
-            len_iter = cache[-1][0] if cache else 0
-
-            # If start and stop are both negative they are comparable and
-            # we can just slice. Otherwise we can adjust start to be negative
-            # and then slice.
-            if start < 0:
-                i, j = start, stop
-            else:
-                i, j = min(start - len_iter, -1), None
-
-            for index, item in list(cache)[i:j:step]:
-                yield item
-        else:
-            # Advance to the stop position
-            if stop is not None:
-                m = stop + 1
-                next(islice(it, m, m), None)
-
-            # stop is positive, so if start is negative they are not comparable
-            # and we need the rest of the items.
-            if start < 0:
-                i = start
-                n = None
-            # stop is None and start is positive, so we just need items up to
-            # the start index.
-            elif stop is None:
-                i = None
-                n = start + 1
-            # Both stop and start are positive, so they are comparable.
-            else:
-                i = None
-                n = start - stop
-                if n <= 0:
-                    return
-
-            cache = list(islice(it, n))
-
-            for item in cache[i::step]:
-                yield item
-
-
-def always_reversible(iterable):
-    """An extension of :func:`reversed` that supports all iterables, not
-    just those which implement the ``Reversible`` or ``Sequence`` protocols.
-
-        >>> print(*always_reversible(x for x in range(3)))
-        2 1 0
-
-    If the iterable is already reversible, this function returns the
-    result of :func:`reversed()`. If the iterable is not reversible,
-    this function will cache the remaining items in the iterable and
-    yield them in reverse order, which may require significant storage.
-    """
-    try:
-        return reversed(iterable)
-    except TypeError:
-        return reversed(list(iterable))
-
-
-def consecutive_groups(iterable, ordering=lambda x: x):
-    """Yield groups of consecutive items using :func:`itertools.groupby`.
-    The *ordering* function determines whether two items are adjacent by
-    returning their position.
-
-    By default, the ordering function is the identity function. This is
-    suitable for finding runs of numbers:
-
-        >>> iterable = [1, 10, 11, 12, 20, 30, 31, 32, 33, 40]
-        >>> for group in consecutive_groups(iterable):
-        ...     print(list(group))
-        [1]
-        [10, 11, 12]
-        [20]
-        [30, 31, 32, 33]
-        [40]
-
-    For finding runs of adjacent letters, try using the :meth:`index` method
-    of a string of letters:
-
-        >>> from string import ascii_lowercase
-        >>> iterable = 'abcdfgilmnop'
-        >>> ordering = ascii_lowercase.index
-        >>> for group in consecutive_groups(iterable, ordering):
-        ...     print(list(group))
-        ['a', 'b', 'c', 'd']
-        ['f', 'g']
-        ['i']
-        ['l', 'm', 'n', 'o', 'p']
-
-    """
-    for k, g in groupby(
-        enumerate(iterable), key=lambda x: x[0] - ordering(x[1])
-    ):
-        yield map(itemgetter(1), g)
-
-
-def difference(iterable, func=sub):
-    """By default, compute the first difference of *iterable* using
-    :func:`operator.sub`.
-
-        >>> iterable = [0, 1, 3, 6, 10]
-        >>> list(difference(iterable))
-        [0, 1, 2, 3, 4]
-
-    This is the opposite of :func:`accumulate`'s default behavior:
-
-        >>> from more_itertools import accumulate
-        >>> iterable = [0, 1, 2, 3, 4]
-        >>> list(accumulate(iterable))
-        [0, 1, 3, 6, 10]
-        >>> list(difference(accumulate(iterable)))
-        [0, 1, 2, 3, 4]
-
-    By default *func* is :func:`operator.sub`, but other functions can be
-    specified. They will be applied as follows::
-
-        A, B, C, D, ... --> A, func(B, A), func(C, B), func(D, C), ...
-
-    For example, to do progressive division:
-
-        >>> iterable = [1, 2, 6, 24, 120]  # Factorial sequence
-        >>> func = lambda x, y: x // y
-        >>> list(difference(iterable, func))
-        [1, 2, 3, 4, 5]
-
-    """
-    a, b = tee(iterable)
-    try:
-        item = next(b)
-    except StopIteration:
-        return iter([])
-    return chain([item], map(lambda x: func(x[1], x[0]), zip(a, b)))
-
-
-class SequenceView(Sequence):
-    """Return a read-only view of the sequence object *target*.
-
-    :class:`SequenceView` objects are analagous to Python's built-in
-    "dictionary view" types. They provide a dynamic view of a sequence's items,
-    meaning that when the sequence updates, so does the view.
-
-        >>> seq = ['0', '1', '2']
-        >>> view = SequenceView(seq)
-        >>> view
-        SequenceView(['0', '1', '2'])
-        >>> seq.append('3')
-        >>> view
-        SequenceView(['0', '1', '2', '3'])
-
-    Sequence views support indexing, slicing, and length queries. They act
-    like the underlying sequence, except they don't allow assignment:
-
-        >>> view[1]
-        '1'
-        >>> view[1:-1]
-        ['1', '2']
-        >>> len(view)
-        4
-
-    Sequence views are useful as an alternative to copying, as they don't
-    require (much) extra storage.
-
-    """
-    def __init__(self, target):
-        if not isinstance(target, Sequence):
-            raise TypeError
-        self._target = target
-
-    def __getitem__(self, index):
-        return self._target[index]
-
-    def __len__(self):
-        return len(self._target)
-
-    def __repr__(self):
-        return '{}({})'.format(self.__class__.__name__, repr(self._target))
-
-
-class seekable(object):
-    """Wrap an iterator to allow for seeking backward and forward. This
-    progressively caches the items in the source iterable so they can be
-    re-visited.
-
-    Call :meth:`seek` with an index to seek to that position in the source
-    iterable.
-
-    To "reset" an iterator, seek to ``0``:
-
-        >>> from itertools import count
-        >>> it = seekable((str(n) for n in count()))
-        >>> next(it), next(it), next(it)
-        ('0', '1', '2')
-        >>> it.seek(0)
-        >>> next(it), next(it), next(it)
-        ('0', '1', '2')
-        >>> next(it)
-        '3'
-
-    You can also seek forward:
-
-        >>> it = seekable((str(n) for n in range(20)))
-        >>> it.seek(10)
-        >>> next(it)
-        '10'
-        >>> it.seek(20)  # Seeking past the end of the source isn't a problem
-        >>> list(it)
-        []
-        >>> it.seek(0)  # Resetting works even after hitting the end
-        >>> next(it), next(it), next(it)
-        ('0', '1', '2')
-
-    The cache grows as the source iterable progresses, so beware of wrapping
-    very large or infinite iterables.
-
-    You may view the contents of the cache with the :meth:`elements` method.
-    That returns a :class:`SequenceView`, a view that updates automatically:
-
-        >>> it = seekable((str(n) for n in range(10)))
-        >>> next(it), next(it), next(it)
-        ('0', '1', '2')
-        >>> elements = it.elements()
-        >>> elements
-        SequenceView(['0', '1', '2'])
-        >>> next(it)
-        '3'
-        >>> elements
-        SequenceView(['0', '1', '2', '3'])
-
-    """
-
-    def __init__(self, iterable):
-        self._source = iter(iterable)
-        self._cache = []
-        self._index = None
-
-    def __iter__(self):
-        return self
-
-    def __next__(self):
-        if self._index is not None:
-            try:
-                item = self._cache[self._index]
-            except IndexError:
-                self._index = None
-            else:
-                self._index += 1
-                return item
-
-        item = next(self._source)
-        self._cache.append(item)
-        return item
-
-    next = __next__
-
-    def elements(self):
-        return SequenceView(self._cache)
-
-    def seek(self, index):
-        self._index = index
-        remainder = index - len(self._cache)
-        if remainder > 0:
-            consume(self, remainder)
-
-
-class run_length(object):
-    """
-    :func:`run_length.encode` compresses an iterable with run-length encoding.
-    It yields groups of repeated items with the count of how many times they
-    were repeated:
-
-        >>> uncompressed = 'abbcccdddd'
-        >>> list(run_length.encode(uncompressed))
-        [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
-
-    :func:`run_length.decode` decompresses an iterable that was previously
-    compressed with run-length encoding. It yields the items of the
-    decompressed iterable:
-
-        >>> compressed = [('a', 1), ('b', 2), ('c', 3), ('d', 4)]
-        >>> list(run_length.decode(compressed))
-        ['a', 'b', 'b', 'c', 'c', 'c', 'd', 'd', 'd', 'd']
-
-    """
-
-    @staticmethod
-    def encode(iterable):
-        return ((k, ilen(g)) for k, g in groupby(iterable))
-
-    @staticmethod
-    def decode(iterable):
-        return chain.from_iterable(repeat(k, n) for k, n in iterable)
-
-
-def exactly_n(iterable, n, predicate=bool):
-    """Return ``True`` if exactly ``n`` items in the iterable are ``True``
-    according to the *predicate* function.
-
-        >>> exactly_n([True, True, False], 2)
-        True
-        >>> exactly_n([True, True, False], 1)
-        False
-        >>> exactly_n([0, 1, 2, 3, 4, 5], 3, lambda x: x < 3)
-        True
-
-    The iterable will be advanced until ``n + 1`` truthy items are encountered,
-    so avoid calling it on infinite iterables.
-
-    """
-    return len(take(n + 1, filter(predicate, iterable))) == n
-
-
-def circular_shifts(iterable):
-    """Return a list of circular shifts of *iterable*.
-
-        >>> circular_shifts(range(4))
-        [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)]
-    """
-    lst = list(iterable)
-    return take(len(lst), windowed(cycle(lst), len(lst)))
-
-
-def make_decorator(wrapping_func, result_index=0):
-    """Return a decorator version of *wrapping_func*, which is a function that
-    modifies an iterable. *result_index* is the position in that function's
-    signature where the iterable goes.
-
-    This lets you use itertools on the "production end," i.e. at function
-    definition. This can augment what the function returns without changing the
-    function's code.
-
-    For example, to produce a decorator version of :func:`chunked`:
-
-        >>> from more_itertools import chunked
-        >>> chunker = make_decorator(chunked, result_index=0)
-        >>> @chunker(3)
-        ... def iter_range(n):
-        ...     return iter(range(n))
-        ...
-        >>> list(iter_range(9))
-        [[0, 1, 2], [3, 4, 5], [6, 7, 8]]
-
-    To only allow truthy items to be returned:
-
-        >>> truth_serum = make_decorator(filter, result_index=1)
-        >>> @truth_serum(bool)
-        ... def boolean_test():
-        ...     return [0, 1, '', ' ', False, True]
-        ...
-        >>> list(boolean_test())
-        [1, ' ', True]
-
-    The :func:`peekable` and :func:`seekable` wrappers make for practical
-    decorators:
-
-        >>> from more_itertools import peekable
-        >>> peekable_function = make_decorator(peekable)
-        >>> @peekable_function()
-        ... def str_range(*args):
-        ...     return (str(x) for x in range(*args))
-        ...
-        >>> it = str_range(1, 20, 2)
-        >>> next(it), next(it), next(it)
-        ('1', '3', '5')
-        >>> it.peek()
-        '7'
-        >>> next(it)
-        '7'
-
-    """
-    # See https://sites.google.com/site/bbayles/index/decorator_factory for
-    # notes on how this works.
-    def decorator(*wrapping_args, **wrapping_kwargs):
-        def outer_wrapper(f):
-            def inner_wrapper(*args, **kwargs):
-                result = f(*args, **kwargs)
-                wrapping_args_ = list(wrapping_args)
-                wrapping_args_.insert(result_index, result)
-                return wrapping_func(*wrapping_args_, **wrapping_kwargs)
-
-            return inner_wrapper
-
-        return outer_wrapper
-
-    return decorator
-
-
-def map_reduce(iterable, keyfunc, valuefunc=None, reducefunc=None):
-    """Return a dictionary that maps the items in *iterable* to categories
-    defined by *keyfunc*, transforms them with *valuefunc*, and
-    then summarizes them by category with *reducefunc*.
-
-    *valuefunc* defaults to the identity function if it is unspecified.
-    If *reducefunc* is unspecified, no summarization takes place:
-
-        >>> keyfunc = lambda x: x.upper()
-        >>> result = map_reduce('abbccc', keyfunc)
-        >>> sorted(result.items())
-        [('A', ['a']), ('B', ['b', 'b']), ('C', ['c', 'c', 'c'])]
-
-    Specifying *valuefunc* transforms the categorized items:
-
-        >>> keyfunc = lambda x: x.upper()
-        >>> valuefunc = lambda x: 1
-        >>> result = map_reduce('abbccc', keyfunc, valuefunc)
-        >>> sorted(result.items())
-        [('A', [1]), ('B', [1, 1]), ('C', [1, 1, 1])]
-
-    Specifying *reducefunc* summarizes the categorized items:
-
-        >>> keyfunc = lambda x: x.upper()
-        >>> valuefunc = lambda x: 1
-        >>> reducefunc = sum
-        >>> result = map_reduce('abbccc', keyfunc, valuefunc, reducefunc)
-        >>> sorted(result.items())
-        [('A', 1), ('B', 2), ('C', 3)]
-
-    You may want to filter the input iterable before applying the map/reduce
-    procedure:
-
-        >>> all_items = range(30)
-        >>> items = [x for x in all_items if 10 <= x <= 20]  # Filter
-        >>> keyfunc = lambda x: x % 2  # Evens map to 0; odds to 1
-        >>> categories = map_reduce(items, keyfunc=keyfunc)
-        >>> sorted(categories.items())
-        [(0, [10, 12, 14, 16, 18, 20]), (1, [11, 13, 15, 17, 19])]
-        >>> summaries = map_reduce(items, keyfunc=keyfunc, reducefunc=sum)
-        >>> sorted(summaries.items())
-        [(0, 90), (1, 75)]
-
-    Note that all items in the iterable are gathered into a list before the
-    summarization step, which may require significant storage.
-
-    The returned object is a :obj:`collections.defaultdict` with the
-    ``default_factory`` set to ``None``, such that it behaves like a normal
-    dictionary.
-
-    """
-    valuefunc = (lambda x: x) if (valuefunc is None) else valuefunc
-
-    ret = defaultdict(list)
-    for item in iterable:
-        key = keyfunc(item)
-        value = valuefunc(item)
-        ret[key].append(value)
-
-    if reducefunc is not None:
-        for key, value_list in ret.items():
-            ret[key] = reducefunc(value_list)
-
-    ret.default_factory = None
-    return ret
-
-
-def rlocate(iterable, pred=bool, window_size=None):
-    """Yield the index of each item in *iterable* for which *pred* returns
-    ``True``, starting from the right and moving left.
-
-    *pred* defaults to :func:`bool`, which will select truthy items:
-
-        >>> list(rlocate([0, 1, 1, 0, 1, 0, 0]))  # Truthy at 1, 2, and 4
-        [4, 2, 1]
-
-    Set *pred* to a custom function to, e.g., find the indexes for a particular
-    item:
-
-        >>> iterable = iter('abcb')
-        >>> pred = lambda x: x == 'b'
-        >>> list(rlocate(iterable, pred))
-        [3, 1]
-
-    If *window_size* is given, then the *pred* function will be called with
-    that many items. This enables searching for sub-sequences:
-
-        >>> iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2, 3]
-        >>> pred = lambda *args: args == (1, 2, 3)
-        >>> list(rlocate(iterable, pred=pred, window_size=3))
-        [9, 5, 1]
-
-    Beware, this function won't return anything for infinite iterables.
-    If *iterable* is reversible, ``rlocate`` will reverse it and search from
-    the right. Otherwise, it will search from the left and return the results
-    in reverse order.
-
-    See :func:`locate` to for other example applications.
-
-    """
-    if window_size is None:
-        try:
-            len_iter = len(iterable)
-            return (
-                len_iter - i - 1 for i in locate(reversed(iterable), pred)
-            )
-        except TypeError:
-            pass
-
-    return reversed(list(locate(iterable, pred, window_size)))
-
-
-def replace(iterable, pred, substitutes, count=None, window_size=1):
-    """Yield the items from *iterable*, replacing the items for which *pred*
-    returns ``True`` with the items from the iterable *substitutes*.
-
-        >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1]
-        >>> pred = lambda x: x == 0
-        >>> substitutes = (2, 3)
-        >>> list(replace(iterable, pred, substitutes))
-        [1, 1, 2, 3, 1, 1, 2, 3, 1, 1]
-
-    If *count* is given, the number of replacements will be limited:
-
-        >>> iterable = [1, 1, 0, 1, 1, 0, 1, 1, 0]
-        >>> pred = lambda x: x == 0
-        >>> substitutes = [None]
-        >>> list(replace(iterable, pred, substitutes, count=2))
-        [1, 1, None, 1, 1, None, 1, 1, 0]
-
-    Use *window_size* to control the number of items passed as arguments to
-    *pred*. This allows for locating and replacing subsequences.
-
-        >>> iterable = [0, 1, 2, 5, 0, 1, 2, 5]
-        >>> window_size = 3
-        >>> pred = lambda *args: args == (0, 1, 2)  # 3 items passed to pred
-        >>> substitutes = [3, 4] # Splice in these items
-        >>> list(replace(iterable, pred, substitutes, window_size=window_size))
-        [3, 4, 5, 3, 4, 5]
-
-    """
-    if window_size < 1:
-        raise ValueError('window_size must be at least 1')
-
-    # Save the substitutes iterable, since it's used more than once
-    substitutes = tuple(substitutes)
-
-    # Add padding such that the number of windows matches the length of the
-    # iterable
-    it = chain(iterable, [_marker] * (window_size - 1))
-    windows = windowed(it, window_size)
-
-    n = 0
-    for w in windows:
-        # If the current window matches our predicate (and we haven't hit
-        # our maximum number of replacements), splice in the substitutes
-        # and then consume the following windows that overlap with this one.
-        # For example, if the iterable is (0, 1, 2, 3, 4...)
-        # and the window size is 2, we have (0, 1), (1, 2), (2, 3)...
-        # If the predicate matches on (0, 1), we need to zap (0, 1) and (1, 2)
-        if pred(*w):
-            if (count is None) or (n < count):
-                n += 1
-                for s in substitutes:
-                    yield s
-                consume(windows, window_size - 1)
-                continue
-
-        # If there was no match (or we've reached the replacement limit),
-        # yield the first item from the window.
-        if w and (w[0] is not _marker):
-            yield w[0]
diff --git a/libraries/more_itertools/recipes.py b/libraries/more_itertools/recipes.py
deleted file mode 100644
index 3a7706cb..00000000
--- a/libraries/more_itertools/recipes.py
+++ /dev/null
@@ -1,565 +0,0 @@
-"""Imported from the recipes section of the itertools documentation.
-
-All functions taken from the recipes section of the itertools library docs
-[1]_.
-Some backward-compatible usability improvements have been made.
-
-.. [1] http://docs.python.org/library/itertools.html#recipes
-
-"""
-from collections import deque
-from itertools import (
-    chain, combinations, count, cycle, groupby, islice, repeat, starmap, tee
-)
-import operator
-from random import randrange, sample, choice
-
-from six import PY2
-from six.moves import filter, filterfalse, map, range, zip, zip_longest
-
-__all__ = [
-    'accumulate',
-    'all_equal',
-    'consume',
-    'dotproduct',
-    'first_true',
-    'flatten',
-    'grouper',
-    'iter_except',
-    'ncycles',
-    'nth',
-    'nth_combination',
-    'padnone',
-    'pairwise',
-    'partition',
-    'powerset',
-    'prepend',
-    'quantify',
-    'random_combination_with_replacement',
-    'random_combination',
-    'random_permutation',
-    'random_product',
-    'repeatfunc',
-    'roundrobin',
-    'tabulate',
-    'tail',
-    'take',
-    'unique_everseen',
-    'unique_justseen',
-]
-
-
-def accumulate(iterable, func=operator.add):
-    """
-    Return an iterator whose items are the accumulated results of a function
-    (specified by the optional *func* argument) that takes two arguments.
-    By default, returns accumulated sums with :func:`operator.add`.
-
-        >>> list(accumulate([1, 2, 3, 4, 5]))  # Running sum
-        [1, 3, 6, 10, 15]
-        >>> list(accumulate([1, 2, 3], func=operator.mul))  # Running product
-        [1, 2, 6]
-        >>> list(accumulate([0, 1, -1, 2, 3, 2], func=max))  # Running maximum
-        [0, 1, 1, 2, 3, 3]
-
-    This function is available in the ``itertools`` module for Python 3.2 and
-    greater.
-
-    """
-    it = iter(iterable)
-    try:
-        total = next(it)
-    except StopIteration:
-        return
-    else:
-        yield total
-
-    for element in it:
-        total = func(total, element)
-        yield total
-
-
-def take(n, iterable):
-    """Return first *n* items of the iterable as a list.
-
-        >>> take(3, range(10))
-        [0, 1, 2]
-        >>> take(5, range(3))
-        [0, 1, 2]
-
-    Effectively a short replacement for ``next`` based iterator consumption
-    when you want more than one item, but less than the whole iterator.
-
-    """
-    return list(islice(iterable, n))
-
-
-def tabulate(function, start=0):
-    """Return an iterator over the results of ``func(start)``,
-    ``func(start + 1)``, ``func(start + 2)``...
-
-    *func* should be a function that accepts one integer argument.
-
-    If *start* is not specified it defaults to 0. It will be incremented each
-    time the iterator is advanced.
-
-        >>> square = lambda x: x ** 2
-        >>> iterator = tabulate(square, -3)
-        >>> take(4, iterator)
-        [9, 4, 1, 0]
-
-    """
-    return map(function, count(start))
-
-
-def tail(n, iterable):
-    """Return an iterator over the last *n* items of *iterable*.
-
-        >>> t = tail(3, 'ABCDEFG')
-        >>> list(t)
-        ['E', 'F', 'G']
-
-    """
-    return iter(deque(iterable, maxlen=n))
-
-
-def consume(iterator, n=None):
-    """Advance *iterable* by *n* steps. If *n* is ``None``, consume it
-    entirely.
-
-    Efficiently exhausts an iterator without returning values. Defaults to
-    consuming the whole iterator, but an optional second argument may be
-    provided to limit consumption.
-
-        >>> i = (x for x in range(10))
-        >>> next(i)
-        0
-        >>> consume(i, 3)
-        >>> next(i)
-        4
-        >>> consume(i)
-        >>> next(i)
-        Traceback (most recent call last):
-          File "<stdin>", line 1, in <module>
-        StopIteration
-
-    If the iterator has fewer items remaining than the provided limit, the
-    whole iterator will be consumed.
-
-        >>> i = (x for x in range(3))
-        >>> consume(i, 5)
-        >>> next(i)
-        Traceback (most recent call last):
-          File "<stdin>", line 1, in <module>
-        StopIteration
-
-    """
-    # Use functions that consume iterators at C speed.
-    if n is None:
-        # feed the entire iterator into a zero-length deque
-        deque(iterator, maxlen=0)
-    else:
-        # advance to the empty slice starting at position n
-        next(islice(iterator, n, n), None)
-
-
-def nth(iterable, n, default=None):
-    """Returns the nth item or a default value.
-
-        >>> l = range(10)
-        >>> nth(l, 3)
-        3
-        >>> nth(l, 20, "zebra")
-        'zebra'
-
-    """
-    return next(islice(iterable, n, None), default)
-
-
-def all_equal(iterable):
-    """
-    Returns ``True`` if all the elements are equal to each other.
-
-        >>> all_equal('aaaa')
-        True
-        >>> all_equal('aaab')
-        False
-
-    """
-    g = groupby(iterable)
-    return next(g, True) and not next(g, False)
-
-
-def quantify(iterable, pred=bool):
-    """Return the how many times the predicate is true.
-
-        >>> quantify([True, False, True])
-        2
-
-    """
-    return sum(map(pred, iterable))
-
-
-def padnone(iterable):
-    """Returns the sequence of elements and then returns ``None`` indefinitely.
-
-        >>> take(5, padnone(range(3)))
-        [0, 1, 2, None, None]
-
-    Useful for emulating the behavior of the built-in :func:`map` function.
-
-    See also :func:`padded`.
-
-    """
-    return chain(iterable, repeat(None))
-
-
-def ncycles(iterable, n):
-    """Returns the sequence elements *n* times
-
-        >>> list(ncycles(["a", "b"], 3))
-        ['a', 'b', 'a', 'b', 'a', 'b']
-
-    """
-    return chain.from_iterable(repeat(tuple(iterable), n))
-
-
-def dotproduct(vec1, vec2):
-    """Returns the dot product of the two iterables.
-
-        >>> dotproduct([10, 10], [20, 20])
-        400
-
-    """
-    return sum(map(operator.mul, vec1, vec2))
-
-
-def flatten(listOfLists):
-    """Return an iterator flattening one level of nesting in a list of lists.
-
-        >>> list(flatten([[0, 1], [2, 3]]))
-        [0, 1, 2, 3]
-
-    See also :func:`collapse`, which can flatten multiple levels of nesting.
-
-    """
-    return chain.from_iterable(listOfLists)
-
-
-def repeatfunc(func, times=None, *args):
-    """Call *func* with *args* repeatedly, returning an iterable over the
-    results.
-
-    If *times* is specified, the iterable will terminate after that many
-    repetitions:
-
-        >>> from operator import add
-        >>> times = 4
-        >>> args = 3, 5
-        >>> list(repeatfunc(add, times, *args))
-        [8, 8, 8, 8]
-
-    If *times* is ``None`` the iterable will not terminate:
-
-        >>> from random import randrange
-        >>> times = None
-        >>> args = 1, 11
-        >>> take(6, repeatfunc(randrange, times, *args))  # doctest:+SKIP
-        [2, 4, 8, 1, 8, 4]
-
-    """
-    if times is None:
-        return starmap(func, repeat(args))
-    return starmap(func, repeat(args, times))
-
-
-def pairwise(iterable):
-    """Returns an iterator of paired items, overlapping, from the original
-
-        >>> take(4, pairwise(count()))
-        [(0, 1), (1, 2), (2, 3), (3, 4)]
-
-    """
-    a, b = tee(iterable)
-    next(b, None)
-    return zip(a, b)
-
-
-def grouper(n, iterable, fillvalue=None):
-    """Collect data into fixed-length chunks or blocks.
-
-        >>> list(grouper(3, 'ABCDEFG', 'x'))
-        [('A', 'B', 'C'), ('D', 'E', 'F'), ('G', 'x', 'x')]
-
-    """
-    args = [iter(iterable)] * n
-    return zip_longest(fillvalue=fillvalue, *args)
-
-
-def roundrobin(*iterables):
-    """Yields an item from each iterable, alternating between them.
-
-        >>> list(roundrobin('ABC', 'D', 'EF'))
-        ['A', 'D', 'E', 'B', 'F', 'C']
-
-    This function produces the same output as :func:`interleave_longest`, but
-    may perform better for some inputs (in particular when the number of
-    iterables is small).
-
-    """
-    # Recipe credited to George Sakkis
-    pending = len(iterables)
-    if PY2:
-        nexts = cycle(iter(it).next for it in iterables)
-    else:
-        nexts = cycle(iter(it).__next__ for it in iterables)
-    while pending:
-        try:
-            for next in nexts:
-                yield next()
-        except StopIteration:
-            pending -= 1
-            nexts = cycle(islice(nexts, pending))
-
-
-def partition(pred, iterable):
-    """
-    Returns a 2-tuple of iterables derived from the input iterable.
-    The first yields the items that have ``pred(item) == False``.
-    The second yields the items that have ``pred(item) == True``.
-
-        >>> is_odd = lambda x: x % 2 != 0
-        >>> iterable = range(10)
-        >>> even_items, odd_items = partition(is_odd, iterable)
-        >>> list(even_items), list(odd_items)
-        ([0, 2, 4, 6, 8], [1, 3, 5, 7, 9])
-
-    """
-    # partition(is_odd, range(10)) --> 0 2 4 6 8   and  1 3 5 7 9
-    t1, t2 = tee(iterable)
-    return filterfalse(pred, t1), filter(pred, t2)
-
-
-def powerset(iterable):
-    """Yields all possible subsets of the iterable.
-
-        >>> list(powerset([1,2,3]))
-        [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
-
-    """
-    s = list(iterable)
-    return chain.from_iterable(combinations(s, r) for r in range(len(s) + 1))
-
-
-def unique_everseen(iterable, key=None):
-    """
-    Yield unique elements, preserving order.
-
-        >>> list(unique_everseen('AAAABBBCCDAABBB'))
-        ['A', 'B', 'C', 'D']
-        >>> list(unique_everseen('ABBCcAD', str.lower))
-        ['A', 'B', 'C', 'D']
-
-    Sequences with a mix of hashable and unhashable items can be used.
-    The function will be slower (i.e., `O(n^2)`) for unhashable items.
-
-    """
-    seenset = set()
-    seenset_add = seenset.add
-    seenlist = []
-    seenlist_add = seenlist.append
-    if key is None:
-        for element in iterable:
-            try:
-                if element not in seenset:
-                    seenset_add(element)
-                    yield element
-            except TypeError:
-                if element not in seenlist:
-                    seenlist_add(element)
-                    yield element
-    else:
-        for element in iterable:
-            k = key(element)
-            try:
-                if k not in seenset:
-                    seenset_add(k)
-                    yield element
-            except TypeError:
-                if k not in seenlist:
-                    seenlist_add(k)
-                    yield element
-
-
-def unique_justseen(iterable, key=None):
-    """Yields elements in order, ignoring serial duplicates
-
-        >>> list(unique_justseen('AAAABBBCCDAABBB'))
-        ['A', 'B', 'C', 'D', 'A', 'B']
-        >>> list(unique_justseen('ABBCcAD', str.lower))
-        ['A', 'B', 'C', 'A', 'D']
-
-    """
-    return map(next, map(operator.itemgetter(1), groupby(iterable, key)))
-
-
-def iter_except(func, exception, first=None):
-    """Yields results from a function repeatedly until an exception is raised.
-
-    Converts a call-until-exception interface to an iterator interface.
-    Like ``iter(func, sentinel)``, but uses an exception instead of a sentinel
-    to end the loop.
-
-        >>> l = [0, 1, 2]
-        >>> list(iter_except(l.pop, IndexError))
-        [2, 1, 0]
-
-    """
-    try:
-        if first is not None:
-            yield first()
-        while 1:
-            yield func()
-    except exception:
-        pass
-
-
-def first_true(iterable, default=False, pred=None):
-    """
-    Returns the first true value in the iterable.
-
-    If no true value is found, returns *default*
-
-    If *pred* is not None, returns the first item for which
-    ``pred(item) == True`` .
-
-        >>> first_true(range(10))
-        1
-        >>> first_true(range(10), pred=lambda x: x > 5)
-        6
-        >>> first_true(range(10), default='missing', pred=lambda x: x > 9)
-        'missing'
-
-    """
-    return next(filter(pred, iterable), default)
-
-
-def random_product(*args, **kwds):
-    """Draw an item at random from each of the input iterables.
-
-        >>> random_product('abc', range(4), 'XYZ')  # doctest:+SKIP
-        ('c', 3, 'Z')
-
-    If *repeat* is provided as a keyword argument, that many items will be
-    drawn from each iterable.
-
-        >>> random_product('abcd', range(4), repeat=2)  # doctest:+SKIP
-        ('a', 2, 'd', 3)
-
-    This equivalent to taking a random selection from
-    ``itertools.product(*args, **kwarg)``.
-
-    """
-    pools = [tuple(pool) for pool in args] * kwds.get('repeat', 1)
-    return tuple(choice(pool) for pool in pools)
-
-
-def random_permutation(iterable, r=None):
-    """Return a random *r* length permutation of the elements in *iterable*.
-
-    If *r* is not specified or is ``None``, then *r* defaults to the length of
-    *iterable*.
-
-        >>> random_permutation(range(5))  # doctest:+SKIP
-        (3, 4, 0, 1, 2)
-
-    This equivalent to taking a random selection from
-    ``itertools.permutations(iterable, r)``.
-
-    """
-    pool = tuple(iterable)
-    r = len(pool) if r is None else r
-    return tuple(sample(pool, r))
-
-
-def random_combination(iterable, r):
-    """Return a random *r* length subsequence of the elements in *iterable*.
-
-        >>> random_combination(range(5), 3)  # doctest:+SKIP
-        (2, 3, 4)
-
-    This equivalent to taking a random selection from
-    ``itertools.combinations(iterable, r)``.
-
-    """
-    pool = tuple(iterable)
-    n = len(pool)
-    indices = sorted(sample(range(n), r))
-    return tuple(pool[i] for i in indices)
-
-
-def random_combination_with_replacement(iterable, r):
-    """Return a random *r* length subsequence of elements in *iterable*,
-    allowing individual elements to be repeated.
-
-        >>> random_combination_with_replacement(range(3), 5) # doctest:+SKIP
-        (0, 0, 1, 2, 2)
-
-    This equivalent to taking a random selection from
-    ``itertools.combinations_with_replacement(iterable, r)``.
-
-    """
-    pool = tuple(iterable)
-    n = len(pool)
-    indices = sorted(randrange(n) for i in range(r))
-    return tuple(pool[i] for i in indices)
-
-
-def nth_combination(iterable, r, index):
-    """Equivalent to ``list(combinations(iterable, r))[index]``.
-
-    The subsequences of *iterable* that are of length *r* can be ordered
-    lexicographically. :func:`nth_combination` computes the subsequence at
-    sort position *index* directly, without computing the previous
-    subsequences.
-
-    """
-    pool = tuple(iterable)
-    n = len(pool)
-    if (r < 0) or (r > n):
-        raise ValueError
-
-    c = 1
-    k = min(r, n - r)
-    for i in range(1, k + 1):
-        c = c * (n - k + i) // i
-
-    if index < 0:
-        index += c
-
-    if (index < 0) or (index >= c):
-        raise IndexError
-
-    result = []
-    while r:
-        c, n, r = c * r // n, n - 1, r - 1
-        while index >= c:
-            index -= c
-            c, n = c * (n - r) // n, n - 1
-        result.append(pool[-1 - n])
-
-    return tuple(result)
-
-
-def prepend(value, iterator):
-    """Yield *value*, followed by the elements in *iterator*.
-
-        >>> value = '0'
-        >>> iterator = ['1', '2', '3']
-        >>> list(prepend(value, iterator))
-        ['0', '1', '2', '3']
-
-    To prepend multiple values, see :func:`itertools.chain`.
-
-    """
-    return chain([value], iterator)
diff --git a/libraries/more_itertools/tests/__init__.py b/libraries/more_itertools/tests/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/libraries/more_itertools/tests/test_more.py b/libraries/more_itertools/tests/test_more.py
deleted file mode 100644
index a1b1e431..00000000
--- a/libraries/more_itertools/tests/test_more.py
+++ /dev/null
@@ -1,2074 +0,0 @@
-from __future__ import division, print_function, unicode_literals
-
-from collections import OrderedDict
-from decimal import Decimal
-from doctest import DocTestSuite
-from fractions import Fraction
-from functools import partial, reduce
-from heapq import merge
-from io import StringIO
-from itertools import (
-    chain,
-    count,
-    groupby,
-    islice,
-    permutations,
-    product,
-    repeat,
-)
-from operator import add, mul, itemgetter
-from unittest import TestCase
-
-from six.moves import filter, map, range, zip
-
-import more_itertools as mi
-
-
-def load_tests(loader, tests, ignore):
-    # Add the doctests
-    tests.addTests(DocTestSuite('more_itertools.more'))
-    return tests
-
-
-class CollateTests(TestCase):
-    """Unit tests for ``collate()``"""
-    # Also accidentally tests peekable, though that could use its own tests
-
-    def test_default(self):
-        """Test with the default `key` function."""
-        iterables = [range(4), range(7), range(3, 6)]
-        self.assertEqual(
-            sorted(reduce(list.__add__, [list(it) for it in iterables])),
-            list(mi.collate(*iterables))
-        )
-
-    def test_key(self):
-        """Test using a custom `key` function."""
-        iterables = [range(5, 0, -1), range(4, 0, -1)]
-        actual = sorted(
-            reduce(list.__add__, [list(it) for it in iterables]), reverse=True
-        )
-        expected = list(mi.collate(*iterables, key=lambda x: -x))
-        self.assertEqual(actual, expected)
-
-    def test_empty(self):
-        """Be nice if passed an empty list of iterables."""
-        self.assertEqual([], list(mi.collate()))
-
-    def test_one(self):
-        """Work when only 1 iterable is passed."""
-        self.assertEqual([0, 1], list(mi.collate(range(2))))
-
-    def test_reverse(self):
-        """Test the `reverse` kwarg."""
-        iterables = [range(4, 0, -1), range(7, 0, -1), range(3, 6, -1)]
-
-        actual = sorted(
-            reduce(list.__add__, [list(it) for it in iterables]), reverse=True
-        )
-        expected = list(mi.collate(*iterables, reverse=True))
-        self.assertEqual(actual, expected)
-
-    def test_alias(self):
-        self.assertNotEqual(merge.__doc__, mi.collate.__doc__)
-        self.assertNotEqual(partial.__doc__, mi.collate.__doc__)
-
-
-class ChunkedTests(TestCase):
-    """Tests for ``chunked()``"""
-
-    def test_even(self):
-        """Test when ``n`` divides evenly into the length of the iterable."""
-        self.assertEqual(
-            list(mi.chunked('ABCDEF', 3)), [['A', 'B', 'C'], ['D', 'E', 'F']]
-        )
-
-    def test_odd(self):
-        """Test when ``n`` does not divide evenly into the length of the
-        iterable.
-
-        """
-        self.assertEqual(
-            list(mi.chunked('ABCDE', 3)), [['A', 'B', 'C'], ['D', 'E']]
-        )
-
-
-class FirstTests(TestCase):
-    """Tests for ``first()``"""
-
-    def test_many(self):
-        """Test that it works on many-item iterables."""
-        # Also try it on a generator expression to make sure it works on
-        # whatever those return, across Python versions.
-        self.assertEqual(mi.first(x for x in range(4)), 0)
-
-    def test_one(self):
-        """Test that it doesn't raise StopIteration prematurely."""
-        self.assertEqual(mi.first([3]), 3)
-
-    def test_empty_stop_iteration(self):
-        """It should raise StopIteration for empty iterables."""
-        self.assertRaises(ValueError, lambda: mi.first([]))
-
-    def test_default(self):
-        """It should return the provided default arg for empty iterables."""
-        self.assertEqual(mi.first([], 'boo'), 'boo')
-
-
-class IterOnlyRange:
-    """User-defined iterable class which only support __iter__.
-
-    It is not specified to inherit ``object``, so indexing on a instance will
-    raise an ``AttributeError`` rather than ``TypeError`` in Python 2.
-
-    >>> r = IterOnlyRange(5)
-    >>> r[0]
-    AttributeError: IterOnlyRange instance has no attribute '__getitem__'
-
-    Note: In Python 3, ``TypeError`` will be raised because ``object`` is
-    inherited implicitly by default.
-
-    >>> r[0]
-    TypeError: 'IterOnlyRange' object does not support indexing
-    """
-    def __init__(self, n):
-        """Set the length of the range."""
-        self.n = n
-
-    def __iter__(self):
-        """Works same as range()."""
-        return iter(range(self.n))
-
-
-class LastTests(TestCase):
-    """Tests for ``last()``"""
-
-    def test_many_nonsliceable(self):
-        """Test that it works on many-item non-slice-able iterables."""
-        # Also try it on a generator expression to make sure it works on
-        # whatever those return, across Python versions.
-        self.assertEqual(mi.last(x for x in range(4)), 3)
-
-    def test_one_nonsliceable(self):
-        """Test that it doesn't raise StopIteration prematurely."""
-        self.assertEqual(mi.last(x for x in range(1)), 0)
-
-    def test_empty_stop_iteration_nonsliceable(self):
-        """It should raise ValueError for empty non-slice-able iterables."""
-        self.assertRaises(ValueError, lambda: mi.last(x for x in range(0)))
-
-    def test_default_nonsliceable(self):
-        """It should return the provided default arg for empty non-slice-able
-        iterables.
-        """
-        self.assertEqual(mi.last((x for x in range(0)), 'boo'), 'boo')
-
-    def test_many_sliceable(self):
-        """Test that it works on many-item slice-able iterables."""
-        self.assertEqual(mi.last([0, 1, 2, 3]), 3)
-
-    def test_one_sliceable(self):
-        """Test that it doesn't raise StopIteration prematurely."""
-        self.assertEqual(mi.last([3]), 3)
-
-    def test_empty_stop_iteration_sliceable(self):
-        """It should raise ValueError for empty slice-able iterables."""
-        self.assertRaises(ValueError, lambda: mi.last([]))
-
-    def test_default_sliceable(self):
-        """It should return the provided default arg for empty slice-able
-        iterables.
-        """
-        self.assertEqual(mi.last([], 'boo'), 'boo')
-
-    def test_dict(self):
-        """last(dic) and last(dic.keys()) should return same result."""
-        dic = {'a': 1, 'b': 2, 'c': 3}
-        self.assertEqual(mi.last(dic), mi.last(dic.keys()))
-
-    def test_ordereddict(self):
-        """last(dic) should return the last key."""
-        od = OrderedDict()
-        od['a'] = 1
-        od['b'] = 2
-        od['c'] = 3
-        self.assertEqual(mi.last(od), 'c')
-
-    def test_customrange(self):
-        """It should work on custom class where [] raises AttributeError."""
-        self.assertEqual(mi.last(IterOnlyRange(5)), 4)
-
-
-class PeekableTests(TestCase):
-    """Tests for ``peekable()`` behavor not incidentally covered by testing
-    ``collate()``
-
-    """
-    def test_peek_default(self):
-        """Make sure passing a default into ``peek()`` works."""
-        p = mi.peekable([])
-        self.assertEqual(p.peek(7), 7)
-
-    def test_truthiness(self):
-        """Make sure a ``peekable`` tests true iff there are items remaining in
-        the iterable.
-
-        """
-        p = mi.peekable([])
-        self.assertFalse(p)
-
-        p = mi.peekable(range(3))
-        self.assertTrue(p)
-
-    def test_simple_peeking(self):
-        """Make sure ``next`` and ``peek`` advance and don't advance the
-        iterator, respectively.
-
-        """
-        p = mi.peekable(range(10))
-        self.assertEqual(next(p), 0)
-        self.assertEqual(p.peek(), 1)
-        self.assertEqual(next(p), 1)
-
-    def test_indexing(self):
-        """
-        Indexing into the peekable shouldn't advance the iterator.
-        """
-        p = mi.peekable('abcdefghijkl')
-
-        # The 0th index is what ``next()`` will return
-        self.assertEqual(p[0], 'a')
-        self.assertEqual(next(p), 'a')
-
-        # Indexing further into the peekable shouldn't advance the itertor
-        self.assertEqual(p[2], 'd')
-        self.assertEqual(next(p), 'b')
-
-        # The 0th index moves up with the iterator; the last index follows
-        self.assertEqual(p[0], 'c')
-        self.assertEqual(p[9], 'l')
-
-        self.assertEqual(next(p), 'c')
-        self.assertEqual(p[8], 'l')
-
-        # Negative indexing should work too
-        self.assertEqual(p[-2], 'k')
-        self.assertEqual(p[-9], 'd')
-        self.assertRaises(IndexError, lambda: p[-10])
-
-    def test_slicing(self):
-        """Slicing the peekable shouldn't advance the iterator."""
-        seq = list('abcdefghijkl')
-        p = mi.peekable(seq)
-
-        # Slicing the peekable should just be like slicing a re-iterable
-        self.assertEqual(p[1:4], seq[1:4])
-
-        # Advancing the iterator moves the slices up also
-        self.assertEqual(next(p), 'a')
-        self.assertEqual(p[1:4], seq[1:][1:4])
-
-        # Implicit starts and stop should work
-        self.assertEqual(p[:5], seq[1:][:5])
-        self.assertEqual(p[:], seq[1:][:])
-
-        # Indexing past the end should work
-        self.assertEqual(p[:100], seq[1:][:100])
-
-        # Steps should work, including negative
-        self.assertEqual(p[::2], seq[1:][::2])
-        self.assertEqual(p[::-1], seq[1:][::-1])
-
-    def test_slicing_reset(self):
-        """Test slicing on a fresh iterable each time"""
-        iterable = ['0', '1', '2', '3', '4', '5']
-        indexes = list(range(-4, len(iterable) + 4)) + [None]
-        steps = [1, 2, 3, 4, -1, -2, -3, 4]
-        for slice_args in product(indexes, indexes, steps):
-            it = iter(iterable)
-            p = mi.peekable(it)
-            next(p)
-            index = slice(*slice_args)
-            actual = p[index]
-            expected = iterable[1:][index]
-            self.assertEqual(actual, expected, slice_args)
-
-    def test_slicing_error(self):
-        iterable = '01234567'
-        p = mi.peekable(iter(iterable))
-
-        # Prime the cache
-        p.peek()
-        old_cache = list(p._cache)
-
-        # Illegal slice
-        with self.assertRaises(ValueError):
-            p[1:-1:0]
-
-        # Neither the cache nor the iteration should be affected
-        self.assertEqual(old_cache, list(p._cache))
-        self.assertEqual(list(p), list(iterable))
-
-    def test_passthrough(self):
-        """Iterating a peekable without using ``peek()`` or ``prepend()``
-        should just give the underlying iterable's elements (a trivial test but
-        useful to set a baseline in case something goes wrong)"""
-        expected = [1, 2, 3, 4, 5]
-        actual = list(mi.peekable(expected))
-        self.assertEqual(actual, expected)
-
-    # prepend() behavior tests
-
-    def test_prepend(self):
-        """Tests intersperesed ``prepend()`` and ``next()`` calls"""
-        it = mi.peekable(range(2))
-        actual = []
-
-        # Test prepend() before next()
-        it.prepend(10)
-        actual += [next(it), next(it)]
-
-        # Test prepend() between next()s
-        it.prepend(11)
-        actual += [next(it), next(it)]
-
-        # Test prepend() after source iterable is consumed
-        it.prepend(12)
-        actual += [next(it)]
-
-        expected = [10, 0, 11, 1, 12]
-        self.assertEqual(actual, expected)
-
-    def test_multi_prepend(self):
-        """Tests prepending multiple items and getting them in proper order"""
-        it = mi.peekable(range(5))
-        actual = [next(it), next(it)]
-        it.prepend(10, 11, 12)
-        it.prepend(20, 21)
-        actual += list(it)
-        expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4]
-        self.assertEqual(actual, expected)
-
-    def test_empty(self):
-        """Tests prepending in front of an empty iterable"""
-        it = mi.peekable([])
-        it.prepend(10)
-        actual = list(it)
-        expected = [10]
-        self.assertEqual(actual, expected)
-
-    def test_prepend_truthiness(self):
-        """Tests that ``__bool__()`` or ``__nonzero__()`` works properly
-        with ``prepend()``"""
-        it = mi.peekable(range(5))
-        self.assertTrue(it)
-        actual = list(it)
-        self.assertFalse(it)
-        it.prepend(10)
-        self.assertTrue(it)
-        actual += [next(it)]
-        self.assertFalse(it)
-        expected = [0, 1, 2, 3, 4, 10]
-        self.assertEqual(actual, expected)
-
-    def test_multi_prepend_peek(self):
-        """Tests prepending multiple elements and getting them in reverse order
-        while peeking"""
-        it = mi.peekable(range(5))
-        actual = [next(it), next(it)]
-        self.assertEqual(it.peek(), 2)
-        it.prepend(10, 11, 12)
-        self.assertEqual(it.peek(), 10)
-        it.prepend(20, 21)
-        self.assertEqual(it.peek(), 20)
-        actual += list(it)
-        self.assertFalse(it)
-        expected = [0, 1, 20, 21, 10, 11, 12, 2, 3, 4]
-        self.assertEqual(actual, expected)
-
-    def test_prepend_after_stop(self):
-        """Test resuming iteration after a previous exhaustion"""
-        it = mi.peekable(range(3))
-        self.assertEqual(list(it), [0, 1, 2])
-        self.assertRaises(StopIteration, lambda: next(it))
-        it.prepend(10)
-        self.assertEqual(next(it), 10)
-        self.assertRaises(StopIteration, lambda: next(it))
-
-    def test_prepend_slicing(self):
-        """Tests interaction between prepending and slicing"""
-        seq = list(range(20))
-        p = mi.peekable(seq)
-
-        p.prepend(30, 40, 50)
-        pseq = [30, 40, 50] + seq  # pseq for prepended_seq
-
-        # adapt the specific tests from test_slicing
-        self.assertEqual(p[0], 30)
-        self.assertEqual(p[1:8], pseq[1:8])
-        self.assertEqual(p[1:], pseq[1:])
-        self.assertEqual(p[:5], pseq[:5])
-        self.assertEqual(p[:], pseq[:])
-        self.assertEqual(p[:100], pseq[:100])
-        self.assertEqual(p[::2], pseq[::2])
-        self.assertEqual(p[::-1], pseq[::-1])
-
-    def test_prepend_indexing(self):
-        """Tests interaction between prepending and indexing"""
-        seq = list(range(20))
-        p = mi.peekable(seq)
-
-        p.prepend(30, 40, 50)
-
-        self.assertEqual(p[0], 30)
-        self.assertEqual(next(p), 30)
-        self.assertEqual(p[2], 0)
-        self.assertEqual(next(p), 40)
-        self.assertEqual(p[0], 50)
-        self.assertEqual(p[9], 8)
-        self.assertEqual(next(p), 50)
-        self.assertEqual(p[8], 8)
-        self.assertEqual(p[-2], 18)
-        self.assertEqual(p[-9], 11)
-        self.assertRaises(IndexError, lambda: p[-21])
-
-    def test_prepend_iterable(self):
-        """Tests prepending from an iterable"""
-        it = mi.peekable(range(5))
-        # Don't directly use the range() object to avoid any range-specific
-        # optimizations
-        it.prepend(*(x for x in range(5)))
-        actual = list(it)
-        expected = list(chain(range(5), range(5)))
-        self.assertEqual(actual, expected)
-
-    def test_prepend_many(self):
-        """Tests that prepending a huge number of elements works"""
-        it = mi.peekable(range(5))
-        # Don't directly use the range() object to avoid any range-specific
-        # optimizations
-        it.prepend(*(x for x in range(20000)))
-        actual = list(it)
-        expected = list(chain(range(20000), range(5)))
-        self.assertEqual(actual, expected)
-
-    def test_prepend_reversed(self):
-        """Tests prepending from a reversed iterable"""
-        it = mi.peekable(range(3))
-        it.prepend(*reversed((10, 11, 12)))
-        actual = list(it)
-        expected = [12, 11, 10, 0, 1, 2]
-        self.assertEqual(actual, expected)
-
-
-class ConsumerTests(TestCase):
-    """Tests for ``consumer()``"""
-
-    def test_consumer(self):
-        @mi.consumer
-        def eater():
-            while True:
-                x = yield  # noqa
-
-        e = eater()
-        e.send('hi')  # without @consumer, would raise TypeError
-
-
-class DistinctPermutationsTests(TestCase):
-    def test_distinct_permutations(self):
-        """Make sure the output for ``distinct_permutations()`` is the same as
-        set(permutations(it)).
-
-        """
-        iterable = ['z', 'a', 'a', 'q', 'q', 'q', 'y']
-        test_output = sorted(mi.distinct_permutations(iterable))
-        ref_output = sorted(set(permutations(iterable)))
-        self.assertEqual(test_output, ref_output)
-
-    def test_other_iterables(self):
-        """Make sure ``distinct_permutations()`` accepts a different type of
-        iterables.
-
-        """
-        # a generator
-        iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y'])
-        test_output = sorted(mi.distinct_permutations(iterable))
-        # "reload" it
-        iterable = (c for c in ['z', 'a', 'a', 'q', 'q', 'q', 'y'])
-        ref_output = sorted(set(permutations(iterable)))
-        self.assertEqual(test_output, ref_output)
-
-        # an iterator
-        iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y'])
-        test_output = sorted(mi.distinct_permutations(iterable))
-        # "reload" it
-        iterable = iter(['z', 'a', 'a', 'q', 'q', 'q', 'y'])
-        ref_output = sorted(set(permutations(iterable)))
-        self.assertEqual(test_output, ref_output)
-
-
-class IlenTests(TestCase):
-    def test_ilen(self):
-        """Sanity-checks for ``ilen()``."""
-        # Non-empty
-        self.assertEqual(
-            mi.ilen(filter(lambda x: x % 10 == 0, range(101))), 11
-        )
-
-        # Empty
-        self.assertEqual(mi.ilen((x for x in range(0))), 0)
-
-        # Iterable with __len__
-        self.assertEqual(mi.ilen(list(range(6))), 6)
-
-
-class WithIterTests(TestCase):
-    def test_with_iter(self):
-        s = StringIO('One fish\nTwo fish')
-        initial_words = [line.split()[0] for line in mi.with_iter(s)]
-
-        # Iterable's items should be faithfully represented
-        self.assertEqual(initial_words, ['One', 'Two'])
-        # The file object should be closed
-        self.assertEqual(s.closed, True)
-
-
-class OneTests(TestCase):
-    def test_basic(self):
-        it = iter(['item'])
-        self.assertEqual(mi.one(it), 'item')
-
-    def test_too_short(self):
-        it = iter([])
-        self.assertRaises(ValueError, lambda: mi.one(it))
-        self.assertRaises(IndexError, lambda: mi.one(it, too_short=IndexError))
-
-    def test_too_long(self):
-        it = count()
-        self.assertRaises(ValueError, lambda: mi.one(it))  # burn 0 and 1
-        self.assertEqual(next(it), 2)
-        self.assertRaises(
-            OverflowError, lambda: mi.one(it, too_long=OverflowError)
-        )
-
-
-class IntersperseTest(TestCase):
-    """ Tests for intersperse() """
-
-    def test_even(self):
-        iterable = (x for x in '01')
-        self.assertEqual(
-            list(mi.intersperse(None, iterable)), ['0', None, '1']
-        )
-
-    def test_odd(self):
-        iterable = (x for x in '012')
-        self.assertEqual(
-            list(mi.intersperse(None, iterable)), ['0', None, '1', None, '2']
-        )
-
-    def test_nested(self):
-        element = ('a', 'b')
-        iterable = (x for x in '012')
-        actual = list(mi.intersperse(element, iterable))
-        expected = ['0', ('a', 'b'), '1', ('a', 'b'), '2']
-        self.assertEqual(actual, expected)
-
-    def test_not_iterable(self):
-        self.assertRaises(TypeError, lambda: mi.intersperse('x', 1))
-
-    def test_n(self):
-        for n, element, expected in [
-            (1, '_', ['0', '_', '1', '_', '2', '_', '3', '_', '4', '_', '5']),
-            (2, '_', ['0', '1', '_', '2', '3', '_', '4', '5']),
-            (3, '_', ['0', '1', '2', '_', '3', '4', '5']),
-            (4, '_', ['0', '1', '2', '3', '_', '4', '5']),
-            (5, '_', ['0', '1', '2', '3', '4', '_', '5']),
-            (6, '_', ['0', '1', '2', '3', '4', '5']),
-            (7, '_', ['0', '1', '2', '3', '4', '5']),
-            (3, ['a', 'b'], ['0', '1', '2', ['a', 'b'], '3', '4', '5']),
-        ]:
-            iterable = (x for x in '012345')
-            actual = list(mi.intersperse(element, iterable, n=n))
-            self.assertEqual(actual, expected)
-
-    def test_n_zero(self):
-        self.assertRaises(
-            ValueError, lambda: list(mi.intersperse('x', '012', n=0))
-        )
-
-
-class UniqueToEachTests(TestCase):
-    """Tests for ``unique_to_each()``"""
-
-    def test_all_unique(self):
-        """When all the input iterables are unique the output should match
-        the input."""
-        iterables = [[1, 2], [3, 4, 5], [6, 7, 8]]
-        self.assertEqual(mi.unique_to_each(*iterables), iterables)
-
-    def test_duplicates(self):
-        """When there are duplicates in any of the input iterables that aren't
-        in the rest, those duplicates should be emitted."""
-        iterables = ["mississippi", "missouri"]
-        self.assertEqual(
-            mi.unique_to_each(*iterables), [['p', 'p'], ['o', 'u', 'r']]
-        )
-
-    def test_mixed(self):
-        """When the input iterables contain different types the function should
-        still behave properly"""
-        iterables = ['x', (i for i in range(3)), [1, 2, 3], tuple()]
-        self.assertEqual(mi.unique_to_each(*iterables), [['x'], [0], [3], []])
-
-
-class WindowedTests(TestCase):
-    """Tests for ``windowed()``"""
-
-    def test_basic(self):
-        actual = list(mi.windowed([1, 2, 3, 4, 5], 3))
-        expected = [(1, 2, 3), (2, 3, 4), (3, 4, 5)]
-        self.assertEqual(actual, expected)
-
-    def test_large_size(self):
-        """
-        When the window size is larger than the iterable, and no fill value is
-        given,``None`` should be filled in.
-        """
-        actual = list(mi.windowed([1, 2, 3, 4, 5], 6))
-        expected = [(1, 2, 3, 4, 5, None)]
-        self.assertEqual(actual, expected)
-
-    def test_fillvalue(self):
-        """
-        When sizes don't match evenly, the given fill value should be used.
-        """
-        iterable = [1, 2, 3, 4, 5]
-
-        for n, kwargs, expected in [
-            (6, {}, [(1, 2, 3, 4, 5, '!')]),  # n > len(iterable)
-            (3, {'step': 3}, [(1, 2, 3), (4, 5, '!')]),  # using ``step``
-        ]:
-            actual = list(mi.windowed(iterable, n, fillvalue='!', **kwargs))
-            self.assertEqual(actual, expected)
-
-    def test_zero(self):
-        """When the window size is zero, an empty tuple should be emitted."""
-        actual = list(mi.windowed([1, 2, 3, 4, 5], 0))
-        expected = [tuple()]
-        self.assertEqual(actual, expected)
-
-    def test_negative(self):
-        """When the window size is negative, ValueError should be raised."""
-        with self.assertRaises(ValueError):
-            list(mi.windowed([1, 2, 3, 4, 5], -1))
-
-    def test_step(self):
-        """The window should advance by the number of steps provided"""
-        iterable = [1, 2, 3, 4, 5, 6, 7]
-        for n, step, expected in [
-            (3, 2, [(1, 2, 3), (3, 4, 5), (5, 6, 7)]),  # n > step
-            (3, 3, [(1, 2, 3), (4, 5, 6), (7, None, None)]),  # n == step
-            (3, 4, [(1, 2, 3), (5, 6, 7)]),  # line up nicely
-            (3, 5, [(1, 2, 3), (6, 7, None)]),  # off by one
-            (3, 6, [(1, 2, 3), (7, None, None)]),  # off by two
-            (3, 7, [(1, 2, 3)]),  # step past the end
-            (7, 8, [(1, 2, 3, 4, 5, 6, 7)]),  # step > len(iterable)
-        ]:
-            actual = list(mi.windowed(iterable, n, step=step))
-            self.assertEqual(actual, expected)
-
-        # Step must be greater than or equal to 1
-        with self.assertRaises(ValueError):
-            list(mi.windowed(iterable, 3, step=0))
-
-
-class BucketTests(TestCase):
-    """Tests for ``bucket()``"""
-
-    def test_basic(self):
-        iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33]
-        D = mi.bucket(iterable, key=lambda x: 10 * (x // 10))
-
-        # In-order access
-        self.assertEqual(list(D[10]), [10, 11, 12])
-
-        # Out of order access
-        self.assertEqual(list(D[30]), [30, 31, 33])
-        self.assertEqual(list(D[20]), [20, 21, 22, 23])
-
-        self.assertEqual(list(D[40]), [])  # Nothing in here!
-
-    def test_in(self):
-        iterable = [10, 20, 30, 11, 21, 31, 12, 22, 23, 33]
-        D = mi.bucket(iterable, key=lambda x: 10 * (x // 10))
-
-        self.assertTrue(10 in D)
-        self.assertFalse(40 in D)
-        self.assertTrue(20 in D)
-        self.assertFalse(21 in D)
-
-        # Checking in-ness shouldn't advance the iterator
-        self.assertEqual(next(D[10]), 10)
-
-    def test_validator(self):
-        iterable = count(0)
-        key = lambda x: int(str(x)[0])  # First digit of each number
-        validator = lambda x: 0 < x < 10  # No leading zeros
-        D = mi.bucket(iterable, key, validator=validator)
-        self.assertEqual(mi.take(3, D[1]), [1, 10, 11])
-        self.assertNotIn(0, D)  # Non-valid entries don't return True
-        self.assertNotIn(0, D._cache)  # Don't store non-valid entries
-        self.assertEqual(list(D[0]), [])
-
-
-class SpyTests(TestCase):
-    """Tests for ``spy()``"""
-
-    def test_basic(self):
-        original_iterable = iter('abcdefg')
-        head, new_iterable = mi.spy(original_iterable)
-        self.assertEqual(head, ['a'])
-        self.assertEqual(
-            list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g']
-        )
-
-    def test_unpacking(self):
-        original_iterable = iter('abcdefg')
-        (first, second, third), new_iterable = mi.spy(original_iterable, 3)
-        self.assertEqual(first, 'a')
-        self.assertEqual(second, 'b')
-        self.assertEqual(third, 'c')
-        self.assertEqual(
-            list(new_iterable), ['a', 'b', 'c', 'd', 'e', 'f', 'g']
-        )
-
-    def test_too_many(self):
-        original_iterable = iter('abc')
-        head, new_iterable = mi.spy(original_iterable, 4)
-        self.assertEqual(head, ['a', 'b', 'c'])
-        self.assertEqual(list(new_iterable), ['a', 'b', 'c'])
-
-    def test_zero(self):
-        original_iterable = iter('abc')
-        head, new_iterable = mi.spy(original_iterable, 0)
-        self.assertEqual(head, [])
-        self.assertEqual(list(new_iterable), ['a', 'b', 'c'])
-
-
-class InterleaveTests(TestCase):
-    def test_even(self):
-        actual = list(mi.interleave([1, 4, 7], [2, 5, 8], [3, 6, 9]))
-        expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        self.assertEqual(actual, expected)
-
-    def test_short(self):
-        actual = list(mi.interleave([1, 4], [2, 5, 7], [3, 6, 8]))
-        expected = [1, 2, 3, 4, 5, 6]
-        self.assertEqual(actual, expected)
-
-    def test_mixed_types(self):
-        it_list = ['a', 'b', 'c', 'd']
-        it_str = '12345'
-        it_inf = count()
-        actual = list(mi.interleave(it_list, it_str, it_inf))
-        expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', 3]
-        self.assertEqual(actual, expected)
-
-
-class InterleaveLongestTests(TestCase):
-    def test_even(self):
-        actual = list(mi.interleave_longest([1, 4, 7], [2, 5, 8], [3, 6, 9]))
-        expected = [1, 2, 3, 4, 5, 6, 7, 8, 9]
-        self.assertEqual(actual, expected)
-
-    def test_short(self):
-        actual = list(mi.interleave_longest([1, 4], [2, 5, 7], [3, 6, 8]))
-        expected = [1, 2, 3, 4, 5, 6, 7, 8]
-        self.assertEqual(actual, expected)
-
-    def test_mixed_types(self):
-        it_list = ['a', 'b', 'c', 'd']
-        it_str = '12345'
-        it_gen = (x for x in range(3))
-        actual = list(mi.interleave_longest(it_list, it_str, it_gen))
-        expected = ['a', '1', 0, 'b', '2', 1, 'c', '3', 2, 'd', '4', '5']
-        self.assertEqual(actual, expected)
-
-
-class TestCollapse(TestCase):
-    """Tests for ``collapse()``"""
-
-    def test_collapse(self):
-        l = [[1], 2, [[3], 4], [[[5]]]]
-        self.assertEqual(list(mi.collapse(l)), [1, 2, 3, 4, 5])
-
-    def test_collapse_to_string(self):
-        l = [["s1"], "s2", [["s3"], "s4"], [[["s5"]]]]
-        self.assertEqual(list(mi.collapse(l)), ["s1", "s2", "s3", "s4", "s5"])
-
-    def test_collapse_flatten(self):
-        l = [[1], [2], [[3], 4], [[[5]]]]
-        self.assertEqual(list(mi.collapse(l, levels=1)), list(mi.flatten(l)))
-
-    def test_collapse_to_level(self):
-        l = [[1], 2, [[3], 4], [[[5]]]]
-        self.assertEqual(list(mi.collapse(l, levels=2)), [1, 2, 3, 4, [5]])
-        self.assertEqual(
-            list(mi.collapse(mi.collapse(l, levels=1), levels=1)),
-            list(mi.collapse(l, levels=2))
-        )
-
-    def test_collapse_to_list(self):
-        l = (1, [2], (3, [4, (5,)], 'ab'))
-        actual = list(mi.collapse(l, base_type=list))
-        expected = [1, [2], 3, [4, (5,)], 'ab']
-        self.assertEqual(actual, expected)
-
-
-class SideEffectTests(TestCase):
-    """Tests for ``side_effect()``"""
-
-    def test_individual(self):
-        # The function increments the counter for each call
-        counter = [0]
-
-        def func(arg):
-            counter[0] += 1
-
-        result = list(mi.side_effect(func, range(10)))
-        self.assertEqual(result, list(range(10)))
-        self.assertEqual(counter[0], 10)
-
-    def test_chunked(self):
-        # The function increments the counter for each call
-        counter = [0]
-
-        def func(arg):
-            counter[0] += 1
-
-        result = list(mi.side_effect(func, range(10), 2))
-        self.assertEqual(result, list(range(10)))
-        self.assertEqual(counter[0], 5)
-
-    def test_before_after(self):
-        f = StringIO()
-        collector = []
-
-        def func(item):
-            print(item, file=f)
-            collector.append(f.getvalue())
-
-        def it():
-            yield u'a'
-            yield u'b'
-            raise RuntimeError('kaboom')
-
-        before = lambda: print('HEADER', file=f)
-        after = f.close
-
-        try:
-            mi.consume(mi.side_effect(func, it(), before=before, after=after))
-        except RuntimeError:
-            pass
-
-        # The iterable should have been written to the file
-        self.assertEqual(collector, [u'HEADER\na\n', u'HEADER\na\nb\n'])
-
-        # The file should be closed even though something bad happened
-        self.assertTrue(f.closed)
-
-    def test_before_fails(self):
-        f = StringIO()
-        func = lambda x: print(x, file=f)
-
-        def before():
-            raise RuntimeError('ouch')
-
-        try:
-            mi.consume(
-                mi.side_effect(func, u'abc', before=before, after=f.close)
-            )
-        except RuntimeError:
-            pass
-
-        # The file should be closed even though something bad happened in the
-        # before function
-        self.assertTrue(f.closed)
-
-
-class SlicedTests(TestCase):
-    """Tests for ``sliced()``"""
-
-    def test_even(self):
-        """Test when the length of the sequence is divisible by *n*"""
-        seq = 'ABCDEFGHI'
-        self.assertEqual(list(mi.sliced(seq, 3)), ['ABC', 'DEF', 'GHI'])
-
-    def test_odd(self):
-        """Test when the length of the sequence is not divisible by *n*"""
-        seq = 'ABCDEFGHI'
-        self.assertEqual(list(mi.sliced(seq, 4)), ['ABCD', 'EFGH', 'I'])
-
-    def test_not_sliceable(self):
-        seq = (x for x in 'ABCDEFGHI')
-
-        with self.assertRaises(TypeError):
-            list(mi.sliced(seq, 3))
-
-
-class SplitAtTests(TestCase):
-    """Tests for ``split()``"""
-
-    def comp_with_str_split(self, str_to_split, delim):
-        pred = lambda c: c == delim
-        actual = list(map(''.join, mi.split_at(str_to_split, pred)))
-        expected = str_to_split.split(delim)
-        self.assertEqual(actual, expected)
-
-    def test_seperators(self):
-        test_strs = ['', 'abcba', 'aaabbbcccddd', 'e']
-        for s, delim in product(test_strs, 'abcd'):
-            self.comp_with_str_split(s, delim)
-
-
-class SplitBeforeTest(TestCase):
-    """Tests for ``split_before()``"""
-
-    def test_starts_with_sep(self):
-        actual = list(mi.split_before('xooxoo', lambda c: c == 'x'))
-        expected = [['x', 'o', 'o'], ['x', 'o', 'o']]
-        self.assertEqual(actual, expected)
-
-    def test_ends_with_sep(self):
-        actual = list(mi.split_before('ooxoox', lambda c: c == 'x'))
-        expected = [['o', 'o'], ['x', 'o', 'o'], ['x']]
-        self.assertEqual(actual, expected)
-
-    def test_no_sep(self):
-        actual = list(mi.split_before('ooo', lambda c: c == 'x'))
-        expected = [['o', 'o', 'o']]
-        self.assertEqual(actual, expected)
-
-
-class SplitAfterTest(TestCase):
-    """Tests for ``split_after()``"""
-
-    def test_starts_with_sep(self):
-        actual = list(mi.split_after('xooxoo', lambda c: c == 'x'))
-        expected = [['x'], ['o', 'o', 'x'], ['o', 'o']]
-        self.assertEqual(actual, expected)
-
-    def test_ends_with_sep(self):
-        actual = list(mi.split_after('ooxoox', lambda c: c == 'x'))
-        expected = [['o', 'o', 'x'], ['o', 'o', 'x']]
-        self.assertEqual(actual, expected)
-
-    def test_no_sep(self):
-        actual = list(mi.split_after('ooo', lambda c: c == 'x'))
-        expected = [['o', 'o', 'o']]
-        self.assertEqual(actual, expected)
-
-
-class PaddedTest(TestCase):
-    """Tests for ``padded()``"""
-
-    def test_no_n(self):
-        seq = [1, 2, 3]
-
-        # No fillvalue
-        self.assertEqual(mi.take(5, mi.padded(seq)), [1, 2, 3, None, None])
-
-        # With fillvalue
-        self.assertEqual(
-            mi.take(5, mi.padded(seq, fillvalue='')), [1, 2, 3, '', '']
-        )
-
-    def test_invalid_n(self):
-        self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=-1)))
-        self.assertRaises(ValueError, lambda: list(mi.padded([1, 2, 3], n=0)))
-
-    def test_valid_n(self):
-        seq = [1, 2, 3, 4, 5]
-
-        # No need for padding: len(seq) <= n
-        self.assertEqual(list(mi.padded(seq, n=4)), [1, 2, 3, 4, 5])
-        self.assertEqual(list(mi.padded(seq, n=5)), [1, 2, 3, 4, 5])
-
-        # No fillvalue
-        self.assertEqual(
-            list(mi.padded(seq, n=7)), [1, 2, 3, 4, 5, None, None]
-        )
-
-        # With fillvalue
-        self.assertEqual(
-            list(mi.padded(seq, fillvalue='', n=7)), [1, 2, 3, 4, 5, '', '']
-        )
-
-    def test_next_multiple(self):
-        seq = [1, 2, 3, 4, 5, 6]
-
-        # No need for padding: len(seq) % n == 0
-        self.assertEqual(
-            list(mi.padded(seq, n=3, next_multiple=True)), [1, 2, 3, 4, 5, 6]
-        )
-
-        # Padding needed: len(seq) < n
-        self.assertEqual(
-            list(mi.padded(seq, n=8, next_multiple=True)),
-            [1, 2, 3, 4, 5, 6, None, None]
-        )
-
-        # No padding needed: len(seq) == n
-        self.assertEqual(
-            list(mi.padded(seq, n=6, next_multiple=True)), [1, 2, 3, 4, 5, 6]
-        )
-
-        # Padding needed: len(seq) > n
-        self.assertEqual(
-            list(mi.padded(seq, n=4, next_multiple=True)),
-            [1, 2, 3, 4, 5, 6, None, None]
-        )
-
-        # With fillvalue
-        self.assertEqual(
-            list(mi.padded(seq, fillvalue='', n=4, next_multiple=True)),
-            [1, 2, 3, 4, 5, 6, '', '']
-        )
-
-
-class DistributeTest(TestCase):
-    """Tests for distribute()"""
-
-    def test_invalid_n(self):
-        self.assertRaises(ValueError, lambda: mi.distribute(-1, [1, 2, 3]))
-        self.assertRaises(ValueError, lambda: mi.distribute(0, [1, 2, 3]))
-
-    def test_basic(self):
-        iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-
-        for n, expected in [
-            (1, [iterable]),
-            (2, [[1, 3, 5, 7, 9], [2, 4, 6, 8, 10]]),
-            (3, [[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]),
-            (10, [[n] for n in range(1, 10 + 1)]),
-        ]:
-            self.assertEqual(
-                [list(x) for x in mi.distribute(n, iterable)], expected
-            )
-
-    def test_large_n(self):
-        iterable = [1, 2, 3, 4]
-        self.assertEqual(
-            [list(x) for x in mi.distribute(6, iterable)],
-            [[1], [2], [3], [4], [], []]
-        )
-
-
-class StaggerTest(TestCase):
-    """Tests for ``stagger()``"""
-
-    def test_default(self):
-        iterable = [0, 1, 2, 3]
-        actual = list(mi.stagger(iterable))
-        expected = [(None, 0, 1), (0, 1, 2), (1, 2, 3)]
-        self.assertEqual(actual, expected)
-
-    def test_offsets(self):
-        iterable = [0, 1, 2, 3]
-        for offsets, expected in [
-            ((-2, 0, 2), [('', 0, 2), ('', 1, 3)]),
-            ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3)]),
-            ((1, 2), [(1, 2), (2, 3)]),
-        ]:
-            all_groups = mi.stagger(iterable, offsets=offsets, fillvalue='')
-            self.assertEqual(list(all_groups), expected)
-
-    def test_longest(self):
-        iterable = [0, 1, 2, 3]
-        for offsets, expected in [
-            (
-                (-1, 0, 1),
-                [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, ''), (3, '', '')]
-            ),
-            ((-2, -1), [('', ''), ('', 0), (0, 1), (1, 2), (2, 3), (3, '')]),
-            ((1, 2), [(1, 2), (2, 3), (3, '')]),
-        ]:
-            all_groups = mi.stagger(
-                iterable, offsets=offsets, fillvalue='', longest=True
-            )
-            self.assertEqual(list(all_groups), expected)
-
-
-class ZipOffsetTest(TestCase):
-    """Tests for ``zip_offset()``"""
-
-    def test_shortest(self):
-        a_1 = [0, 1, 2, 3]
-        a_2 = [0, 1, 2, 3, 4, 5]
-        a_3 = [0, 1, 2, 3, 4, 5, 6, 7]
-        actual = list(
-            mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), fillvalue='')
-        )
-        expected = [('', 0, 1), (0, 1, 2), (1, 2, 3), (2, 3, 4), (3, 4, 5)]
-        self.assertEqual(actual, expected)
-
-    def test_longest(self):
-        a_1 = [0, 1, 2, 3]
-        a_2 = [0, 1, 2, 3, 4, 5]
-        a_3 = [0, 1, 2, 3, 4, 5, 6, 7]
-        actual = list(
-            mi.zip_offset(a_1, a_2, a_3, offsets=(-1, 0, 1), longest=True)
-        )
-        expected = [
-            (None, 0, 1),
-            (0, 1, 2),
-            (1, 2, 3),
-            (2, 3, 4),
-            (3, 4, 5),
-            (None, 5, 6),
-            (None, None, 7),
-        ]
-        self.assertEqual(actual, expected)
-
-    def test_mismatch(self):
-        iterables = [0, 1, 2], [2, 3, 4]
-        offsets = (-1, 0, 1)
-        self.assertRaises(
-            ValueError,
-            lambda: list(mi.zip_offset(*iterables, offsets=offsets))
-        )
-
-
-class SortTogetherTest(TestCase):
-    """Tests for sort_together()"""
-
-    def test_key_list(self):
-        """tests `key_list` including default, iterables include duplicates"""
-        iterables = [
-            ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
-            ['May', 'Aug.', 'May', 'June', 'July', 'July'],
-            [97, 20, 100, 70, 100, 20]
-        ]
-
-        self.assertEqual(
-            mi.sort_together(iterables),
-            [
-                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
-                ('June', 'July', 'July', 'May', 'Aug.', 'May'),
-                (70, 100, 20, 97, 20, 100)
-            ]
-        )
-
-        self.assertEqual(
-            mi.sort_together(iterables, key_list=(0, 1)),
-            [
-                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
-                ('July', 'July', 'June', 'Aug.', 'May', 'May'),
-                (100, 20, 70, 20, 97, 100)
-            ]
-        )
-
-        self.assertEqual(
-            mi.sort_together(iterables, key_list=(0, 1, 2)),
-            [
-                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
-                ('July', 'July', 'June', 'Aug.', 'May', 'May'),
-                (20, 100, 70, 20, 97, 100)
-            ]
-        )
-
-        self.assertEqual(
-            mi.sort_together(iterables, key_list=(2,)),
-            [
-                ('GA', 'CT', 'CT', 'GA', 'GA', 'CT'),
-                ('Aug.', 'July', 'June', 'May', 'May', 'July'),
-                (20, 20, 70, 97, 100, 100)
-            ]
-        )
-
-    def test_invalid_key_list(self):
-        """tests `key_list` for indexes not available in `iterables`"""
-        iterables = [
-            ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
-            ['May', 'Aug.', 'May', 'June', 'July', 'July'],
-            [97, 20, 100, 70, 100, 20]
-        ]
-
-        self.assertRaises(
-            IndexError, lambda: mi.sort_together(iterables, key_list=(5,))
-        )
-
-    def test_reverse(self):
-        """tests `reverse` to ensure a reverse sort for `key_list` iterables"""
-        iterables = [
-            ['GA', 'GA', 'GA', 'CT', 'CT', 'CT'],
-            ['May', 'Aug.', 'May', 'June', 'July', 'July'],
-            [97, 20, 100, 70, 100, 20]
-        ]
-
-        self.assertEqual(
-            mi.sort_together(iterables, key_list=(0, 1, 2), reverse=True),
-            [('GA', 'GA', 'GA', 'CT', 'CT', 'CT'),
-             ('May', 'May', 'Aug.', 'June', 'July', 'July'),
-             (100, 97, 20, 70, 100, 20)]
-        )
-
-    def test_uneven_iterables(self):
-        """tests trimming of iterables to the shortest length before sorting"""
-        iterables = [['GA', 'GA', 'GA', 'CT', 'CT', 'CT', 'MA'],
-                     ['May', 'Aug.', 'May', 'June', 'July', 'July'],
-                     [97, 20, 100, 70, 100, 20, 0]]
-
-        self.assertEqual(
-            mi.sort_together(iterables),
-            [
-                ('CT', 'CT', 'CT', 'GA', 'GA', 'GA'),
-                ('June', 'July', 'July', 'May', 'Aug.', 'May'),
-                (70, 100, 20, 97, 20, 100)
-            ]
-        )
-
-
-class DivideTest(TestCase):
-    """Tests for divide()"""
-
-    def test_invalid_n(self):
-        self.assertRaises(ValueError, lambda: mi.divide(-1, [1, 2, 3]))
-        self.assertRaises(ValueError, lambda: mi.divide(0, [1, 2, 3]))
-
-    def test_basic(self):
-        iterable = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
-
-        for n, expected in [
-            (1, [iterable]),
-            (2, [[1, 2, 3, 4, 5], [6, 7, 8, 9, 10]]),
-            (3, [[1, 2, 3, 4], [5, 6, 7], [8, 9, 10]]),
-            (10, [[n] for n in range(1, 10 + 1)]),
-        ]:
-            self.assertEqual(
-                [list(x) for x in mi.divide(n, iterable)], expected
-            )
-
-    def test_large_n(self):
-        iterable = [1, 2, 3, 4]
-        self.assertEqual(
-            [list(x) for x in mi.divide(6, iterable)],
-            [[1], [2], [3], [4], [], []]
-        )
-
-
-class TestAlwaysIterable(TestCase):
-    """Tests for always_iterable()"""
-    def test_single(self):
-        self.assertEqual(list(mi.always_iterable(1)), [1])
-
-    def test_strings(self):
-        for obj in ['foo', b'bar', u'baz']:
-            actual = list(mi.always_iterable(obj))
-            expected = [obj]
-            self.assertEqual(actual, expected)
-
-    def test_base_type(self):
-        dict_obj = {'a': 1, 'b': 2}
-        str_obj = '123'
-
-        # Default: dicts are iterable like they normally are
-        default_actual = list(mi.always_iterable(dict_obj))
-        default_expected = list(dict_obj)
-        self.assertEqual(default_actual, default_expected)
-
-        # Unitary types set: dicts are not iterable
-        custom_actual = list(mi.always_iterable(dict_obj, base_type=dict))
-        custom_expected = [dict_obj]
-        self.assertEqual(custom_actual, custom_expected)
-
-        # With unitary types set, strings are iterable
-        str_actual = list(mi.always_iterable(str_obj, base_type=None))
-        str_expected = list(str_obj)
-        self.assertEqual(str_actual, str_expected)
-
-    def test_iterables(self):
-        self.assertEqual(list(mi.always_iterable([0, 1])), [0, 1])
-        self.assertEqual(
-            list(mi.always_iterable([0, 1], base_type=list)), [[0, 1]]
-        )
-        self.assertEqual(
-            list(mi.always_iterable(iter('foo'))), ['f', 'o', 'o']
-        )
-        self.assertEqual(list(mi.always_iterable([])), [])
-
-    def test_none(self):
-        self.assertEqual(list(mi.always_iterable(None)), [])
-
-    def test_generator(self):
-        def _gen():
-            yield 0
-            yield 1
-
-        self.assertEqual(list(mi.always_iterable(_gen())), [0, 1])
-
-
-class AdjacentTests(TestCase):
-    def test_typical(self):
-        actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10)))
-        expected = [(True, 0), (True, 1), (False, 2), (False, 3), (True, 4),
-                    (True, 5), (True, 6), (False, 7), (False, 8), (False, 9)]
-        self.assertEqual(actual, expected)
-
-    def test_empty_iterable(self):
-        actual = list(mi.adjacent(lambda x: x % 5 == 0, []))
-        expected = []
-        self.assertEqual(actual, expected)
-
-    def test_length_one(self):
-        actual = list(mi.adjacent(lambda x: x % 5 == 0, [0]))
-        expected = [(True, 0)]
-        self.assertEqual(actual, expected)
-
-        actual = list(mi.adjacent(lambda x: x % 5 == 0, [1]))
-        expected = [(False, 1)]
-        self.assertEqual(actual, expected)
-
-    def test_consecutive_true(self):
-        """Test that when the predicate matches multiple consecutive elements
-        it doesn't repeat elements in the output"""
-        actual = list(mi.adjacent(lambda x: x % 5 < 2, range(10)))
-        expected = [(True, 0), (True, 1), (True, 2), (False, 3), (True, 4),
-                    (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)]
-        self.assertEqual(actual, expected)
-
-    def test_distance(self):
-        actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=2))
-        expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4),
-                    (True, 5), (True, 6), (True, 7), (False, 8), (False, 9)]
-        self.assertEqual(actual, expected)
-
-        actual = list(mi.adjacent(lambda x: x % 5 == 0, range(10), distance=3))
-        expected = [(True, 0), (True, 1), (True, 2), (True, 3), (True, 4),
-                    (True, 5), (True, 6), (True, 7), (True, 8), (False, 9)]
-        self.assertEqual(actual, expected)
-
-    def test_large_distance(self):
-        """Test distance larger than the length of the iterable"""
-        iterable = range(10)
-        actual = list(mi.adjacent(lambda x: x % 5 == 4, iterable, distance=20))
-        expected = list(zip(repeat(True), iterable))
-        self.assertEqual(actual, expected)
-
-        actual = list(mi.adjacent(lambda x: False, iterable, distance=20))
-        expected = list(zip(repeat(False), iterable))
-        self.assertEqual(actual, expected)
-
-    def test_zero_distance(self):
-        """Test that adjacent() reduces to zip+map when distance is 0"""
-        iterable = range(1000)
-        predicate = lambda x: x % 4 == 2
-        actual = mi.adjacent(predicate, iterable, 0)
-        expected = zip(map(predicate, iterable), iterable)
-        self.assertTrue(all(a == e for a, e in zip(actual, expected)))
-
-    def test_negative_distance(self):
-        """Test that adjacent() raises an error with negative distance"""
-        pred = lambda x: x
-        self.assertRaises(
-            ValueError, lambda: mi.adjacent(pred, range(1000), -1)
-        )
-        self.assertRaises(
-            ValueError, lambda: mi.adjacent(pred, range(10), -10)
-        )
-
-    def test_grouping(self):
-        """Test interaction of adjacent() with groupby_transform()"""
-        iterable = mi.adjacent(lambda x: x % 5 == 0, range(10))
-        grouper = mi.groupby_transform(iterable, itemgetter(0), itemgetter(1))
-        actual = [(k, list(g)) for k, g in grouper]
-        expected = [
-            (True, [0, 1]),
-            (False, [2, 3]),
-            (True, [4, 5, 6]),
-            (False, [7, 8, 9]),
-        ]
-        self.assertEqual(actual, expected)
-
-    def test_call_once(self):
-        """Test that the predicate is only called once per item."""
-        already_seen = set()
-        iterable = range(10)
-
-        def predicate(item):
-            self.assertNotIn(item, already_seen)
-            already_seen.add(item)
-            return True
-
-        actual = list(mi.adjacent(predicate, iterable))
-        expected = [(True, x) for x in iterable]
-        self.assertEqual(actual, expected)
-
-
-class GroupByTransformTests(TestCase):
-    def assertAllGroupsEqual(self, groupby1, groupby2):
-        """Compare two groupby objects for equality, both keys and groups."""
-        for a, b in zip(groupby1, groupby2):
-            key1, group1 = a
-            key2, group2 = b
-            self.assertEqual(key1, key2)
-            self.assertListEqual(list(group1), list(group2))
-        self.assertRaises(StopIteration, lambda: next(groupby1))
-        self.assertRaises(StopIteration, lambda: next(groupby2))
-
-    def test_default_funcs(self):
-        """Test that groupby_transform() with default args mimics groupby()"""
-        iterable = [(x // 5, x) for x in range(1000)]
-        actual = mi.groupby_transform(iterable)
-        expected = groupby(iterable)
-        self.assertAllGroupsEqual(actual, expected)
-
-    def test_valuefunc(self):
-        iterable = [(int(x / 5), int(x / 3), x) for x in range(10)]
-
-        # Test the standard usage of grouping one iterable using another's keys
-        grouper = mi.groupby_transform(
-            iterable, keyfunc=itemgetter(0), valuefunc=itemgetter(-1)
-        )
-        actual = [(k, list(g)) for k, g in grouper]
-        expected = [(0, [0, 1, 2, 3, 4]), (1, [5, 6, 7, 8, 9])]
-        self.assertEqual(actual, expected)
-
-        grouper = mi.groupby_transform(
-            iterable, keyfunc=itemgetter(1), valuefunc=itemgetter(-1)
-        )
-        actual = [(k, list(g)) for k, g in grouper]
-        expected = [(0, [0, 1, 2]), (1, [3, 4, 5]), (2, [6, 7, 8]), (3, [9])]
-        self.assertEqual(actual, expected)
-
-        # and now for something a little different
-        d = dict(zip(range(10), 'abcdefghij'))
-        grouper = mi.groupby_transform(
-            range(10), keyfunc=lambda x: x // 5, valuefunc=d.get
-        )
-        actual = [(k, ''.join(g)) for k, g in grouper]
-        expected = [(0, 'abcde'), (1, 'fghij')]
-        self.assertEqual(actual, expected)
-
-    def test_no_valuefunc(self):
-        iterable = range(1000)
-
-        def key(x):
-            return x // 5
-
-        actual = mi.groupby_transform(iterable, key, valuefunc=None)
-        expected = groupby(iterable, key)
-        self.assertAllGroupsEqual(actual, expected)
-
-        actual = mi.groupby_transform(iterable, key)  # default valuefunc
-        expected = groupby(iterable, key)
-        self.assertAllGroupsEqual(actual, expected)
-
-
-class NumericRangeTests(TestCase):
-    def test_basic(self):
-        for args, expected in [
-            ((4,), [0, 1, 2, 3]),
-            ((4.0,), [0.0, 1.0, 2.0, 3.0]),
-            ((1.0, 4), [1.0, 2.0, 3.0]),
-            ((1, 4.0), [1, 2, 3]),
-            ((1.0, 5), [1.0, 2.0, 3.0, 4.0]),
-            ((0, 20, 5), [0, 5, 10, 15]),
-            ((0, 20, 5.0), [0.0, 5.0, 10.0, 15.0]),
-            ((0, 10, 3), [0, 3, 6, 9]),
-            ((0, 10, 3.0), [0.0, 3.0, 6.0, 9.0]),
-            ((0, -5, -1), [0, -1, -2, -3, -4]),
-            ((0.0, -5, -1), [0.0, -1.0, -2.0, -3.0, -4.0]),
-            ((1, 2, Fraction(1, 2)), [Fraction(1, 1), Fraction(3, 2)]),
-            ((0,), []),
-            ((0.0,), []),
-            ((1, 0), []),
-            ((1.0, 0.0), []),
-            ((Fraction(2, 1),), [Fraction(0, 1), Fraction(1, 1)]),
-            ((Decimal('2.0'),), [Decimal('0.0'), Decimal('1.0')]),
-        ]:
-            actual = list(mi.numeric_range(*args))
-            self.assertEqual(actual, expected)
-            self.assertTrue(
-                all(type(a) == type(e) for a, e in zip(actual, expected))
-            )
-
-    def test_arg_count(self):
-        self.assertRaises(TypeError, lambda: list(mi.numeric_range()))
-        self.assertRaises(
-            TypeError, lambda: list(mi.numeric_range(0, 1, 2, 3))
-        )
-
-    def test_zero_step(self):
-        self.assertRaises(
-            ValueError, lambda: list(mi.numeric_range(1, 2, 0))
-        )
-
-
-class CountCycleTests(TestCase):
-    def test_basic(self):
-        expected = [
-            (0, 'a'), (0, 'b'), (0, 'c'),
-            (1, 'a'), (1, 'b'), (1, 'c'),
-            (2, 'a'), (2, 'b'), (2, 'c'),
-        ]
-        for actual in [
-            mi.take(9, mi.count_cycle('abc')),  # n=None
-            list(mi.count_cycle('abc', 3)),  # n=3
-        ]:
-            self.assertEqual(actual, expected)
-
-    def test_empty(self):
-        self.assertEqual(list(mi.count_cycle('')), [])
-        self.assertEqual(list(mi.count_cycle('', 2)), [])
-
-    def test_negative(self):
-        self.assertEqual(list(mi.count_cycle('abc', -3)), [])
-
-
-class LocateTests(TestCase):
-    def test_default_pred(self):
-        iterable = [0, 1, 1, 0, 1, 0, 0]
-        actual = list(mi.locate(iterable))
-        expected = [1, 2, 4]
-        self.assertEqual(actual, expected)
-
-    def test_no_matches(self):
-        iterable = [0, 0, 0]
-        actual = list(mi.locate(iterable))
-        expected = []
-        self.assertEqual(actual, expected)
-
-    def test_custom_pred(self):
-        iterable = ['0', 1, 1, '0', 1, '0', '0']
-        pred = lambda x: x == '0'
-        actual = list(mi.locate(iterable, pred))
-        expected = [0, 3, 5, 6]
-        self.assertEqual(actual, expected)
-
-    def test_window_size(self):
-        iterable = ['0', 1, 1, '0', 1, '0', '0']
-        pred = lambda *args: args == ('0', 1)
-        actual = list(mi.locate(iterable, pred, window_size=2))
-        expected = [0, 3]
-        self.assertEqual(actual, expected)
-
-    def test_window_size_large(self):
-        iterable = [1, 2, 3, 4]
-        pred = lambda a, b, c, d, e: True
-        actual = list(mi.locate(iterable, pred, window_size=5))
-        expected = [0]
-        self.assertEqual(actual, expected)
-
-    def test_window_size_zero(self):
-        iterable = [1, 2, 3, 4]
-        pred = lambda: True
-        with self.assertRaises(ValueError):
-            list(mi.locate(iterable, pred, window_size=0))
-
-
-class StripFunctionTests(TestCase):
-    def test_hashable(self):
-        iterable = list('www.example.com')
-        pred = lambda x: x in set('cmowz.')
-
-        self.assertEqual(list(mi.lstrip(iterable, pred)), list('example.com'))
-        self.assertEqual(list(mi.rstrip(iterable, pred)), list('www.example'))
-        self.assertEqual(list(mi.strip(iterable, pred)), list('example'))
-
-    def test_not_hashable(self):
-        iterable = [
-            list('http://'), list('www'), list('.example'), list('.com')
-        ]
-        pred = lambda x: x in [list('http://'), list('www'), list('.com')]
-
-        self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[2:])
-        self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:3])
-        self.assertEqual(list(mi.strip(iterable, pred)), iterable[2: 3])
-
-    def test_math(self):
-        iterable = [0, 1, 2, 3, 0, 1, 2, 3, 0, 1, 2]
-        pred = lambda x: x <= 2
-
-        self.assertEqual(list(mi.lstrip(iterable, pred)), iterable[3:])
-        self.assertEqual(list(mi.rstrip(iterable, pred)), iterable[:-3])
-        self.assertEqual(list(mi.strip(iterable, pred)), iterable[3:-3])
-
-
-class IsliceExtendedTests(TestCase):
-    def test_all(self):
-        iterable = ['0', '1', '2', '3', '4', '5']
-        indexes = list(range(-4, len(iterable) + 4)) + [None]
-        steps = [1, 2, 3, 4, -1, -2, -3, 4]
-        for slice_args in product(indexes, indexes, steps):
-            try:
-                actual = list(mi.islice_extended(iterable, *slice_args))
-            except Exception as e:
-                self.fail((slice_args, e))
-
-            expected = iterable[slice(*slice_args)]
-            self.assertEqual(actual, expected, slice_args)
-
-    def test_zero_step(self):
-        with self.assertRaises(ValueError):
-            list(mi.islice_extended([1, 2, 3], 0, 1, 0))
-
-
-class ConsecutiveGroupsTest(TestCase):
-    def test_numbers(self):
-        iterable = [-10, -8, -7, -6, 1, 2, 4, 5, -1, 7]
-        actual = [list(g) for g in mi.consecutive_groups(iterable)]
-        expected = [[-10], [-8, -7, -6], [1, 2], [4, 5], [-1], [7]]
-        self.assertEqual(actual, expected)
-
-    def test_custom_ordering(self):
-        iterable = ['1', '10', '11', '20', '21', '22', '30', '31']
-        ordering = lambda x: int(x)
-        actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)]
-        expected = [['1'], ['10', '11'], ['20', '21', '22'], ['30', '31']]
-        self.assertEqual(actual, expected)
-
-    def test_exotic_ordering(self):
-        iterable = [
-            ('a', 'b', 'c', 'd'),
-            ('a', 'c', 'b', 'd'),
-            ('a', 'c', 'd', 'b'),
-            ('a', 'd', 'b', 'c'),
-            ('d', 'b', 'c', 'a'),
-            ('d', 'c', 'a', 'b'),
-        ]
-        ordering = list(permutations('abcd')).index
-        actual = [list(g) for g in mi.consecutive_groups(iterable, ordering)]
-        expected = [
-            [('a', 'b', 'c', 'd')],
-            [('a', 'c', 'b', 'd'), ('a', 'c', 'd', 'b'), ('a', 'd', 'b', 'c')],
-            [('d', 'b', 'c', 'a'), ('d', 'c', 'a', 'b')],
-        ]
-        self.assertEqual(actual, expected)
-
-
-class DifferenceTest(TestCase):
-    def test_normal(self):
-        iterable = [10, 20, 30, 40, 50]
-        actual = list(mi.difference(iterable))
-        expected = [10, 10, 10, 10, 10]
-        self.assertEqual(actual, expected)
-
-    def test_custom(self):
-        iterable = [10, 20, 30, 40, 50]
-        actual = list(mi.difference(iterable, add))
-        expected = [10, 30, 50, 70, 90]
-        self.assertEqual(actual, expected)
-
-    def test_roundtrip(self):
-        original = list(range(100))
-        accumulated = mi.accumulate(original)
-        actual = list(mi.difference(accumulated))
-        self.assertEqual(actual, original)
-
-    def test_one(self):
-        self.assertEqual(list(mi.difference([0])), [0])
-
-    def test_empty(self):
-        self.assertEqual(list(mi.difference([])), [])
-
-
-class SeekableTest(TestCase):
-    def test_exhaustion_reset(self):
-        iterable = [str(n) for n in range(10)]
-
-        s = mi.seekable(iterable)
-        self.assertEqual(list(s), iterable)  # Normal iteration
-        self.assertEqual(list(s), [])  # Iterable is exhausted
-
-        s.seek(0)
-        self.assertEqual(list(s), iterable)  # Back in action
-
-    def test_partial_reset(self):
-        iterable = [str(n) for n in range(10)]
-
-        s = mi.seekable(iterable)
-        self.assertEqual(mi.take(5, s), iterable[:5])  # Normal iteration
-
-        s.seek(1)
-        self.assertEqual(list(s), iterable[1:])  # Get the rest of the iterable
-
-    def test_forward(self):
-        iterable = [str(n) for n in range(10)]
-
-        s = mi.seekable(iterable)
-        self.assertEqual(mi.take(1, s), iterable[:1])  # Normal iteration
-
-        s.seek(3)  # Skip over index 2
-        self.assertEqual(list(s), iterable[3:])  # Result is similar to slicing
-
-        s.seek(0)  # Back to 0
-        self.assertEqual(list(s), iterable)  # No difference in result
-
-    def test_past_end(self):
-        iterable = [str(n) for n in range(10)]
-
-        s = mi.seekable(iterable)
-        self.assertEqual(mi.take(1, s), iterable[:1])  # Normal iteration
-
-        s.seek(20)
-        self.assertEqual(list(s), [])  # Iterable is exhausted
-
-        s.seek(0)  # Back to 0
-        self.assertEqual(list(s), iterable)  # No difference in result
-
-    def test_elements(self):
-        iterable = map(str, count())
-
-        s = mi.seekable(iterable)
-        mi.take(10, s)
-
-        elements = s.elements()
-        self.assertEqual(
-            [elements[i] for i in range(10)], [str(n) for n in range(10)]
-        )
-        self.assertEqual(len(elements), 10)
-
-        mi.take(10, s)
-        self.assertEqual(list(elements), [str(n) for n in range(20)])
-
-
-class SequenceViewTests(TestCase):
-    def test_init(self):
-        view = mi.SequenceView((1, 2, 3))
-        self.assertEqual(repr(view), "SequenceView((1, 2, 3))")
-        self.assertRaises(TypeError, lambda: mi.SequenceView({}))
-
-    def test_update(self):
-        seq = [1, 2, 3]
-        view = mi.SequenceView(seq)
-        self.assertEqual(len(view), 3)
-        self.assertEqual(repr(view), "SequenceView([1, 2, 3])")
-
-        seq.pop()
-        self.assertEqual(len(view), 2)
-        self.assertEqual(repr(view), "SequenceView([1, 2])")
-
-    def test_indexing(self):
-        seq = ('a', 'b', 'c', 'd', 'e', 'f')
-        view = mi.SequenceView(seq)
-        for i in range(-len(seq), len(seq)):
-            self.assertEqual(view[i], seq[i])
-
-    def test_slicing(self):
-        seq = ('a', 'b', 'c', 'd', 'e', 'f')
-        view = mi.SequenceView(seq)
-        n = len(seq)
-        indexes = list(range(-n - 1, n + 1)) + [None]
-        steps = list(range(-n, n + 1))
-        steps.remove(0)
-        for slice_args in product(indexes, indexes, steps):
-            i = slice(*slice_args)
-            self.assertEqual(view[i], seq[i])
-
-    def test_abc_methods(self):
-        # collections.Sequence should provide all of this functionality
-        seq = ('a', 'b', 'c', 'd', 'e', 'f', 'f')
-        view = mi.SequenceView(seq)
-
-        # __contains__
-        self.assertIn('b', view)
-        self.assertNotIn('g', view)
-
-        # __iter__
-        self.assertEqual(list(iter(view)), list(seq))
-
-        # __reversed__
-        self.assertEqual(list(reversed(view)), list(reversed(seq)))
-
-        # index
-        self.assertEqual(view.index('b'), 1)
-
-        # count
-        self.assertEqual(seq.count('f'), 2)
-
-
-class RunLengthTest(TestCase):
-    def test_encode(self):
-        iterable = (int(str(n)[0]) for n in count(800))
-        actual = mi.take(4, mi.run_length.encode(iterable))
-        expected = [(8, 100), (9, 100), (1, 1000), (2, 1000)]
-        self.assertEqual(actual, expected)
-
-    def test_decode(self):
-        iterable = [('d', 4), ('c', 3), ('b', 2), ('a', 1)]
-        actual = ''.join(mi.run_length.decode(iterable))
-        expected = 'ddddcccbba'
-        self.assertEqual(actual, expected)
-
-
-class ExactlyNTests(TestCase):
-    """Tests for ``exactly_n()``"""
-
-    def test_true(self):
-        """Iterable has ``n`` ``True`` elements"""
-        self.assertTrue(mi.exactly_n([True, False, True], 2))
-        self.assertTrue(mi.exactly_n([1, 1, 1, 0], 3))
-        self.assertTrue(mi.exactly_n([False, False], 0))
-        self.assertTrue(mi.exactly_n(range(100), 10, lambda x: x < 10))
-
-    def test_false(self):
-        """Iterable does not have ``n`` ``True`` elements"""
-        self.assertFalse(mi.exactly_n([True, False, False], 2))
-        self.assertFalse(mi.exactly_n([True, True, False], 1))
-        self.assertFalse(mi.exactly_n([False], 1))
-        self.assertFalse(mi.exactly_n([True], -1))
-        self.assertFalse(mi.exactly_n(repeat(True), 100))
-
-    def test_empty(self):
-        """Return ``True`` if the iterable is empty and ``n`` is 0"""
-        self.assertTrue(mi.exactly_n([], 0))
-        self.assertFalse(mi.exactly_n([], 1))
-
-
-class AlwaysReversibleTests(TestCase):
-    """Tests for ``always_reversible()``"""
-
-    def test_regular_reversed(self):
-        self.assertEqual(list(reversed(range(10))),
-                         list(mi.always_reversible(range(10))))
-        self.assertEqual(list(reversed([1, 2, 3])),
-                         list(mi.always_reversible([1, 2, 3])))
-        self.assertEqual(reversed([1, 2, 3]).__class__,
-                         mi.always_reversible([1, 2, 3]).__class__)
-
-    def test_nonseq_reversed(self):
-        # Create a non-reversible generator from a sequence
-        with self.assertRaises(TypeError):
-            reversed(x for x in range(10))
-
-        self.assertEqual(list(reversed(range(10))),
-                         list(mi.always_reversible(x for x in range(10))))
-        self.assertEqual(list(reversed([1, 2, 3])),
-                         list(mi.always_reversible(x for x in [1, 2, 3])))
-        self.assertNotEqual(reversed((1, 2)).__class__,
-                            mi.always_reversible(x for x in (1, 2)).__class__)
-
-
-class CircularShiftsTests(TestCase):
-    def test_empty(self):
-        # empty iterable -> empty list
-        self.assertEqual(list(mi.circular_shifts([])), [])
-
-    def test_simple_circular_shifts(self):
-        # test the a simple iterator case
-        self.assertEqual(
-            mi.circular_shifts(range(4)),
-            [(0, 1, 2, 3), (1, 2, 3, 0), (2, 3, 0, 1), (3, 0, 1, 2)]
-        )
-
-    def test_duplicates(self):
-        # test non-distinct entries
-        self.assertEqual(
-            mi.circular_shifts([0, 1, 0, 1]),
-            [(0, 1, 0, 1), (1, 0, 1, 0), (0, 1, 0, 1), (1, 0, 1, 0)]
-        )
-
-
-class MakeDecoratorTests(TestCase):
-    def test_basic(self):
-        slicer = mi.make_decorator(islice)
-
-        @slicer(1, 10, 2)
-        def user_function(arg_1, arg_2, kwarg_1=None):
-            self.assertEqual(arg_1, 'arg_1')
-            self.assertEqual(arg_2, 'arg_2')
-            self.assertEqual(kwarg_1, 'kwarg_1')
-            return map(str, count())
-
-        it = user_function('arg_1', 'arg_2', kwarg_1='kwarg_1')
-        actual = list(it)
-        expected = ['1', '3', '5', '7', '9']
-        self.assertEqual(actual, expected)
-
-    def test_result_index(self):
-        def stringify(*args, **kwargs):
-            self.assertEqual(args[0], 'arg_0')
-            iterable = args[1]
-            self.assertEqual(args[2], 'arg_2')
-            self.assertEqual(kwargs['kwarg_1'], 'kwarg_1')
-            return map(str, iterable)
-
-        stringifier = mi.make_decorator(stringify, result_index=1)
-
-        @stringifier('arg_0', 'arg_2', kwarg_1='kwarg_1')
-        def user_function(n):
-            return count(n)
-
-        it = user_function(1)
-        actual = mi.take(5, it)
-        expected = ['1', '2', '3', '4', '5']
-        self.assertEqual(actual, expected)
-
-    def test_wrap_class(self):
-        seeker = mi.make_decorator(mi.seekable)
-
-        @seeker()
-        def user_function(n):
-            return map(str, range(n))
-
-        it = user_function(5)
-        self.assertEqual(list(it), ['0', '1', '2', '3', '4'])
-
-        it.seek(0)
-        self.assertEqual(list(it), ['0', '1', '2', '3', '4'])
-
-
-class MapReduceTests(TestCase):
-    def test_default(self):
-        iterable = (str(x) for x in range(5))
-        keyfunc = lambda x: int(x) // 2
-        actual = sorted(mi.map_reduce(iterable, keyfunc).items())
-        expected = [(0, ['0', '1']), (1, ['2', '3']), (2, ['4'])]
-        self.assertEqual(actual, expected)
-
-    def test_valuefunc(self):
-        iterable = (str(x) for x in range(5))
-        keyfunc = lambda x: int(x) // 2
-        valuefunc = int
-        actual = sorted(mi.map_reduce(iterable, keyfunc, valuefunc).items())
-        expected = [(0, [0, 1]), (1, [2, 3]), (2, [4])]
-        self.assertEqual(actual, expected)
-
-    def test_reducefunc(self):
-        iterable = (str(x) for x in range(5))
-        keyfunc = lambda x: int(x) // 2
-        valuefunc = int
-        reducefunc = lambda value_list: reduce(mul, value_list, 1)
-        actual = sorted(
-            mi.map_reduce(iterable, keyfunc, valuefunc, reducefunc).items()
-        )
-        expected = [(0, 0), (1, 6), (2, 4)]
-        self.assertEqual(actual, expected)
-
-    def test_ret(self):
-        d = mi.map_reduce([1, 0, 2, 0, 1, 0], bool)
-        self.assertEqual(d, {False: [0, 0, 0], True: [1, 2, 1]})
-        self.assertRaises(KeyError, lambda: d[None].append(1))
-
-
-class RlocateTests(TestCase):
-    def test_default_pred(self):
-        iterable = [0, 1, 1, 0, 1, 0, 0]
-        for it in (iterable[:], iter(iterable)):
-            actual = list(mi.rlocate(it))
-            expected = [4, 2, 1]
-            self.assertEqual(actual, expected)
-
-    def test_no_matches(self):
-        iterable = [0, 0, 0]
-        for it in (iterable[:], iter(iterable)):
-            actual = list(mi.rlocate(it))
-            expected = []
-            self.assertEqual(actual, expected)
-
-    def test_custom_pred(self):
-        iterable = ['0', 1, 1, '0', 1, '0', '0']
-        pred = lambda x: x == '0'
-        for it in (iterable[:], iter(iterable)):
-            actual = list(mi.rlocate(it, pred))
-            expected = [6, 5, 3, 0]
-            self.assertEqual(actual, expected)
-
-    def test_efficient_reversal(self):
-        iterable = range(10 ** 10)  # Is efficiently reversible
-        target = 10 ** 10 - 2
-        pred = lambda x: x == target  # Find-able from the right
-        actual = next(mi.rlocate(iterable, pred))
-        self.assertEqual(actual, target)
-
-    def test_window_size(self):
-        iterable = ['0', 1, 1, '0', 1, '0', '0']
-        pred = lambda *args: args == ('0', 1)
-        for it in (iterable, iter(iterable)):
-            actual = list(mi.rlocate(it, pred, window_size=2))
-            expected = [3, 0]
-            self.assertEqual(actual, expected)
-
-    def test_window_size_large(self):
-        iterable = [1, 2, 3, 4]
-        pred = lambda a, b, c, d, e: True
-        for it in (iterable, iter(iterable)):
-            actual = list(mi.rlocate(iterable, pred, window_size=5))
-            expected = [0]
-            self.assertEqual(actual, expected)
-
-    def test_window_size_zero(self):
-        iterable = [1, 2, 3, 4]
-        pred = lambda: True
-        for it in (iterable, iter(iterable)):
-            with self.assertRaises(ValueError):
-                list(mi.locate(iterable, pred, window_size=0))
-
-
-class ReplaceTests(TestCase):
-    def test_basic(self):
-        iterable = range(10)
-        pred = lambda x: x % 2 == 0
-        substitutes = []
-        actual = list(mi.replace(iterable, pred, substitutes))
-        expected = [1, 3, 5, 7, 9]
-        self.assertEqual(actual, expected)
-
-    def test_count(self):
-        iterable = range(10)
-        pred = lambda x: x % 2 == 0
-        substitutes = []
-        actual = list(mi.replace(iterable, pred, substitutes, count=4))
-        expected = [1, 3, 5, 7, 8, 9]
-        self.assertEqual(actual, expected)
-
-    def test_window_size(self):
-        iterable = range(10)
-        pred = lambda *args: args == (0, 1, 2)
-        substitutes = []
-        actual = list(mi.replace(iterable, pred, substitutes, window_size=3))
-        expected = [3, 4, 5, 6, 7, 8, 9]
-        self.assertEqual(actual, expected)
-
-    def test_window_size_end(self):
-        iterable = range(10)
-        pred = lambda *args: args == (7, 8, 9)
-        substitutes = []
-        actual = list(mi.replace(iterable, pred, substitutes, window_size=3))
-        expected = [0, 1, 2, 3, 4, 5, 6]
-        self.assertEqual(actual, expected)
-
-    def test_window_size_count(self):
-        iterable = range(10)
-        pred = lambda *args: (args == (0, 1, 2)) or (args == (7, 8, 9))
-        substitutes = []
-        actual = list(
-            mi.replace(iterable, pred, substitutes, count=1, window_size=3)
-        )
-        expected = [3, 4, 5, 6, 7, 8, 9]
-        self.assertEqual(actual, expected)
-
-    def test_window_size_large(self):
-        iterable = range(4)
-        pred = lambda a, b, c, d, e: True
-        substitutes = [5, 6, 7]
-        actual = list(mi.replace(iterable, pred, substitutes, window_size=5))
-        expected = [5, 6, 7]
-        self.assertEqual(actual, expected)
-
-    def test_window_size_zero(self):
-        iterable = range(10)
-        pred = lambda *args: True
-        substitutes = []
-        with self.assertRaises(ValueError):
-            list(mi.replace(iterable, pred, substitutes, window_size=0))
-
-    def test_iterable_substitutes(self):
-        iterable = range(5)
-        pred = lambda x: x % 2 == 0
-        substitutes = iter('__')
-        actual = list(mi.replace(iterable, pred, substitutes))
-        expected = ['_', '_', 1, '_', '_', 3, '_', '_']
-        self.assertEqual(actual, expected)
diff --git a/libraries/more_itertools/tests/test_recipes.py b/libraries/more_itertools/tests/test_recipes.py
deleted file mode 100644
index 98981fe8..00000000
--- a/libraries/more_itertools/tests/test_recipes.py
+++ /dev/null
@@ -1,616 +0,0 @@
-from doctest import DocTestSuite
-from unittest import TestCase
-
-from itertools import combinations
-from six.moves import range
-
-import more_itertools as mi
-
-
-def load_tests(loader, tests, ignore):
-    # Add the doctests
-    tests.addTests(DocTestSuite('more_itertools.recipes'))
-    return tests
-
-
-class AccumulateTests(TestCase):
-    """Tests for ``accumulate()``"""
-
-    def test_empty(self):
-        """Test that an empty input returns an empty output"""
-        self.assertEqual(list(mi.accumulate([])), [])
-
-    def test_default(self):
-        """Test accumulate with the default function (addition)"""
-        self.assertEqual(list(mi.accumulate([1, 2, 3])), [1, 3, 6])
-
-    def test_bogus_function(self):
-        """Test accumulate with an invalid function"""
-        with self.assertRaises(TypeError):
-            list(mi.accumulate([1, 2, 3], func=lambda x: x))
-
-    def test_custom_function(self):
-        """Test accumulate with a custom function"""
-        self.assertEqual(
-            list(mi.accumulate((1, 2, 3, 2, 1), func=max)), [1, 2, 3, 3, 3]
-        )
-
-
-class TakeTests(TestCase):
-    """Tests for ``take()``"""
-
-    def test_simple_take(self):
-        """Test basic usage"""
-        t = mi.take(5, range(10))
-        self.assertEqual(t, [0, 1, 2, 3, 4])
-
-    def test_null_take(self):
-        """Check the null case"""
-        t = mi.take(0, range(10))
-        self.assertEqual(t, [])
-
-    def test_negative_take(self):
-        """Make sure taking negative items results in a ValueError"""
-        self.assertRaises(ValueError, lambda: mi.take(-3, range(10)))
-
-    def test_take_too_much(self):
-        """Taking more than an iterator has remaining should return what the
-        iterator has remaining.
-
-        """
-        t = mi.take(10, range(5))
-        self.assertEqual(t, [0, 1, 2, 3, 4])
-
-
-class TabulateTests(TestCase):
-    """Tests for ``tabulate()``"""
-
-    def test_simple_tabulate(self):
-        """Test the happy path"""
-        t = mi.tabulate(lambda x: x)
-        f = tuple([next(t) for _ in range(3)])
-        self.assertEqual(f, (0, 1, 2))
-
-    def test_count(self):
-        """Ensure tabulate accepts specific count"""
-        t = mi.tabulate(lambda x: 2 * x, -1)
-        f = (next(t), next(t), next(t))
-        self.assertEqual(f, (-2, 0, 2))
-
-
-class TailTests(TestCase):
-    """Tests for ``tail()``"""
-
-    def test_greater(self):
-        """Length of iterable is greather than requested tail"""
-        self.assertEqual(list(mi.tail(3, 'ABCDEFG')), ['E', 'F', 'G'])
-
-    def test_equal(self):
-        """Length of iterable is equal to the requested tail"""
-        self.assertEqual(
-            list(mi.tail(7, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G']
-        )
-
-    def test_less(self):
-        """Length of iterable is less than requested tail"""
-        self.assertEqual(
-            list(mi.tail(8, 'ABCDEFG')), ['A', 'B', 'C', 'D', 'E', 'F', 'G']
-        )
-
-
-class ConsumeTests(TestCase):
-    """Tests for ``consume()``"""
-
-    def test_sanity(self):
-        """Test basic functionality"""
-        r = (x for x in range(10))
-        mi.consume(r, 3)
-        self.assertEqual(3, next(r))
-
-    def test_null_consume(self):
-        """Check the null case"""
-        r = (x for x in range(10))
-        mi.consume(r, 0)
-        self.assertEqual(0, next(r))
-
-    def test_negative_consume(self):
-        """Check that negative consumsion throws an error"""
-        r = (x for x in range(10))
-        self.assertRaises(ValueError, lambda: mi.consume(r, -1))
-
-    def test_total_consume(self):
-        """Check that iterator is totally consumed by default"""
-        r = (x for x in range(10))
-        mi.consume(r)
-        self.assertRaises(StopIteration, lambda: next(r))
-
-
-class NthTests(TestCase):
-    """Tests for ``nth()``"""
-
-    def test_basic(self):
-        """Make sure the nth item is returned"""
-        l = range(10)
-        for i, v in enumerate(l):
-            self.assertEqual(mi.nth(l, i), v)
-
-    def test_default(self):
-        """Ensure a default value is returned when nth item not found"""
-        l = range(3)
-        self.assertEqual(mi.nth(l, 100, "zebra"), "zebra")
-
-    def test_negative_item_raises(self):
-        """Ensure asking for a negative item raises an exception"""
-        self.assertRaises(ValueError, lambda: mi.nth(range(10), -3))
-
-
-class AllEqualTests(TestCase):
-    """Tests for ``all_equal()``"""
-
-    def test_true(self):
-        """Everything is equal"""
-        self.assertTrue(mi.all_equal('aaaaaa'))
-        self.assertTrue(mi.all_equal([0, 0, 0, 0]))
-
-    def test_false(self):
-        """Not everything is equal"""
-        self.assertFalse(mi.all_equal('aaaaab'))
-        self.assertFalse(mi.all_equal([0, 0, 0, 1]))
-
-    def test_tricky(self):
-        """Not everything is identical, but everything is equal"""
-        items = [1, complex(1, 0), 1.0]
-        self.assertTrue(mi.all_equal(items))
-
-    def test_empty(self):
-        """Return True if the iterable is empty"""
-        self.assertTrue(mi.all_equal(''))
-        self.assertTrue(mi.all_equal([]))
-
-    def test_one(self):
-        """Return True if the iterable is singular"""
-        self.assertTrue(mi.all_equal('0'))
-        self.assertTrue(mi.all_equal([0]))
-
-
-class QuantifyTests(TestCase):
-    """Tests for ``quantify()``"""
-
-    def test_happy_path(self):
-        """Make sure True count is returned"""
-        q = [True, False, True]
-        self.assertEqual(mi.quantify(q), 2)
-
-    def test_custom_predicate(self):
-        """Ensure non-default predicates return as expected"""
-        q = range(10)
-        self.assertEqual(mi.quantify(q, lambda x: x % 2 == 0), 5)
-
-
-class PadnoneTests(TestCase):
-    """Tests for ``padnone()``"""
-
-    def test_happy_path(self):
-        """wrapper iterator should return None indefinitely"""
-        r = range(2)
-        p = mi.padnone(r)
-        self.assertEqual([0, 1, None, None], [next(p) for _ in range(4)])
-
-
-class NcyclesTests(TestCase):
-    """Tests for ``nyclces()``"""
-
-    def test_happy_path(self):
-        """cycle a sequence three times"""
-        r = ["a", "b", "c"]
-        n = mi.ncycles(r, 3)
-        self.assertEqual(
-            ["a", "b", "c", "a", "b", "c", "a", "b", "c"],
-            list(n)
-        )
-
-    def test_null_case(self):
-        """asking for 0 cycles should return an empty iterator"""
-        n = mi.ncycles(range(100), 0)
-        self.assertRaises(StopIteration, lambda: next(n))
-
-    def test_pathalogical_case(self):
-        """asking for negative cycles should return an empty iterator"""
-        n = mi.ncycles(range(100), -10)
-        self.assertRaises(StopIteration, lambda: next(n))
-
-
-class DotproductTests(TestCase):
-    """Tests for ``dotproduct()``'"""
-
-    def test_happy_path(self):
-        """simple dotproduct example"""
-        self.assertEqual(400, mi.dotproduct([10, 10], [20, 20]))
-
-
-class FlattenTests(TestCase):
-    """Tests for ``flatten()``"""
-
-    def test_basic_usage(self):
-        """ensure list of lists is flattened one level"""
-        f = [[0, 1, 2], [3, 4, 5]]
-        self.assertEqual(list(range(6)), list(mi.flatten(f)))
-
-    def test_single_level(self):
-        """ensure list of lists is flattened only one level"""
-        f = [[0, [1, 2]], [[3, 4], 5]]
-        self.assertEqual([0, [1, 2], [3, 4], 5], list(mi.flatten(f)))
-
-
-class RepeatfuncTests(TestCase):
-    """Tests for ``repeatfunc()``"""
-
-    def test_simple_repeat(self):
-        """test simple repeated functions"""
-        r = mi.repeatfunc(lambda: 5)
-        self.assertEqual([5, 5, 5, 5, 5], [next(r) for _ in range(5)])
-
-    def test_finite_repeat(self):
-        """ensure limited repeat when times is provided"""
-        r = mi.repeatfunc(lambda: 5, times=5)
-        self.assertEqual([5, 5, 5, 5, 5], list(r))
-
-    def test_added_arguments(self):
-        """ensure arguments are applied to the function"""
-        r = mi.repeatfunc(lambda x: x, 2, 3)
-        self.assertEqual([3, 3], list(r))
-
-    def test_null_times(self):
-        """repeat 0 should return an empty iterator"""
-        r = mi.repeatfunc(range, 0, 3)
-        self.assertRaises(StopIteration, lambda: next(r))
-
-
-class PairwiseTests(TestCase):
-    """Tests for ``pairwise()``"""
-
-    def test_base_case(self):
-        """ensure an iterable will return pairwise"""
-        p = mi.pairwise([1, 2, 3])
-        self.assertEqual([(1, 2), (2, 3)], list(p))
-
-    def test_short_case(self):
-        """ensure an empty iterator if there's not enough values to pair"""
-        p = mi.pairwise("a")
-        self.assertRaises(StopIteration, lambda: next(p))
-
-
-class GrouperTests(TestCase):
-    """Tests for ``grouper()``"""
-
-    def test_even(self):
-        """Test when group size divides evenly into the length of
-        the iterable.
-
-        """
-        self.assertEqual(
-            list(mi.grouper(3, 'ABCDEF')), [('A', 'B', 'C'), ('D', 'E', 'F')]
-        )
-
-    def test_odd(self):
-        """Test when group size does not divide evenly into the length of the
-        iterable.
-
-        """
-        self.assertEqual(
-            list(mi.grouper(3, 'ABCDE')), [('A', 'B', 'C'), ('D', 'E', None)]
-        )
-
-    def test_fill_value(self):
-        """Test that the fill value is used to pad the final group"""
-        self.assertEqual(
-            list(mi.grouper(3, 'ABCDE', 'x')),
-            [('A', 'B', 'C'), ('D', 'E', 'x')]
-        )
-
-
-class RoundrobinTests(TestCase):
-    """Tests for ``roundrobin()``"""
-
-    def test_even_groups(self):
-        """Ensure ordered output from evenly populated iterables"""
-        self.assertEqual(
-            list(mi.roundrobin('ABC', [1, 2, 3], range(3))),
-            ['A', 1, 0, 'B', 2, 1, 'C', 3, 2]
-        )
-
-    def test_uneven_groups(self):
-        """Ensure ordered output from unevenly populated iterables"""
-        self.assertEqual(
-            list(mi.roundrobin('ABCD', [1, 2], range(0))),
-            ['A', 1, 'B', 2, 'C', 'D']
-        )
-
-
-class PartitionTests(TestCase):
-    """Tests for ``partition()``"""
-
-    def test_bool(self):
-        """Test when pred() returns a boolean"""
-        lesser, greater = mi.partition(lambda x: x > 5, range(10))
-        self.assertEqual(list(lesser), [0, 1, 2, 3, 4, 5])
-        self.assertEqual(list(greater), [6, 7, 8, 9])
-
-    def test_arbitrary(self):
-        """Test when pred() returns an integer"""
-        divisibles, remainders = mi.partition(lambda x: x % 3, range(10))
-        self.assertEqual(list(divisibles), [0, 3, 6, 9])
-        self.assertEqual(list(remainders), [1, 2, 4, 5, 7, 8])
-
-
-class PowersetTests(TestCase):
-    """Tests for ``powerset()``"""
-
-    def test_combinatorics(self):
-        """Ensure a proper enumeration"""
-        p = mi.powerset([1, 2, 3])
-        self.assertEqual(
-            list(p),
-            [(), (1,), (2,), (3,), (1, 2), (1, 3), (2, 3), (1, 2, 3)]
-        )
-
-
-class UniqueEverseenTests(TestCase):
-    """Tests for ``unique_everseen()``"""
-
-    def test_everseen(self):
-        """ensure duplicate elements are ignored"""
-        u = mi.unique_everseen('AAAABBBBCCDAABBB')
-        self.assertEqual(
-            ['A', 'B', 'C', 'D'],
-            list(u)
-        )
-
-    def test_custom_key(self):
-        """ensure the custom key comparison works"""
-        u = mi.unique_everseen('aAbACCc', key=str.lower)
-        self.assertEqual(list('abC'), list(u))
-
-    def test_unhashable(self):
-        """ensure things work for unhashable items"""
-        iterable = ['a', [1, 2, 3], [1, 2, 3], 'a']
-        u = mi.unique_everseen(iterable)
-        self.assertEqual(list(u), ['a', [1, 2, 3]])
-
-    def test_unhashable_key(self):
-        """ensure things work for unhashable items with a custom key"""
-        iterable = ['a', [1, 2, 3], [1, 2, 3], 'a']
-        u = mi.unique_everseen(iterable, key=lambda x: x)
-        self.assertEqual(list(u), ['a', [1, 2, 3]])
-
-
-class UniqueJustseenTests(TestCase):
-    """Tests for ``unique_justseen()``"""
-
-    def test_justseen(self):
-        """ensure only last item is remembered"""
-        u = mi.unique_justseen('AAAABBBCCDABB')
-        self.assertEqual(list('ABCDAB'), list(u))
-
-    def test_custom_key(self):
-        """ensure the custom key comparison works"""
-        u = mi.unique_justseen('AABCcAD', str.lower)
-        self.assertEqual(list('ABCAD'), list(u))
-
-
-class IterExceptTests(TestCase):
-    """Tests for ``iter_except()``"""
-
-    def test_exact_exception(self):
-        """ensure the exact specified exception is caught"""
-        l = [1, 2, 3]
-        i = mi.iter_except(l.pop, IndexError)
-        self.assertEqual(list(i), [3, 2, 1])
-
-    def test_generic_exception(self):
-        """ensure the generic exception can be caught"""
-        l = [1, 2]
-        i = mi.iter_except(l.pop, Exception)
-        self.assertEqual(list(i), [2, 1])
-
-    def test_uncaught_exception_is_raised(self):
-        """ensure a non-specified exception is raised"""
-        l = [1, 2, 3]
-        i = mi.iter_except(l.pop, KeyError)
-        self.assertRaises(IndexError, lambda: list(i))
-
-    def test_first(self):
-        """ensure first is run before the function"""
-        l = [1, 2, 3]
-        f = lambda: 25
-        i = mi.iter_except(l.pop, IndexError, f)
-        self.assertEqual(list(i), [25, 3, 2, 1])
-
-
-class FirstTrueTests(TestCase):
-    """Tests for ``first_true()``"""
-
-    def test_something_true(self):
-        """Test with no keywords"""
-        self.assertEqual(mi.first_true(range(10)), 1)
-
-    def test_nothing_true(self):
-        """Test default return value."""
-        self.assertEqual(mi.first_true([0, 0, 0]), False)
-
-    def test_default(self):
-        """Test with a default keyword"""
-        self.assertEqual(mi.first_true([0, 0, 0], default='!'), '!')
-
-    def test_pred(self):
-        """Test with a custom predicate"""
-        self.assertEqual(
-            mi.first_true([2, 4, 6], pred=lambda x: x % 3 == 0), 6
-        )
-
-
-class RandomProductTests(TestCase):
-    """Tests for ``random_product()``
-
-    Since random.choice() has different results with the same seed across
-    python versions 2.x and 3.x, these tests use highly probably events to
-    create predictable outcomes across platforms.
-    """
-
-    def test_simple_lists(self):
-        """Ensure that one item is chosen from each list in each pair.
-        Also ensure that each item from each list eventually appears in
-        the chosen combinations.
-
-        Odds are roughly 1 in 7.1 * 10e16 that one item from either list will
-        not be chosen after 100 samplings of one item from each list. Just to
-        be safe, better use a known random seed, too.
-
-        """
-        nums = [1, 2, 3]
-        lets = ['a', 'b', 'c']
-        n, m = zip(*[mi.random_product(nums, lets) for _ in range(100)])
-        n, m = set(n), set(m)
-        self.assertEqual(n, set(nums))
-        self.assertEqual(m, set(lets))
-        self.assertEqual(len(n), len(nums))
-        self.assertEqual(len(m), len(lets))
-
-    def test_list_with_repeat(self):
-        """ensure multiple items are chosen, and that they appear to be chosen
-        from one list then the next, in proper order.
-
-        """
-        nums = [1, 2, 3]
-        lets = ['a', 'b', 'c']
-        r = list(mi.random_product(nums, lets, repeat=100))
-        self.assertEqual(2 * 100, len(r))
-        n, m = set(r[::2]), set(r[1::2])
-        self.assertEqual(n, set(nums))
-        self.assertEqual(m, set(lets))
-        self.assertEqual(len(n), len(nums))
-        self.assertEqual(len(m), len(lets))
-
-
-class RandomPermutationTests(TestCase):
-    """Tests for ``random_permutation()``"""
-
-    def test_full_permutation(self):
-        """ensure every item from the iterable is returned in a new ordering
-
-        15 elements have a 1 in 1.3 * 10e12 of appearing in sorted order, so
-        we fix a seed value just to be sure.
-
-        """
-        i = range(15)
-        r = mi.random_permutation(i)
-        self.assertEqual(set(i), set(r))
-        if i == r:
-            raise AssertionError("Values were not permuted")
-
-    def test_partial_permutation(self):
-        """ensure all returned items are from the iterable, that the returned
-        permutation is of the desired length, and that all items eventually
-        get returned.
-
-        Sampling 100 permutations of length 5 from a set of 15 leaves a
-        (2/3)^100 chance that an item will not be chosen. Multiplied by 15
-        items, there is a 1 in 2.6e16 chance that at least 1 item will not
-        show up in the resulting output. Using a random seed will fix that.
-
-        """
-        items = range(15)
-        item_set = set(items)
-        all_items = set()
-        for _ in range(100):
-            permutation = mi.random_permutation(items, 5)
-            self.assertEqual(len(permutation), 5)
-            permutation_set = set(permutation)
-            self.assertLessEqual(permutation_set, item_set)
-            all_items |= permutation_set
-        self.assertEqual(all_items, item_set)
-
-
-class RandomCombinationTests(TestCase):
-    """Tests for ``random_combination()``"""
-
-    def test_psuedorandomness(self):
-        """ensure different subsets of the iterable get returned over many
-        samplings of random combinations"""
-        items = range(15)
-        all_items = set()
-        for _ in range(50):
-            combination = mi.random_combination(items, 5)
-            all_items |= set(combination)
-        self.assertEqual(all_items, set(items))
-
-    def test_no_replacement(self):
-        """ensure that elements are sampled without replacement"""
-        items = range(15)
-        for _ in range(50):
-            combination = mi.random_combination(items, len(items))
-            self.assertEqual(len(combination), len(set(combination)))
-        self.assertRaises(
-            ValueError, lambda: mi.random_combination(items, len(items) + 1)
-        )
-
-
-class RandomCombinationWithReplacementTests(TestCase):
-    """Tests for ``random_combination_with_replacement()``"""
-
-    def test_replacement(self):
-        """ensure that elements are sampled with replacement"""
-        items = range(5)
-        combo = mi.random_combination_with_replacement(items, len(items) * 2)
-        self.assertEqual(2 * len(items), len(combo))
-        if len(set(combo)) == len(combo):
-            raise AssertionError("Combination contained no duplicates")
-
-    def test_pseudorandomness(self):
-        """ensure different subsets of the iterable get returned over many
-        samplings of random combinations"""
-        items = range(15)
-        all_items = set()
-        for _ in range(50):
-            combination = mi.random_combination_with_replacement(items, 5)
-            all_items |= set(combination)
-        self.assertEqual(all_items, set(items))
-
-
-class NthCombinationTests(TestCase):
-    def test_basic(self):
-        iterable = 'abcdefg'
-        r = 4
-        for index, expected in enumerate(combinations(iterable, r)):
-            actual = mi.nth_combination(iterable, r, index)
-            self.assertEqual(actual, expected)
-
-    def test_long(self):
-        actual = mi.nth_combination(range(180), 4, 2000000)
-        expected = (2, 12, 35, 126)
-        self.assertEqual(actual, expected)
-
-    def test_invalid_r(self):
-        for r in (-1, 3):
-            with self.assertRaises(ValueError):
-                mi.nth_combination([], r, 0)
-
-    def test_invalid_index(self):
-        with self.assertRaises(IndexError):
-            mi.nth_combination('abcdefg', 3, -36)
-
-
-class PrependTests(TestCase):
-    def test_basic(self):
-        value = 'a'
-        iterator = iter('bcdefg')
-        actual = list(mi.prepend(value, iterator))
-        expected = list('abcdefg')
-        self.assertEqual(actual, expected)
-
-    def test_multiple(self):
-        value = 'ab'
-        iterator = iter('cdefg')
-        actual = tuple(mi.prepend(value, iterator))
-        expected = ('ab',) + tuple('cdefg')
-        self.assertEqual(actual, expected)
diff --git a/libraries/portend.py b/libraries/portend.py
deleted file mode 100644
index 4c393806..00000000
--- a/libraries/portend.py
+++ /dev/null
@@ -1,212 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-A simple library for managing the availability of ports.
-"""
-
-from __future__ import print_function, division
-
-import time
-import socket
-import argparse
-import sys
-import itertools
-import contextlib
-import collections
-import platform
-
-from tempora import timing
-
-
-def client_host(server_host):
-	"""Return the host on which a client can connect to the given listener."""
-	if server_host == '0.0.0.0':
-		# 0.0.0.0 is INADDR_ANY, which should answer on localhost.
-		return '127.0.0.1'
-	if server_host in ('::', '::0', '::0.0.0.0'):
-		# :: is IN6ADDR_ANY, which should answer on localhost.
-		# ::0 and ::0.0.0.0 are non-canonical but common
-		# ways to write IN6ADDR_ANY.
-		return '::1'
-	return server_host
-
-
-class Checker(object):
-	def __init__(self, timeout=1.0):
-		self.timeout = timeout
-
-	def assert_free(self, host, port=None):
-		"""
-		Assert that the given addr is free
-		in that all attempts to connect fail within the timeout
-		or raise a PortNotFree exception.
-
-		>>> free_port = find_available_local_port()
-
-		>>> Checker().assert_free('localhost', free_port)
-		>>> Checker().assert_free('127.0.0.1', free_port)
-		>>> Checker().assert_free('::1', free_port)
-
-		Also accepts an addr tuple
-
-		>>> addr = '::1', free_port, 0, 0
-		>>> Checker().assert_free(addr)
-
-		Host might refer to a server bind address like '::', which
-		should use localhost to perform the check.
-
-		>>> Checker().assert_free('::', free_port)
-		"""
-		if port is None and isinstance(host, collections.Sequence):
-			host, port = host[:2]
-		if platform.system() == 'Windows':
-			host = client_host(host)
-		info = socket.getaddrinfo(
-			host, port, socket.AF_UNSPEC, socket.SOCK_STREAM,
-		)
-		list(itertools.starmap(self._connect, info))
-
-	def _connect(self, af, socktype, proto, canonname, sa):
-		s = socket.socket(af, socktype, proto)
-		# fail fast with a small timeout
-		s.settimeout(self.timeout)
-
-		with contextlib.closing(s):
-			try:
-				s.connect(sa)
-			except socket.error:
-				return
-
-		# the connect succeeded, so the port isn't free
-		port, host = sa[:2]
-		tmpl = "Port {port} is in use on {host}."
-		raise PortNotFree(tmpl.format(**locals()))
-
-
-class Timeout(IOError):
-	pass
-
-
-class PortNotFree(IOError):
-	pass
-
-
-def free(host, port, timeout=float('Inf')):
-	"""
-	Wait for the specified port to become free (dropping or rejecting
-	requests). Return when the port is free or raise a Timeout if timeout has
-	elapsed.
-
-	Timeout may be specified in seconds or as a timedelta.
-	If timeout is None or ∞, the routine will run indefinitely.
-
-	>>> free('localhost', find_available_local_port())
-	"""
-	if not host:
-		raise ValueError("Host values of '' or None are not allowed.")
-
-	timer = timing.Timer(timeout)
-
-	while not timer.expired():
-		try:
-			# Expect a free port, so use a small timeout
-			Checker(timeout=0.1).assert_free(host, port)
-			return
-		except PortNotFree:
-			# Politely wait.
-			time.sleep(0.1)
-
-	raise Timeout("Port {port} not free on {host}.".format(**locals()))
-wait_for_free_port = free
-
-
-def occupied(host, port, timeout=float('Inf')):
-	"""
-	Wait for the specified port to become occupied (accepting requests).
-	Return when the port is occupied or raise a Timeout if timeout has
-	elapsed.
-
-	Timeout may be specified in seconds or as a timedelta.
-	If timeout is None or ∞, the routine will run indefinitely.
-
-	>>> occupied('localhost', find_available_local_port(), .1) # doctest: +IGNORE_EXCEPTION_DETAIL
-	Traceback (most recent call last):
-	    ...
-	Timeout: Port ... not bound on localhost.
-	"""
-	if not host:
-		raise ValueError("Host values of '' or None are not allowed.")
-
-	timer = timing.Timer(timeout)
-
-	while not timer.expired():
-		try:
-			Checker(timeout=.5).assert_free(host, port)
-			# Politely wait
-			time.sleep(0.1)
-		except PortNotFree:
-			# port is occupied
-			return
-
-	raise Timeout("Port {port} not bound on {host}.".format(**locals()))
-wait_for_occupied_port = occupied
-
-
-def find_available_local_port():
-	"""
-	Find a free port on localhost.
-
-	>>> 0 < find_available_local_port() < 65536
-	True
-	"""
-	sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
-	addr = '', 0
-	sock.bind(addr)
-	addr, port = sock.getsockname()[:2]
-	sock.close()
-	return port
-
-
-class HostPort(str):
-	"""
-	A simple representation of a host/port pair as a string
-
-	>>> hp = HostPort('localhost:32768')
-
-	>>> hp.host
-	'localhost'
-
-	>>> hp.port
-	32768
-
-	>>> len(hp)
-	15
-	"""
-
-	@property
-	def host(self):
-		host, sep, port = self.partition(':')
-		return host
-
-	@property
-	def port(self):
-		host, sep, port = self.partition(':')
-		return int(port)
-
-
-def _main():
-	parser = argparse.ArgumentParser()
-	global_lookup = lambda key: globals()[key]
-	parser.add_argument('target', metavar='host:port', type=HostPort)
-	parser.add_argument('func', metavar='state', type=global_lookup)
-	parser.add_argument('-t', '--timeout', default=None, type=float)
-	args = parser.parse_args()
-	try:
-		args.func(args.target.host, args.target.port, timeout=args.timeout)
-	except Timeout as timeout:
-		print(timeout, file=sys.stderr)
-		raise SystemExit(1)
-
-
-if __name__ == '__main__':
-	_main()
diff --git a/libraries/tempora/__init__.py b/libraries/tempora/__init__.py
deleted file mode 100644
index e0cdead0..00000000
--- a/libraries/tempora/__init__.py
+++ /dev/null
@@ -1,505 +0,0 @@
-# -*- coding: UTF-8 -*-
-
-"Objects and routines pertaining to date and time (tempora)"
-
-from __future__ import division, unicode_literals
-
-import datetime
-import time
-import re
-import numbers
-import functools
-
-import six
-
-__metaclass__ = type
-
-
-class Parser:
-	"""
-	Datetime parser: parses a date-time string using multiple possible
-	formats.
-
-	>>> p = Parser(('%H%M', '%H:%M'))
-	>>> tuple(p.parse('1319'))
-	(1900, 1, 1, 13, 19, 0, 0, 1, -1)
-	>>> dateParser = Parser(('%m/%d/%Y', '%Y-%m-%d', '%d-%b-%Y'))
-	>>> tuple(dateParser.parse('2003-12-20'))
-	(2003, 12, 20, 0, 0, 0, 5, 354, -1)
-	>>> tuple(dateParser.parse('16-Dec-1994'))
-	(1994, 12, 16, 0, 0, 0, 4, 350, -1)
-	>>> tuple(dateParser.parse('5/19/2003'))
-	(2003, 5, 19, 0, 0, 0, 0, 139, -1)
-	>>> dtParser = Parser(('%Y-%m-%d %H:%M:%S', '%a %b %d %H:%M:%S %Y'))
-	>>> tuple(dtParser.parse('2003-12-20 19:13:26'))
-	(2003, 12, 20, 19, 13, 26, 5, 354, -1)
-	>>> tuple(dtParser.parse('Tue Jan 20 16:19:33 2004'))
-	(2004, 1, 20, 16, 19, 33, 1, 20, -1)
-
-	Be forewarned, a ValueError will be raised if more than one format
-	matches:
-
-	>>> Parser(('%H%M', '%H%M%S')).parse('732')
-	Traceback (most recent call last):
-		...
-	ValueError: More than one format string matched target 732.
-	"""
-
-	formats = ('%m/%d/%Y', '%m/%d/%y', '%Y-%m-%d', '%d-%b-%Y', '%d-%b-%y')
-	"some common default formats"
-
-	def __init__(self, formats=None):
-		if formats:
-			self.formats = formats
-
-	def parse(self, target):
-		self.target = target
-		results = tuple(filter(None, map(self._parse, self.formats)))
-		del self.target
-		if not results:
-			tmpl = "No format strings matched the target {target}."
-			raise ValueError(tmpl.format(**locals()))
-		if not len(results) == 1:
-			tmpl = "More than one format string matched target {target}."
-			raise ValueError(tmpl.format(**locals()))
-		return results[0]
-
-	def _parse(self, format):
-		try:
-			result = time.strptime(self.target, format)
-		except ValueError:
-			result = False
-		return result
-
-
-# some useful constants
-osc_per_year = 290091329207984000
-"""
-mean vernal equinox year expressed in oscillations of atomic cesium at the
-year 2000 (see http://webexhibits.org/calendars/timeline.html for more info).
-"""
-osc_per_second = 9192631770
-seconds_per_second = 1
-seconds_per_year = 31556940
-seconds_per_minute = 60
-minutes_per_hour = 60
-hours_per_day = 24
-seconds_per_hour = seconds_per_minute * minutes_per_hour
-seconds_per_day = seconds_per_hour * hours_per_day
-days_per_year = seconds_per_year / seconds_per_day
-thirty_days = datetime.timedelta(days=30)
-# these values provide useful averages
-six_months = datetime.timedelta(days=days_per_year / 2)
-seconds_per_month = seconds_per_year / 12
-hours_per_month = hours_per_day * days_per_year / 12
-
-
-def strftime(fmt, t):
-	"""A class to replace the strftime in datetime package or time module.
-	Identical to strftime behavior in those modules except supports any
-	year.
-	Also supports datetime.datetime times.
-	Also supports milliseconds using %s
-	Also supports microseconds using %u"""
-	if isinstance(t, (time.struct_time, tuple)):
-		t = datetime.datetime(*t[:6])
-	assert isinstance(t, (datetime.datetime, datetime.time, datetime.date))
-	try:
-		year = t.year
-		if year < 1900:
-			t = t.replace(year=1900)
-	except AttributeError:
-		year = 1900
-	subs = (
-		('%Y', '%04d' % year),
-		('%y', '%02d' % (year % 100)),
-		('%s', '%03d' % (t.microsecond // 1000)),
-		('%u', '%03d' % (t.microsecond % 1000))
-	)
-
-	def doSub(s, sub):
-		return s.replace(*sub)
-
-	def doSubs(s):
-		return functools.reduce(doSub, subs, s)
-
-	fmt = '%%'.join(map(doSubs, fmt.split('%%')))
-	return t.strftime(fmt)
-
-
-def strptime(s, fmt, tzinfo=None):
-	"""
-	A function to replace strptime in the time module.  Should behave
-	identically to the strptime function except it returns a datetime.datetime
-	object instead of a time.struct_time object.
-	Also takes an optional tzinfo parameter which is a time zone info object.
-	"""
-	res = time.strptime(s, fmt)
-	return datetime.datetime(tzinfo=tzinfo, *res[:6])
-
-
-class DatetimeConstructor:
-	"""
-	>>> cd = DatetimeConstructor.construct_datetime
-	>>> cd(datetime.datetime(2011,1,1))
-	datetime.datetime(2011, 1, 1, 0, 0)
-	"""
-	@classmethod
-	def construct_datetime(cls, *args, **kwargs):
-		"""Construct a datetime.datetime from a number of different time
-		types found in python and pythonwin"""
-		if len(args) == 1:
-			arg = args[0]
-			method = cls.__get_dt_constructor(
-				type(arg).__module__,
-				type(arg).__name__,
-			)
-			result = method(arg)
-			try:
-				result = result.replace(tzinfo=kwargs.pop('tzinfo'))
-			except KeyError:
-				pass
-			if kwargs:
-				first_key = kwargs.keys()[0]
-				tmpl = (
-					"{first_key} is an invalid keyword "
-					"argument for this function."
-				)
-				raise TypeError(tmpl.format(**locals()))
-		else:
-			result = datetime.datetime(*args, **kwargs)
-		return result
-
-	@classmethod
-	def __get_dt_constructor(cls, moduleName, name):
-		try:
-			method_name = '__dt_from_{moduleName}_{name}__'.format(**locals())
-			return getattr(cls, method_name)
-		except AttributeError:
-			tmpl = (
-				"No way to construct datetime.datetime from "
-				"{moduleName}.{name}"
-			)
-			raise TypeError(tmpl.format(**locals()))
-
-	@staticmethod
-	def __dt_from_datetime_datetime__(source):
-		dtattrs = (
-			'year', 'month', 'day', 'hour', 'minute', 'second',
-			'microsecond', 'tzinfo',
-		)
-		attrs = map(lambda a: getattr(source, a), dtattrs)
-		return datetime.datetime(*attrs)
-
-	@staticmethod
-	def __dt_from___builtin___time__(pyt):
-		"Construct a datetime.datetime from a pythonwin time"
-		fmtString = '%Y-%m-%d %H:%M:%S'
-		result = strptime(pyt.Format(fmtString), fmtString)
-		# get milliseconds and microseconds.  The only way to do this is
-		#  to use the __float__ attribute of the time, which is in days.
-		microseconds_per_day = seconds_per_day * 1000000
-		microseconds = float(pyt) * microseconds_per_day
-		microsecond = int(microseconds % 1000000)
-		result = result.replace(microsecond=microsecond)
-		return result
-
-	@staticmethod
-	def __dt_from_timestamp__(timestamp):
-		return datetime.datetime.utcfromtimestamp(timestamp)
-	__dt_from___builtin___float__ = __dt_from_timestamp__
-	__dt_from___builtin___long__ = __dt_from_timestamp__
-	__dt_from___builtin___int__ = __dt_from_timestamp__
-
-	@staticmethod
-	def __dt_from_time_struct_time__(s):
-		return datetime.datetime(*s[:6])
-
-
-def datetime_mod(dt, period, start=None):
-	"""
-	Find the time which is the specified date/time truncated to the time delta
-	relative to the start date/time.
-	By default, the start time is midnight of the same day as the specified
-	date/time.
-
-	>>> datetime_mod(datetime.datetime(2004, 1, 2, 3),
-	...     datetime.timedelta(days = 1.5),
-	...     start = datetime.datetime(2004, 1, 1))
-	datetime.datetime(2004, 1, 1, 0, 0)
-	>>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
-	...     datetime.timedelta(days = 1.5),
-	...     start = datetime.datetime(2004, 1, 1))
-	datetime.datetime(2004, 1, 2, 12, 0)
-	>>> datetime_mod(datetime.datetime(2004, 1, 2, 13),
-	...     datetime.timedelta(days = 7),
-	...     start = datetime.datetime(2004, 1, 1))
-	datetime.datetime(2004, 1, 1, 0, 0)
-	>>> datetime_mod(datetime.datetime(2004, 1, 10, 13),
-	...     datetime.timedelta(days = 7),
-	...     start = datetime.datetime(2004, 1, 1))
-	datetime.datetime(2004, 1, 8, 0, 0)
-	"""
-	if start is None:
-		# use midnight of the same day
-		start = datetime.datetime.combine(dt.date(), datetime.time())
-	# calculate the difference between the specified time and the start date.
-	delta = dt - start
-
-	# now aggregate the delta and the period into microseconds
-	# Use microseconds because that's the highest precision of these time
-	# pieces.  Also, using microseconds ensures perfect precision (no floating
-	# point errors).
-	def get_time_delta_microseconds(td):
-		return (td.days * seconds_per_day + td.seconds) * 1000000 + td.microseconds
-	delta, period = map(get_time_delta_microseconds, (delta, period))
-	offset = datetime.timedelta(microseconds=delta % period)
-	# the result is the original specified time minus the offset
-	result = dt - offset
-	return result
-
-
-def datetime_round(dt, period, start=None):
-	"""
-	Find the nearest even period for the specified date/time.
-
-	>>> datetime_round(datetime.datetime(2004, 11, 13, 8, 11, 13),
-	...     datetime.timedelta(hours = 1))
-	datetime.datetime(2004, 11, 13, 8, 0)
-	>>> datetime_round(datetime.datetime(2004, 11, 13, 8, 31, 13),
-	...     datetime.timedelta(hours = 1))
-	datetime.datetime(2004, 11, 13, 9, 0)
-	>>> datetime_round(datetime.datetime(2004, 11, 13, 8, 30),
-	...     datetime.timedelta(hours = 1))
-	datetime.datetime(2004, 11, 13, 9, 0)
-	"""
-	result = datetime_mod(dt, period, start)
-	if abs(dt - result) >= period // 2:
-		result += period
-	return result
-
-
-def get_nearest_year_for_day(day):
-	"""
-	Returns the nearest year to now inferred from a Julian date.
-	"""
-	now = time.gmtime()
-	result = now.tm_year
-	# if the day is far greater than today, it must be from last year
-	if day - now.tm_yday > 365 // 2:
-		result -= 1
-	# if the day is far less than today, it must be for next year.
-	if now.tm_yday - day > 365 // 2:
-		result += 1
-	return result
-
-
-def gregorian_date(year, julian_day):
-	"""
-	Gregorian Date is defined as a year and a julian day (1-based
-	index into the days of the year).
-
-	>>> gregorian_date(2007, 15)
-	datetime.date(2007, 1, 15)
-	"""
-	result = datetime.date(year, 1, 1)
-	result += datetime.timedelta(days=julian_day - 1)
-	return result
-
-
-def get_period_seconds(period):
-	"""
-	return the number of seconds in the specified period
-
-	>>> get_period_seconds('day')
-	86400
-	>>> get_period_seconds(86400)
-	86400
-	>>> get_period_seconds(datetime.timedelta(hours=24))
-	86400
-	>>> get_period_seconds('day + os.system("rm -Rf *")')
-	Traceback (most recent call last):
-	...
-	ValueError: period not in (second, minute, hour, day, month, year)
-	"""
-	if isinstance(period, six.string_types):
-		try:
-			name = 'seconds_per_' + period.lower()
-			result = globals()[name]
-		except KeyError:
-			msg = "period not in (second, minute, hour, day, month, year)"
-			raise ValueError(msg)
-	elif isinstance(period, numbers.Number):
-		result = period
-	elif isinstance(period, datetime.timedelta):
-		result = period.days * get_period_seconds('day') + period.seconds
-	else:
-		raise TypeError('period must be a string or integer')
-	return result
-
-
-def get_date_format_string(period):
-	"""
-	For a given period (e.g. 'month', 'day', or some numeric interval
-	such as 3600 (in secs)), return the format string that can be
-	used with strftime to format that time to specify the times
-	across that interval, but no more detailed.
-	For example,
-
-	>>> get_date_format_string('month')
-	'%Y-%m'
-	>>> get_date_format_string(3600)
-	'%Y-%m-%d %H'
-	>>> get_date_format_string('hour')
-	'%Y-%m-%d %H'
-	>>> get_date_format_string(None)
-	Traceback (most recent call last):
-		...
-	TypeError: period must be a string or integer
-	>>> get_date_format_string('garbage')
-	Traceback (most recent call last):
-		...
-	ValueError: period not in (second, minute, hour, day, month, year)
-	"""
-	# handle the special case of 'month' which doesn't have
-	#  a static interval in seconds
-	if isinstance(period, six.string_types) and period.lower() == 'month':
-		return '%Y-%m'
-	file_period_secs = get_period_seconds(period)
-	format_pieces = ('%Y', '-%m-%d', ' %H', '-%M', '-%S')
-	seconds_per_second = 1
-	intervals = (
-		seconds_per_year,
-		seconds_per_day,
-		seconds_per_hour,
-		seconds_per_minute,
-		seconds_per_second,
-	)
-	mods = list(map(lambda interval: file_period_secs % interval, intervals))
-	format_pieces = format_pieces[: mods.index(0) + 1]
-	return ''.join(format_pieces)
-
-
-def divide_timedelta_float(td, divisor):
-	"""
-	Divide a timedelta by a float value
-
-	>>> one_day = datetime.timedelta(days=1)
-	>>> half_day = datetime.timedelta(days=.5)
-	>>> divide_timedelta_float(one_day, 2.0) == half_day
-	True
-	>>> divide_timedelta_float(one_day, 2) == half_day
-	True
-	"""
-	# td is comprised of days, seconds, microseconds
-	dsm = [getattr(td, attr) for attr in ('days', 'seconds', 'microseconds')]
-	dsm = map(lambda elem: elem / divisor, dsm)
-	return datetime.timedelta(*dsm)
-
-
-def calculate_prorated_values():
-	"""
-	A utility function to prompt for a rate (a string in units per
-	unit time), and return that same rate for various time periods.
-	"""
-	rate = six.moves.input("Enter the rate (3/hour, 50/month)> ")
-	res = re.match('(?P<value>[\d.]+)/(?P<period>\w+)$', rate).groupdict()
-	value = float(res['value'])
-	value_per_second = value / get_period_seconds(res['period'])
-	for period in ('minute', 'hour', 'day', 'month', 'year'):
-		period_value = value_per_second * get_period_seconds(period)
-		print("per {period}: {period_value}".format(**locals()))
-
-
-def parse_timedelta(str):
-	"""
-	Take a string representing a span of time and parse it to a time delta.
-	Accepts any string of comma-separated numbers each with a unit indicator.
-
-	>>> parse_timedelta('1 day')
-	datetime.timedelta(days=1)
-
-	>>> parse_timedelta('1 day, 30 seconds')
-	datetime.timedelta(days=1, seconds=30)
-
-	>>> parse_timedelta('47.32 days, 20 minutes, 15.4 milliseconds')
-	datetime.timedelta(days=47, seconds=28848, microseconds=15400)
-
-	Supports weeks, months, years
-
-	>>> parse_timedelta('1 week')
-	datetime.timedelta(days=7)
-
-	>>> parse_timedelta('1 year, 1 month')
-	datetime.timedelta(days=395, seconds=58685)
-
-	Note that months and years strict intervals, not aligned
-	to a calendar:
-
-	>>> now = datetime.datetime.now()
-	>>> later = now + parse_timedelta('1 year')
-	>>> later.replace(year=now.year) - now
-	datetime.timedelta(seconds=20940)
-	"""
-	deltas = (_parse_timedelta_part(part.strip()) for part in str.split(','))
-	return sum(deltas, datetime.timedelta())
-
-
-def _parse_timedelta_part(part):
-	match = re.match('(?P<value>[\d.]+) (?P<unit>\w+)', part)
-	if not match:
-		msg = "Unable to parse {part!r} as a time delta".format(**locals())
-		raise ValueError(msg)
-	unit = match.group('unit').lower()
-	if not unit.endswith('s'):
-		unit += 's'
-	value = float(match.group('value'))
-	if unit == 'months':
-		unit = 'years'
-		value = value / 12
-	if unit == 'years':
-		unit = 'days'
-		value = value * days_per_year
-	return datetime.timedelta(**{unit: value})
-
-
-def divide_timedelta(td1, td2):
-	"""
-	Get the ratio of two timedeltas
-
-	>>> one_day = datetime.timedelta(days=1)
-	>>> one_hour = datetime.timedelta(hours=1)
-	>>> divide_timedelta(one_hour, one_day) == 1 / 24
-	True
-	"""
-	try:
-		return td1 / td2
-	except TypeError:
-		# Python 3.2 gets division
-		# http://bugs.python.org/issue2706
-		return td1.total_seconds() / td2.total_seconds()
-
-
-def date_range(start=None, stop=None, step=None):
-	"""
-	Much like the built-in function range, but works with dates
-
-	>>> range_items = date_range(
-	...     datetime.datetime(2005,12,21),
-	...     datetime.datetime(2005,12,25),
-	... )
-	>>> my_range = tuple(range_items)
-	>>> datetime.datetime(2005,12,21) in my_range
-	True
-	>>> datetime.datetime(2005,12,22) in my_range
-	True
-	>>> datetime.datetime(2005,12,25) in my_range
-	False
-	"""
-	if step is None:
-		step = datetime.timedelta(days=1)
-	if start is None:
-		start = datetime.datetime.now()
-	while start < stop:
-		yield start
-		start += step
diff --git a/libraries/tempora/schedule.py b/libraries/tempora/schedule.py
deleted file mode 100644
index 1ad093b2..00000000
--- a/libraries/tempora/schedule.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# -*- coding: utf-8 -*-
-
-"""
-Classes for calling functions a schedule.
-"""
-
-from __future__ import absolute_import
-
-import datetime
-import numbers
-import abc
-import bisect
-
-import pytz
-
-__metaclass__ = type
-
-
-def now():
-    """
-    Provide the current timezone-aware datetime.
-
-    A client may override this function to change the default behavior,
-    such as to use local time or timezone-naïve times.
-    """
-    return datetime.datetime.utcnow().replace(tzinfo=pytz.utc)
-
-
-def from_timestamp(ts):
-    """
-    Convert a numeric timestamp to a timezone-aware datetime.
-
-    A client may override this function to change the default behavior,
-    such as to use local time or timezone-naïve times.
-    """
-    return datetime.datetime.utcfromtimestamp(ts).replace(tzinfo=pytz.utc)
-
-
-class DelayedCommand(datetime.datetime):
-    """
-    A command to be executed after some delay (seconds or timedelta).
-    """
-
-    @classmethod
-    def from_datetime(cls, other):
-        return cls(
-            other.year, other.month, other.day, other.hour,
-            other.minute, other.second, other.microsecond,
-            other.tzinfo,
-        )
-
-    @classmethod
-    def after(cls, delay, target):
-        if not isinstance(delay, datetime.timedelta):
-            delay = datetime.timedelta(seconds=delay)
-        due_time = now() + delay
-        cmd = cls.from_datetime(due_time)
-        cmd.delay = delay
-        cmd.target = target
-        return cmd
-
-    @staticmethod
-    def _from_timestamp(input):
-        """
-        If input is a real number, interpret it as a Unix timestamp
-        (seconds sinc Epoch in UTC) and return a timezone-aware
-        datetime object. Otherwise return input unchanged.
-        """
-        if not isinstance(input, numbers.Real):
-            return input
-        return from_timestamp(input)
-
-    @classmethod
-    def at_time(cls, at, target):
-        """
-        Construct a DelayedCommand to come due at `at`, where `at` may be
-        a datetime or timestamp.
-        """
-        at = cls._from_timestamp(at)
-        cmd = cls.from_datetime(at)
-        cmd.delay = at - now()
-        cmd.target = target
-        return cmd
-
-    def due(self):
-        return now() >= self
-
-
-class PeriodicCommand(DelayedCommand):
-    """
-    Like a delayed command, but expect this command to run every delay
-    seconds.
-    """
-    def _next_time(self):
-        """
-        Add delay to self, localized
-        """
-        return self._localize(self + self.delay)
-
-    @staticmethod
-    def _localize(dt):
-        """
-        Rely on pytz.localize to ensure new result honors DST.
-        """
-        try:
-            tz = dt.tzinfo
-            return tz.localize(dt.replace(tzinfo=None))
-        except AttributeError:
-            return dt
-
-    def next(self):
-        cmd = self.__class__.from_datetime(self._next_time())
-        cmd.delay = self.delay
-        cmd.target = self.target
-        return cmd
-
-    def __setattr__(self, key, value):
-        if key == 'delay' and not value > datetime.timedelta():
-            raise ValueError(
-                "A PeriodicCommand must have a positive, "
-                "non-zero delay."
-            )
-        super(PeriodicCommand, self).__setattr__(key, value)
-
-
-class PeriodicCommandFixedDelay(PeriodicCommand):
-    """
-    Like a periodic command, but don't calculate the delay based on
-    the current time. Instead use a fixed delay following the initial
-    run.
-    """
-
-    @classmethod
-    def at_time(cls, at, delay, target):
-        at = cls._from_timestamp(at)
-        cmd = cls.from_datetime(at)
-        if isinstance(delay, numbers.Number):
-            delay = datetime.timedelta(seconds=delay)
-        cmd.delay = delay
-        cmd.target = target
-        return cmd
-
-    @classmethod
-    def daily_at(cls, at, target):
-        """
-        Schedule a command to run at a specific time each day.
-        """
-        daily = datetime.timedelta(days=1)
-        # convert when to the next datetime matching this time
-        when = datetime.datetime.combine(datetime.date.today(), at)
-        if when < now():
-            when += daily
-        return cls.at_time(cls._localize(when), daily, target)
-
-
-class Scheduler:
-    """
-    A rudimentary abstract scheduler accepting DelayedCommands
-    and dispatching them on schedule.
-    """
-    def __init__(self):
-        self.queue = []
-
-    def add(self, command):
-        assert isinstance(command, DelayedCommand)
-        bisect.insort(self.queue, command)
-
-    def run_pending(self):
-        while self.queue:
-            command = self.queue[0]
-            if not command.due():
-                break
-            self.run(command)
-            if isinstance(command, PeriodicCommand):
-                self.add(command.next())
-            del self.queue[0]
-
-    @abc.abstractmethod
-    def run(self, command):
-        """
-        Run the command
-        """
-
-
-class InvokeScheduler(Scheduler):
-    """
-    Command targets are functions to be invoked on schedule.
-    """
-    def run(self, command):
-        command.target()
-
-
-class CallbackScheduler(Scheduler):
-    """
-    Command targets are passed to a dispatch callable on schedule.
-    """
-    def __init__(self, dispatch):
-        super(CallbackScheduler, self).__init__()
-        self.dispatch = dispatch
-
-    def run(self, command):
-        self.dispatch(command.target)
diff --git a/libraries/tempora/tests/test_schedule.py b/libraries/tempora/tests/test_schedule.py
deleted file mode 100644
index 38eb8dc9..00000000
--- a/libraries/tempora/tests/test_schedule.py
+++ /dev/null
@@ -1,118 +0,0 @@
-import time
-import random
-import datetime
-
-import pytest
-import pytz
-import freezegun
-
-from tempora import schedule
-
-__metaclass__ = type
-
-
-@pytest.fixture
-def naive_times(monkeypatch):
-	monkeypatch.setattr(
-		'irc.schedule.from_timestamp',
-		datetime.datetime.fromtimestamp)
-	monkeypatch.setattr('irc.schedule.now', datetime.datetime.now)
-
-
-do_nothing = type(None)
-try:
-	do_nothing()
-except TypeError:
-	# Python 2 compat
-	def do_nothing():
-		return None
-
-
-def test_delayed_command_order():
-	"""
-	delayed commands should be sorted by delay time
-	"""
-	delays = [random.randint(0, 99) for x in range(5)]
-	cmds = sorted([
-		schedule.DelayedCommand.after(delay, do_nothing)
-		for delay in delays
-	])
-	assert [c.delay.seconds for c in cmds] == sorted(delays)
-
-
-def test_periodic_command_delay():
-	"A PeriodicCommand must have a positive, non-zero delay."
-	with pytest.raises(ValueError) as exc_info:
-		schedule.PeriodicCommand.after(0, None)
-	assert str(exc_info.value) == test_periodic_command_delay.__doc__
-
-
-def test_periodic_command_fixed_delay():
-	"""
-	Test that we can construct a periodic command with a fixed initial
-	delay.
-	"""
-	fd = schedule.PeriodicCommandFixedDelay.at_time(
-		at=schedule.now(),
-		delay=datetime.timedelta(seconds=2),
-		target=lambda: None,
-	)
-	assert fd.due() is True
-	assert fd.next().due() is False
-
-
-class TestCommands:
-	def test_delayed_command_from_timestamp(self):
-		"""
-		Ensure a delayed command can be constructed from a timestamp.
-		"""
-		t = time.time()
-		schedule.DelayedCommand.at_time(t, do_nothing)
-
-	def test_command_at_noon(self):
-		"""
-		Create a periodic command that's run at noon every day.
-		"""
-		when = datetime.time(12, 0, tzinfo=pytz.utc)
-		cmd = schedule.PeriodicCommandFixedDelay.daily_at(when, target=None)
-		assert cmd.due() is False
-		next_cmd = cmd.next()
-		daily = datetime.timedelta(days=1)
-		day_from_now = schedule.now() + daily
-		two_days_from_now = day_from_now + daily
-		assert day_from_now < next_cmd < two_days_from_now
-
-
-class TestTimezones:
-	def test_alternate_timezone_west(self):
-		target_tz = pytz.timezone('US/Pacific')
-		target = schedule.now().astimezone(target_tz)
-		cmd = schedule.DelayedCommand.at_time(target, target=None)
-		assert cmd.due()
-
-	def test_alternate_timezone_east(self):
-		target_tz = pytz.timezone('Europe/Amsterdam')
-		target = schedule.now().astimezone(target_tz)
-		cmd = schedule.DelayedCommand.at_time(target, target=None)
-		assert cmd.due()
-
-	def test_daylight_savings(self):
-		"""
-		A command at 9am should always be 9am regardless of
-		a DST boundary.
-		"""
-		with freezegun.freeze_time('2018-03-10 08:00:00'):
-			target_tz = pytz.timezone('US/Eastern')
-			target_time = datetime.time(9, tzinfo=target_tz)
-			cmd = schedule.PeriodicCommandFixedDelay.daily_at(
-				target_time,
-				target=lambda: None,
-			)
-
-		def naive(dt):
-			return dt.replace(tzinfo=None)
-
-		assert naive(cmd) == datetime.datetime(2018, 3, 10, 9, 0, 0)
-		next_ = cmd.next()
-		assert naive(next_) == datetime.datetime(2018, 3, 11, 9, 0, 0)
-		assert next_ - cmd == datetime.timedelta(hours=23)
diff --git a/libraries/tempora/timing.py b/libraries/tempora/timing.py
deleted file mode 100644
index 03c22454..00000000
--- a/libraries/tempora/timing.py
+++ /dev/null
@@ -1,219 +0,0 @@
-# -*- coding: utf-8 -*-
-
-from __future__ import unicode_literals, absolute_import
-
-import datetime
-import functools
-import numbers
-import time
-
-__metaclass__ = type
-
-
-class Stopwatch:
-	"""
-	A simple stopwatch which starts automatically.
-
-	>>> w = Stopwatch()
-	>>> _1_sec = datetime.timedelta(seconds=1)
-	>>> w.split() < _1_sec
-	True
-	>>> import time
-	>>> time.sleep(1.0)
-	>>> w.split() >= _1_sec
-	True
-	>>> w.stop() >= _1_sec
-	True
-	>>> w.reset()
-	>>> w.start()
-	>>> w.split() < _1_sec
-	True
-
-	It should be possible to launch the Stopwatch in a context:
-
-	>>> with Stopwatch() as watch:
-	...     assert isinstance(watch.split(), datetime.timedelta)
-
-	In that case, the watch is stopped when the context is exited,
-	so to read the elapsed time::
-
-	>>> watch.elapsed
-	datetime.timedelta(...)
-	>>> watch.elapsed.seconds
-	0
-	"""
-	def __init__(self):
-		self.reset()
-		self.start()
-
-	def reset(self):
-		self.elapsed = datetime.timedelta(0)
-		if hasattr(self, 'start_time'):
-			del self.start_time
-
-	def start(self):
-		self.start_time = datetime.datetime.utcnow()
-
-	def stop(self):
-		stop_time = datetime.datetime.utcnow()
-		self.elapsed += stop_time - self.start_time
-		del self.start_time
-		return self.elapsed
-
-	def split(self):
-		local_duration = datetime.datetime.utcnow() - self.start_time
-		return self.elapsed + local_duration
-
-	# context manager support
-	def __enter__(self):
-		self.start()
-		return self
-
-	def __exit__(self, exc_type, exc_value, traceback):
-		self.stop()
-
-
-class IntervalGovernor:
-	"""
-	Decorate a function to only allow it to be called once per
-	min_interval. Otherwise, it returns None.
-	"""
-	def __init__(self, min_interval):
-		if isinstance(min_interval, numbers.Number):
-			min_interval = datetime.timedelta(seconds=min_interval)
-		self.min_interval = min_interval
-		self.last_call = None
-
-	def decorate(self, func):
-		@functools.wraps(func)
-		def wrapper(*args, **kwargs):
-			allow = (
-				not self.last_call
-				or self.last_call.split() > self.min_interval
-			)
-			if allow:
-				self.last_call = Stopwatch()
-				return func(*args, **kwargs)
-		return wrapper
-
-	__call__ = decorate
-
-
-class Timer(Stopwatch):
-	"""
-	Watch for a target elapsed time.
-
-	>>> t = Timer(0.1)
-	>>> t.expired()
-	False
-	>>> __import__('time').sleep(0.15)
-	>>> t.expired()
-	True
-	"""
-	def __init__(self, target=float('Inf')):
-		self.target = self._accept(target)
-		super(Timer, self).__init__()
-
-	def _accept(self, target):
-		"Accept None or ∞ or datetime or numeric for target"
-		if isinstance(target, datetime.timedelta):
-			target = target.total_seconds()
-
-		if target is None:
-			# treat None as infinite target
-			target = float('Inf')
-
-		return target
-
-	def expired(self):
-		return self.split().total_seconds() > self.target
-
-
-class BackoffDelay:
-	"""
-	Exponential backoff delay.
-
-	Useful for defining delays between retries. Consider for use
-	with ``jaraco.functools.retry_call`` as the cleanup.
-
-	Default behavior has no effect; a delay or jitter must
-	be supplied for the call to be non-degenerate.
-
-	>>> bd = BackoffDelay()
-	>>> bd()
-	>>> bd()
-
-	The following instance will delay 10ms for the first call,
-	20ms for the second, etc.
-
-	>>> bd = BackoffDelay(delay=0.01, factor=2)
-	>>> bd()
-	>>> bd()
-
-	Inspect and adjust the state of the delay anytime.
-
-	>>> bd.delay
-	0.04
-	>>> bd.delay = 0.01
-
-	Set limit to prevent the delay from exceeding bounds.
-
-	>>> bd = BackoffDelay(delay=0.01, factor=2, limit=0.015)
-	>>> bd()
-	>>> bd.delay
-	0.015
-
-	Limit may be a callable taking a number and returning
-	the limited number.
-
-	>>> at_least_one = lambda n: max(n, 1)
-	>>> bd = BackoffDelay(delay=0.01, factor=2, limit=at_least_one)
-	>>> bd()
-	>>> bd.delay
-	1
-
-	Pass a jitter to add or subtract seconds to the delay.
-
-	>>> bd = BackoffDelay(jitter=0.01)
-	>>> bd()
-	>>> bd.delay
-	0.01
-
-	Jitter may be a callable. To supply a non-deterministic jitter
-	between -0.5 and 0.5, consider:
-
-	>>> import random
-	>>> jitter=functools.partial(random.uniform, -0.5, 0.5)
-	>>> bd = BackoffDelay(jitter=jitter)
-	>>> bd()
-	>>> 0 <= bd.delay <= 0.5
-	True
-	"""
-
-	delay = 0
-
-	factor = 1
-	"Multiplier applied to delay"
-
-	jitter = 0
-	"Number or callable returning extra seconds to add to delay"
-
-	def __init__(self, delay=0, factor=1, limit=float('inf'), jitter=0):
-		self.delay = delay
-		self.factor = factor
-		if isinstance(limit, numbers.Number):
-			limit_ = limit
-
-			def limit(n):
-				return max(0, min(limit_, n))
-		self.limit = limit
-		if isinstance(jitter, numbers.Number):
-			jitter_ = jitter
-
-			def jitter():
-				return jitter_
-		self.jitter = jitter
-
-	def __call__(self):
-		time.sleep(self.delay)
-		self.delay = self.limit(self.delay * self.factor + self.jitter())
diff --git a/libraries/tempora/utc.py b/libraries/tempora/utc.py
deleted file mode 100644
index 35bfdb06..00000000
--- a/libraries/tempora/utc.py
+++ /dev/null
@@ -1,36 +0,0 @@
-"""
-Facilities for common time operations in UTC.
-
-Inspired by the `utc project <https://pypi.org/project/utc>`_.
-
->>> dt = now()
->>> dt == fromtimestamp(dt.timestamp())
-True
->>> dt.tzinfo
-datetime.timezone.utc
-
->>> from time import time as timestamp
->>> now().timestamp() - timestamp() < 0.1
-True
-
->>> datetime(2018, 6, 26, 0).tzinfo
-datetime.timezone.utc
-
->>> time(0, 0).tzinfo
-datetime.timezone.utc
-"""
-
-import datetime as std
-import functools
-
-
-__all__ = ['now', 'fromtimestamp', 'datetime', 'time']
-
-
-now = functools.partial(std.datetime.now, std.timezone.utc)
-fromtimestamp = functools.partial(
-	std.datetime.fromtimestamp,
-	tz=std.timezone.utc,
-)
-datetime = functools.partial(std.datetime, tzinfo=std.timezone.utc)
-time = functools.partial(std.time, tzinfo=std.timezone.utc)
diff --git a/libraries/zc/__init__.py b/libraries/zc/__init__.py
deleted file mode 100644
index 146c3362..00000000
--- a/libraries/zc/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-__namespace__ = 'zc'
\ No newline at end of file
diff --git a/libraries/zc/lockfile/README.txt b/libraries/zc/lockfile/README.txt
deleted file mode 100644
index 89ef33e9..00000000
--- a/libraries/zc/lockfile/README.txt
+++ /dev/null
@@ -1,70 +0,0 @@
-Lock file support
-=================
-
-The ZODB lock_file module provides support for creating file system
-locks.  These are locks that are implemented with lock files and
-OS-provided locking facilities.  To create a lock, instantiate a
-LockFile object with a file name:
-
-    >>> import zc.lockfile
-    >>> lock = zc.lockfile.LockFile('lock')
-
-If we try to lock the same name, we'll get a lock error:
-
-    >>> import zope.testing.loggingsupport
-    >>> handler = zope.testing.loggingsupport.InstalledHandler('zc.lockfile')
-    >>> try:
-    ...     zc.lockfile.LockFile('lock')
-    ... except zc.lockfile.LockError:
-    ...     print("Can't lock file")
-    Can't lock file
-
-.. We don't log failure to acquire.
-
-    >>> for record in handler.records: # doctest: +ELLIPSIS
-    ...     print(record.levelname+' '+record.getMessage())
-
-To release the lock, use it's close method:
-
-    >>> lock.close()
-
-The lock file is not removed.  It is left behind:
-
-    >>> import os
-    >>> os.path.exists('lock')
-    True
-
-Of course, now that we've released the lock, we can create it again:
-
-    >>> lock = zc.lockfile.LockFile('lock')
-    >>> lock.close()
-
-.. Cleanup
-
-    >>> import os
-    >>> os.remove('lock')
-
-Hostname in lock file
-=====================
-
-In a container environment (e.g. Docker), the PID is typically always
-identical even if multiple containers are running under the same operating
-system instance.
-
-Clearly, inspecting lock files doesn't then help much in debugging. To identify
-the container which created the lock file, we need information about the
-container in the lock file. Since Docker uses the container identifier or name
-as the hostname, this information can be stored in the lock file in addition to
-or instead of the PID.
-
-Use the ``content_template`` keyword argument to ``LockFile`` to specify a
-custom lock file content format:
-
-    >>> lock = zc.lockfile.LockFile('lock', content_template='{pid};{hostname}')
-    >>> lock.close()
-
-If you now inspected the lock file, you would see e.g.:
-
-    $ cat lock
-     123;myhostname
-
diff --git a/libraries/zc/lockfile/__init__.py b/libraries/zc/lockfile/__init__.py
deleted file mode 100644
index a0ac2ff1..00000000
--- a/libraries/zc/lockfile/__init__.py
+++ /dev/null
@@ -1,104 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2001, 2002 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE
-#
-##############################################################################
-
-import os
-import errno
-import logging
-logger = logging.getLogger("zc.lockfile")
-
-class LockError(Exception):
-    """Couldn't get a lock
-    """
-
-try:
-    import fcntl
-except ImportError:
-    try:
-        import msvcrt
-    except ImportError:
-        def _lock_file(file):
-            raise TypeError('No file-locking support on this platform')
-        def _unlock_file(file):
-            raise TypeError('No file-locking support on this platform')
-
-    else:
-        # Windows
-        def _lock_file(file):
-            # Lock just the first byte
-            try:
-                msvcrt.locking(file.fileno(), msvcrt.LK_NBLCK, 1)
-            except IOError:
-                raise LockError("Couldn't lock %r" % file.name)
-
-        def _unlock_file(file):
-            try:
-                file.seek(0)
-                msvcrt.locking(file.fileno(), msvcrt.LK_UNLCK, 1)
-            except IOError:
-                raise LockError("Couldn't unlock %r" % file.name)
-
-else:
-    # Unix
-    _flags = fcntl.LOCK_EX | fcntl.LOCK_NB
-
-    def _lock_file(file):
-        try:
-            fcntl.flock(file.fileno(), _flags)
-        except IOError:
-            raise LockError("Couldn't lock %r" % file.name)
-
-    def _unlock_file(file):
-        fcntl.flock(file.fileno(), fcntl.LOCK_UN)
-
-class LazyHostName(object):
-    """Avoid importing socket and calling gethostname() unnecessarily"""
-    def __str__(self):
-        import socket
-        return socket.gethostname()
-
-
-class LockFile:
-
-    _fp = None
-
-    def __init__(self, path, content_template='{pid}'):
-        self._path = path
-        try:
-            # Try to open for writing without truncation:
-            fp = open(path, 'r+')
-        except IOError:
-            # If the file doesn't exist, we'll get an IO error, try a+
-            # Note that there may be a race here. Multiple processes
-            # could fail on the r+ open and open the file a+, but only
-            # one will get the the lock and write a pid.
-            fp = open(path, 'a+')
-
-        try:
-            _lock_file(fp)
-        except:
-            fp.close()
-            raise
-
-        # We got the lock, record info in the file.
-        self._fp = fp
-        fp.write(" %s\n" % content_template.format(pid=os.getpid(),
-                                                   hostname=LazyHostName()))
-        fp.truncate()
-        fp.flush()
-
-    def close(self):
-        if self._fp is not None:
-            _unlock_file(self._fp)
-            self._fp.close()
-            self._fp = None
diff --git a/libraries/zc/lockfile/tests.py b/libraries/zc/lockfile/tests.py
deleted file mode 100644
index e9fcbff3..00000000
--- a/libraries/zc/lockfile/tests.py
+++ /dev/null
@@ -1,193 +0,0 @@
-##############################################################################
-#
-# Copyright (c) 2004 Zope Foundation and Contributors.
-# All Rights Reserved.
-#
-# This software is subject to the provisions of the Zope Public License,
-# Version 2.1 (ZPL).  A copy of the ZPL should accompany this distribution.
-# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY AND ALL EXPRESS OR IMPLIED
-# WARRANTIES ARE DISCLAIMED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF TITLE, MERCHANTABILITY, AGAINST INFRINGEMENT, AND FITNESS
-# FOR A PARTICULAR PURPOSE.
-#
-##############################################################################
-import os, re, sys, unittest, doctest
-import zc.lockfile, time, threading
-from zope.testing import renormalizing, setupstack
-import tempfile
-try:
-    from unittest.mock import Mock, patch
-except ImportError:
-    from mock import Mock, patch
-
-checker = renormalizing.RENormalizing([
-    # Python 3 adds module path to error class name.
-    (re.compile("zc\.lockfile\.LockError:"),
-     r"LockError:"),
-    ])
-
-def inc():
-    while 1:
-        try:
-            lock = zc.lockfile.LockFile('f.lock')
-        except zc.lockfile.LockError:
-            continue
-        else:
-            break
-    f = open('f', 'r+b')
-    v = int(f.readline().strip())
-    time.sleep(0.01)
-    v += 1
-    f.seek(0)
-    f.write(('%d\n' % v).encode('ASCII'))
-    f.close()
-    lock.close()
-
-def many_threads_read_and_write():
-    r"""
-    >>> with open('f', 'w+b') as file:
-    ...     _ = file.write(b'0\n')
-    >>> with open('f.lock', 'w+b') as file:
-    ...     _ = file.write(b'0\n')
-
-    >>> n = 50
-    >>> threads = [threading.Thread(target=inc) for i in range(n)]
-    >>> _ = [thread.start() for thread in threads]
-    >>> _ = [thread.join() for thread in threads]
-    >>> with open('f', 'rb') as file:
-    ...     saved = int(file.read().strip())
-    >>> saved == n
-    True
-
-    >>> os.remove('f')
-
-    We should only have one pid in the lock file:
-
-    >>> f = open('f.lock')
-    >>> len(f.read().strip().split())
-    1
-    >>> f.close()
-
-    >>> os.remove('f.lock')
-
-    """
-
-def pid_in_lockfile():
-    r"""
-    >>> import os, zc.lockfile
-    >>> pid = os.getpid()
-    >>> lock = zc.lockfile.LockFile("f.lock")
-    >>> f = open("f.lock")
-    >>> _ = f.seek(1)
-    >>> f.read().strip() == str(pid)
-    True
-    >>> f.close()
-
-    Make sure that locking twice does not overwrite the old pid:
-
-    >>> lock = zc.lockfile.LockFile("f.lock")
-    Traceback (most recent call last):
-      ...
-    LockError: Couldn't lock 'f.lock'
-
-    >>> f = open("f.lock")
-    >>> _ = f.seek(1)
-    >>> f.read().strip() == str(pid)
-    True
-    >>> f.close()
-
-    >>> lock.close()
-    """
-
-
-def hostname_in_lockfile():
-    r"""
-    hostname is correctly written into the lock file when it's included in the
-    lock file content template
-
-    >>> import zc.lockfile
-    >>> with patch('socket.gethostname', Mock(return_value='myhostname')):
-    ...     lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}')
-    >>> f = open("f.lock")
-    >>> _ = f.seek(1)
-    >>> f.read().rstrip()
-    'myhostname'
-    >>> f.close()
-
-    Make sure that locking twice does not overwrite the old hostname:
-
-    >>> lock = zc.lockfile.LockFile("f.lock", content_template='{hostname}')
-    Traceback (most recent call last):
-      ...
-    LockError: Couldn't lock 'f.lock'
-
-    >>> f = open("f.lock")
-    >>> _ = f.seek(1)
-    >>> f.read().rstrip()
-    'myhostname'
-    >>> f.close()
-
-    >>> lock.close()
-    """
-
-
-class TestLogger(object):
-    def __init__(self):
-        self.log_entries = []
-
-    def exception(self, msg, *args):
-        self.log_entries.append((msg,) + args)
-
-
-class LockFileLogEntryTestCase(unittest.TestCase):
-    """Tests for logging in case of lock failure"""
-    def setUp(self):
-        self.here = os.getcwd()
-        self.tmp = tempfile.mkdtemp(prefix='zc.lockfile-test-')
-        os.chdir(self.tmp)
-
-    def tearDown(self):
-        os.chdir(self.here)
-        setupstack.rmtree(self.tmp)
-
-    def test_log_formatting(self):
-        # PID and hostname are parsed and logged from lock file on failure
-        with patch('os.getpid', Mock(return_value=123)):
-            with patch('socket.gethostname', Mock(return_value='myhostname')):
-                lock = zc.lockfile.LockFile('f.lock',
-                                            content_template='{pid}/{hostname}')
-                with open('f.lock') as f:
-                    self.assertEqual(' 123/myhostname\n', f.read())
-
-                lock.close()
-
-    def test_unlock_and_lock_while_multiprocessing_process_running(self):
-        import multiprocessing
-
-        lock = zc.lockfile.LockFile('l')
-        q = multiprocessing.Queue()
-        p = multiprocessing.Process(target=q.get)
-        p.daemon = True
-        p.start()
-
-        # release and re-acquire should work (obviously)
-        lock.close()
-        lock = zc.lockfile.LockFile('l')
-        self.assertTrue(p.is_alive())
-
-        q.put(0)
-        lock.close()
-        p.join()
-
-
-def test_suite():
-    suite = unittest.TestSuite()
-    suite.addTest(doctest.DocFileSuite(
-        'README.txt', checker=checker,
-        setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown))
-    suite.addTest(doctest.DocTestSuite(
-        setUp=setupstack.setUpDirectory, tearDown=setupstack.tearDown,
-        checker=checker))
-    # Add unittest test cases from this module
-    suite.addTest(unittest.defaultTestLoader.loadTestsFromName(__name__))
-    return suite
diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py
index 7f692846..512b844c 100644
--- a/resources/lib/webservice.py
+++ b/resources/lib/webservice.py
@@ -2,13 +2,13 @@
 
 #################################################################################################
 
+import BaseHTTPServer
 import logging
+import httplib
 import threading
+import urlparse
 
 import xbmc
-import xbmcvfs
-
-import cherrypy
 
 #################################################################################################
 
@@ -17,60 +17,125 @@ LOG = logging.getLogger("EMBY."+__name__)
 
 #################################################################################################
 
-
-class Root(object):
-
-    @cherrypy.expose
-    def default(self, *args, **kwargs):
-
-        try:
-            if not kwargs.get('Id').isdigit():
-                raise IndexError("Incorrect Id format: %s" % kwargs.get('Id'))
-
-            LOG.info("Webservice called with params: %s", kwargs)
-
-            return ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s"
-                    % (kwargs.get('Id'), kwargs.get('KodiId'), kwargs.get('Name'), kwargs.get('transcode') or False))
-
-        except IndexError as error:
-            LOG.error(error)
-
-            raise cherrypy.HTTPError(404, error)
-
-        except Exception as error:
-            LOG.exception(error)
-
-            raise cherrypy.HTTPError(500, "Exception occurred: %s" % error)
-
 class WebService(threading.Thread):
 
-    root = None
+    ''' Run a webservice to trigger playback.
+    '''
+    stop_thread = False
 
     def __init__(self):
-
-        self.root = Root()
-        cherrypy.config.update({
-            'engine.autoreload.on' : False,
-            'log.screen': False,
-            'engine.timeout_monitor.frequency': 5,
-            'server.shutdown_timeout': 1,
-        })
         threading.Thread.__init__(self)
 
-    def run(self):
-        
-        LOG.info("--->[ webservice/%s ]", PORT)
-        conf = {
-            'global': {
-                'server.socket_host': '0.0.0.0',
-                'server.socket_port': PORT
-            }, '/': {}
-        }
-        cherrypy.quickstart(self.root, '/', conf)
-
     def stop(self):
 
-        cherrypy.engine.exit()
-        self.join(0)
+        ''' Called when the thread needs to stop
+        '''
+        try:
+            conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT)
+            conn.request("QUIT", "/")
+            conn.getresponse()
+            self.stop_thread = True
+        except Exception as error:
+            pass
+
+    def run(self):
+
+        ''' Called to start the webservice.
+        '''
+        LOG.info("--->[ webservice/%s ]", PORT)
+
+        try:
+            server = HttpServer(('127.0.0.1', PORT), requestHandler)
+            server.serve_forever()
+        except Exception as error:
+
+            if '10053' not in error: # ignore host diconnected errors
+                LOG.exception(error)
+
+        LOG.info("---<[ webservice ]")
+
+
+class HttpServer(BaseHTTPServer.HTTPServer):
+
+    ''' Http server that reacts to self.stop flag.
+    '''
+    def serve_forever(self):
+
+        ''' Handle one request at a time until stopped.
+        '''
+        self.stop = False
+
+        while not self.stop:
+            self.handle_request()
+
+
+class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
+
+    #Handler for the GET requests
+
+    def log_message(self, format, *args):
+
+        ''' Mute the webservice requests.
+        '''
+        pass
+
+    def get_params(self):
+
+        ''' Get the params
+        '''
+        try:
+            path = self.path[1:]
+
+            if '?' in path:
+                path = path.split('?', 1)[1]
+
+            params = dict(urlparse.parse_qsl(path))
+        except Exception:
+            params = {}
+
+        return params
+
+    def do_HEAD(self):
+
+        ''' Called on HEAD requests
+        '''
+        self.send_response(200)
+        self.end_headers()
+
+        return
+
+    def do_GET(self):
+
+        ''' Return plugin path
+        '''
+        try:
+            params = self.get_params()
+
+            if not params:
+                raise IndexError("Incomplete URL format")
+
+            if not params.get('Id').isdigit():
+                raise IndexError("Incorrect Id format %s" % params.get('Id'))
+
+            xbmc.log("[ webservice ] path: %s params: %s" % (str(self.path), str(params)), xbmc.LOGWARNING)
+
+            path = ("plugin://plugin.video.emby?mode=play&id=%s&dbid=%s&filename=%s&transcode=%s"
+                    % (params.get('Id'), params.get('KodiId'), params.get('Name'), params.get('transcode') or False))
+
+            self.send_response(200)
+            self.send_header('Content-type','text/html')
+            self.end_headers()
+            self.wfile.write(path)
+
+        except IndexError as error:
+
+            xbmc.log(str(error), xbmc.LOGWARNING)
+            self.send_error(404, "Exception occurred: %s" % error)
+
+        except Exception as error:
+
+            xbmc.log(str(error), xbmc.LOGWARNING)
+            self.send_error(500, "Exception occurred: %s" % error)
+
+        return
 
-        del self.root

From c9d51690dfb166d7c3216f45335872ebb045616e Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Thu, 31 Jan 2019 07:25:21 -0600
Subject: [PATCH 09/12] Fix Kodi hangup on exit

---
 resources/lib/webservice.py | 15 +++++++++++----
 1 file changed, 11 insertions(+), 4 deletions(-)

diff --git a/resources/lib/webservice.py b/resources/lib/webservice.py
index 512b844c..9e5f9d33 100644
--- a/resources/lib/webservice.py
+++ b/resources/lib/webservice.py
@@ -21,8 +21,6 @@ class WebService(threading.Thread):
 
     ''' Run a webservice to trigger playback.
     '''
-    stop_thread = False
-
     def __init__(self):
         threading.Thread.__init__(self)
 
@@ -34,7 +32,6 @@ class WebService(threading.Thread):
             conn = httplib.HTTPConnection("127.0.0.1:%d" % PORT)
             conn.request("QUIT", "/")
             conn.getresponse()
-            self.stop_thread = True
         except Exception as error:
             pass
 
@@ -71,7 +68,9 @@ class HttpServer(BaseHTTPServer.HTTPServer):
 
 class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
 
-    #Handler for the GET requests
+    ''' Http request handler. Do not use LOG here,
+        it will hang requests in Kodi > show information dialog.
+    '''
 
     def log_message(self, format, *args):
 
@@ -79,6 +78,14 @@ class requestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
         '''
         pass
 
+    def do_QUIT(self):
+
+        ''' send 200 OK response, and set server.stop to True
+        '''
+        self.send_response(200)
+        self.end_headers()
+        self.server.stop = True
+
     def get_params(self):
 
         ''' Get the params

From 4ebcb05ed5a56bb48af9ef931736b2f0fb34190e Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Fri, 1 Feb 2019 03:07:47 -0600
Subject: [PATCH 10/12] Update local krypton

---
 resources/lib/objects/__init__.py     | 2 +-
 resources/lib/objects/kodi/queries.py | 2 +-
 2 files changed, 2 insertions(+), 2 deletions(-)

diff --git a/resources/lib/objects/__init__.py b/resources/lib/objects/__init__.py
index 55eb734d..6695119b 100644
--- a/resources/lib/objects/__init__.py
+++ b/resources/lib/objects/__init__.py
@@ -1,4 +1,4 @@
-version = "171076029"
+version = "171076030"
 
 from movies import Movies
 from musicvideos import MusicVideos
diff --git a/resources/lib/objects/kodi/queries.py b/resources/lib/objects/kodi/queries.py
index c823bf93..d2c37110 100644
--- a/resources/lib/objects/kodi/queries.py
+++ b/resources/lib/objects/kodi/queries.py
@@ -355,7 +355,7 @@ update_path_tvshow_obj =    [   "{Path}",None,None,1,"{PathId}"
                             ]
 update_path_episode_obj =   [   "{Path}",None,None,1,"{PathId}"
                             ]
-update_path_mvideo_obj =    [   "{Path}","musicvideos","metadata.local",1,"{PathId}"
+update_path_mvideo_obj =    [   "{Path}","musicvideos",None,1,"{PathId}"
                             ]
 update_file =   		"""	UPDATE 	files 
 							SET 	idPath = ?, strFilename = ?, dateAdded = ? 

From e966b613b049d82a17fe194cabc468494509c1d9 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Fri, 1 Feb 2019 03:09:21 -0600
Subject: [PATCH 11/12] Ensure userid is set for token

---
 resources/lib/emby/core/http.py | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/resources/lib/emby/core/http.py b/resources/lib/emby/core/http.py
index e7d585ea..8b8a6f83 100644
--- a/resources/lib/emby/core/http.py
+++ b/resources/lib/emby/core/http.py
@@ -222,7 +222,7 @@ class HTTP(object):
 
         data['headers'].update({'Authorization': auth})
 
-        if self.config['auth.token']:
+        if self.config['auth.token'] and self.config['auth.user_id']:
 
             auth += ', UserId=%s' % self.config['auth.user_id'].encode('utf-8')
             data['headers'].update({'Authorization': auth, 'X-MediaBrowser-Token': self.config['auth.token'].encode('utf-8')})

From 021d29fc24e31c1eac70ca34928f01ce91ab6c01 Mon Sep 17 00:00:00 2001
From: angelblue05 <angelblue.dev@gmail.com>
Date: Fri, 1 Feb 2019 03:14:14 -0600
Subject: [PATCH 12/12] Version bump 4.0.2

---
 addon.xml | 15 +++++++++------
 1 file changed, 9 insertions(+), 6 deletions(-)

diff --git a/addon.xml b/addon.xml
index e3789fe0..2474e0c5 100644
--- a/addon.xml
+++ b/addon.xml
@@ -1,13 +1,13 @@
 <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
 <addon  id="plugin.video.emby"
         name="Emby" 
-        version="4.0.1"
+        version="4.0.2"
         provider-name="angelblue05">
   <requires>
     <import addon="xbmc.python" version="2.25.0"/>
-    <import addon="plugin.video.emby.movies" version="0.13" />
-    <import addon="plugin.video.emby.tvshows" version="0.13" />
-    <import addon="plugin.video.emby.musicvideos" version="0.13" />
+    <import addon="plugin.video.emby.movies" version="0.14" />
+    <import addon="plugin.video.emby.tvshows" version="0.14" />
+    <import addon="plugin.video.emby.musicvideos" version="0.14" />
   </requires>
   <extension    point="xbmc.python.pluginsource"
                 library="default.py">
@@ -38,8 +38,11 @@
     <description lang="en">Welcome to Emby for Kodi A whole new way to manage and view your media library. The Emby addon for Kodi combines the best of Kodi - ultra smooth navigation, beautiful UIs and playback of any file under the sun, and Emby - the most powerful fully open source multi-client media metadata indexer and server.&#10;&#10;Emby for Kodi is the absolute best way to enjoy the incredible Kodi playback engine combined with the power of Emby's centralized database. Features: Direct integration with the Kodi library for native Kodi speed Instant synchronization with the Emby server Full support for Movie, TV and Music collections Emby Server direct stream and transcoding support - use Kodi when you are away from home!</description>
     <news>
       New stable release
-      The wiki has been updated: https://github.com/MediaBrowser/plugin.video.emby/wiki
-      Small fixes
+      The wiki has been updated, PLEASE READ: https://github.com/MediaBrowser/plugin.video.emby/wiki
+      Fix playback for Kodi Leia
+      Fix masterlock
+      Home videos and pictures now show under videos and picture add-ons
+      Dependencies were updated to 0.14!
     </news>
   </extension>
 </addon>