diff --git a/.mailmap b/.mailmap
new file mode 100644
index 0000000..947e0e9
--- /dev/null
+++ b/.mailmap
@@ -0,0 +1,2 @@
+Dependabot      <support@dependabot.com> 
+Ernst Sjöstrand <ernst.sjostrand@sonymobile.com>
diff --git a/.travis.yml b/.travis.yml
new file mode 100644
index 0000000..a72290f
--- /dev/null
+++ b/.travis.yml
@@ -0,0 +1,24 @@
+sudo: required
+dist: trusty
+
+services:
+  - docker
+
+language: python
+matrix:
+    include:
+        - python: 3.5
+          dist: trusty
+          sudo: false
+        - python: 3.6
+          dist: trusty
+          sudo: false
+        - python: 3.7
+          dist: xenial
+          sudo: true
+
+install:
+  - pip install pipenv
+
+script:
+  - make test
diff --git a/AUTHORS b/AUTHORS
index 64a9e30..97a187e 100644
--- a/AUTHORS
+++ b/AUTHORS
@@ -7,16 +7,20 @@ Christopher Zee <christopher.xs.zee@gmail.com>
 David Pursehouse <david.pursehouse@gmail.com>
 David Pursehouse <david.pursehouse@sonyericsson.com>
 David Pursehouse <david.pursehouse@sonymobile.com>
-Ernst Sjostrand <ernst.sjostrand@sonymobile.com>
+Dependabot <support@dependabot.com>
+Ernst Sjöstrand <ernst.sjostrand@sonymobile.com>
 Fabien Boucher <fabien.boucher@enovance.com>
 Gabriel Féron <feron.gabriel@gmail.com>
 George Peristerakis <gperiste@redhat.com>
 Jens Andersen <jens.andersen@gmail.com>
 Johannes Richter <johannes.richter@kernkonzept.com>
 Justin Simon <jls5177@gmail.com>
+Magnus Bäck <magnus.back@axis.com>
 Marcin Płonka <mplonka@gmail.com>
+Martin Wallgren <martinwa@axis.com>
 Nikki Heald <nicky@notnowlewis.com>
 Peter Theckanath <peter.xa.theckanath@sonymobile.com>
 Vasiliy Kleschov <vkleschov@cloudlinux.com>
 Vineet Naik <vineet@helpshift.com>
-dependabot[bot] <support@dependabot.com>
+z-w-k <welcome.albert.z@gmail.com>
+z-w-k <welcome.albert.z@gmial.com>
diff --git a/ChangeLog b/ChangeLog
index edb824d..216adcb 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,6 +1,135 @@
 CHANGES
 =======
 
+2.0.9
+-----
+
+* Run livetests against 2.15.11 and 2.16.6
+* Don't attempt to parse empty response content
+* Raise exception for error status before attempting to parse content
+* Add debug log of response content type, status code and encoding
+* Add a test for PUT request with a json file
+* Bump pytest from 4.2.1 to 4.3.0
+* Bump pytest from 4.2.0 to 4.2.1
+* Run live tests against 2.15.10 and 2.16.5
+* Bump pytest from 4.1.1 to 4.2.0
+* Fixed an issue with duplicate output log message
+* Run livetests against 2.15.9 and 2.16.4
+* Set logger format and name
+* Run livetests with 2.14.18, 2.15.8 and 2.16.3
+* Bump pytest from 4.1.0 to 4.1.1
+* Bump pytest from 4.0.2 to 4.1.0
+* Run livetests with 2.16.2
+* Bump pytest from 4.0.1 to 4.0.2
+* Add dependabot badge on the pypi description file
+* Run livetests against 2.16.1
+* Bump requests from 2.20.1 to 2.21.0
+* Bump pytest from 4.0.0 to 4.0.1
+* Drop livetests against 2.13.x
+* Run livetests against 2.16 final release
+* Bump pytest from 3.10.1 to 4.0.0
+* Remove unnecessary warning log
+* Run livetests against 2.14.17 and 2.15.7
+* Bump pytest from 3.10.0 to 3.10.1
+
+2.0.8
+-----
+
+* Bump requests from 2.20.0 to 2.20.1
+* Run livetests against 2.16-rc3
+* Bump pbr from 5.1.0 to 5.1.1
+* Bump pytest from 3.9.3 to 3.10.0
+* Run livetests against 2.16-rc1
+* CVE-2018-18074: Upgrade requests to >=2.20.0
+* Run livetests against 2.16-rc0
+* Bump pytest from 3.9.2 to 3.9.3
+* Run livetests against 2.14.16 and 2.15.6
+* Bump pbr from 5.0.0 to 5.1.0
+* Bump pytest from 3.9.1 to 3.9.2
+* Add dependabot status badge
+* Bump pytest from 3.8.2 to 3.9.1
+* Bump pydocstyle from 2.1.1 to 3.0.0
+* Bump pbr from 4.2.0 to 4.3.0
+* Run livetests against 2.14.14 and 2.15.4
+* Bump pytest from 3.8.1 to 3.8.2
+* Bump pytest from 3.8.0 to 3.8.1
+* Run livetests against 2.14.13
+* Run livetests against 2.14.12
+* Bump pytest from 3.7.4 to 3.8.0
+* Bump pytest from 3.7.3 to 3.7.4
+* Update dependencies
+* Run livetests with Gerrit 2.14.11
+* Update Programming Language classifiers
+
+2.0.7
+-----
+
+* Run livetests with Gerrit 2.15.3
+* Update pipenv
+* Run livetests with Gerrit 2.14.10
+* Allow data to be sent as \`dict\` and automatically convert it to \`json\`
+* Travis: Also test with Python 3.7
+* Run style checks before livetests and unit tests
+* livetests: Remove redundant comments
+* README: Tested on OSX with Python 3.7
+* README: Add brief info about history and difference from pygerrit
+* Update Pipfile dependencies
+* Allow requests up to version 2.19.1
+* Update livetests to use 2.14.9
+* Add back an RST version of the project description
+
+2.0.6
+-----
+
+* Test that an exception is raised for invalid auth type
+* Default to HTTP basic auth from netrc when auth is not specified
+* Add tests for HTTP{Basic|Digest}AuthFromNetrc classes
+* Use svg versions of shields.io badges
+* Remove unused 'requests' link from README
+* Update prerequisites
+* No need to mention dependency on 'requests' in the README
+* Update Gerrit documentation link to 2.15.2
+* Update sample code in README
+* Add build status badge
+* Add a test to confirm that GET works with patch zip file
+* flake8: Add back the line length parameter
+* Make all useful classes importable from root package
+* Simplify setup and tests
+* Add Travis CI config
+* Add .pytest\_cache/ to .gitignore
+* Add Pipfile
+* Wrap livetests inside a class and separate to individual test methods
+* tox: Add missing -s on pytest commands
+* Bump pytest from 3.6.0 to 3.6.1
+* Update compatibility section in README
+* Fix author email field
+* Metadata: Promote development status to production/stable
+* Metadata: Add topic classifier
+* Metadata: Add operating system classifiers, and platform field
+* Remove README.rst
+* Metadata: Add 'information technology' to intended audience classifier
+* Metadata: Update programming language classifiers
+* Metadata: Add a couple more keywords
+* Metadata: Fix keywords field formatting
+
+2.0.5
+-----
+
+* Only run livetests with tox in py36 environment
+* Update tox environments
+* Run pytest in tox
+* Use Gerrit 2.13.11 from gerritcodereview/gerrit
+* Allow override of kwargs in get
+* Add livetests for 2.13.11
+* Refactor livetest to make use of pytest parameterization
+* Abstract creation of Gerrit docker container into GerritContainer
+* Run live server tests against Gerrit running in Docker container
+* Invoke tests with pytest
+* Add .mailmap
+* Add basic tests running against a live Gerrit server
+* Fixes #18: Explicitly set content type header when posting review
+* Remove docs build
+
 2.0.4
 -----
 
diff --git a/MANIFEST.in b/MANIFEST.in
index 738616d..b2cc4f5 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -1 +1 @@
-include README.rst LICENSE requirements.txt
\ No newline at end of file
+include README.md LICENSE requirements.txt
diff --git a/Makefile b/Makefile
index 87d45a3..8f10b4e 100644
--- a/Makefile
+++ b/Makefile
@@ -20,74 +20,30 @@
 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 # THE SOFTWARE.
 
-PWD := $(shell pwd)
-VERSION := $(shell git describe)
+FILES := $(shell git ls-files | grep py$$)
 
-VIRTUALENV := $(shell which virtualenv)
-ifeq ($(wildcard $(VIRTUALENV)),)
-  $(error virtualenv must be available)
-endif
+test: clean pyflakes pep8 pydocstyle unittests livetests
 
-PIP := $(shell which pip)
-ifeq ($(wildcard $(PIP)),)
-  $(error pip must be available)
-endif
-
-REQUIRED_VIRTUALENV ?= 1.10
-VIRTUALENV_OK := $(shell expr `virtualenv --version | \
-    cut -f2 -d' '` \>= $(REQUIRED_VIRTUALENV))
-
-all: test
-
-test: clean unittests pyflakes pep8 pydocstyle
-
-sdist: valid-virtualenv test
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          python setup.py sdist"
-
-valid-virtualenv:
-ifeq ($(VIRTUALENV_OK),0)
-  $(error virtualenv version $(REQUIRED_VIRTUALENV) or higher is needed)
-endif
+sdist: test
+	pipenv run python setup.py sdist
 
 pydocstyle: testenvsetup
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          git ls-files | grep \"\.py$$\" | grep -v docs | xargs pydocstyle"
+	pipenv run pydocstyle $(FILES)
 
 pep8: testenvsetup
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          git ls-files | grep \"\.py$$\" | grep -v docs | xargs flake8 --max-line-length 80"
+	pipenv run flake8 $(FILES) --max-line-length 80
 
 pyflakes: testenvsetup
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          git ls-files | grep \"\.py$$\" | grep -v docs | xargs pyflakes"
+	pipenv run pyflakes $(FILES)
 
 unittests: testenvsetup
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          python unittests.py"
-
-testenvsetup: envsetup
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          pip install --upgrade -r test_requirements.txt"
-
-docenvsetup: envsetup
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          pip install --upgrade -r doc_requirements.txt"
+	pipenv run pytest -sv unittests.py
 
-envsetup: envinit
-	bash -c "\
-          source ./pygerrit2env/bin/activate && \
-          pip install --upgrade -r requirements.txt"
+livetests: testenvsetup
+	pipenv run pytest -sv livetests.py
 
-envinit:
-	bash -c "[ -e ./pygerrit2env/bin/activate ] || virtualenv --system-site-packages ./pygerrit2env"
+testenvsetup:
+	pipenv install --dev
 
 clean:
 	@find . -type f -name "*.pyc" -exec rm -f {} \;
diff --git a/PKG-INFO b/PKG-INFO
index 7c03978..3c5bc6c 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,80 +1,24 @@
 Metadata-Version: 1.1
 Name: pygerrit2
-Version: 2.0.4
+Version: 2.0.9
 Summary: Client library for interacting with Gerrit's REST API
 Home-page: https://github.com/dpursehouse/pygerrit2
 Author: David Pursehouse
 Author-email: david.pursehouse@gmail.com
 License: The MIT License
-Description-Content-Type: UNKNOWN
 Description: Pygerrit2 - Client library for interacting with Gerrit Code Review's REST API
         =============================================================================
         
-        .. image:: https://img.shields.io/pypi/v/pygerrit2.png
+        .. image:: https://img.shields.io/pypi/v/pygerrit2.svg
         
-        .. image:: https://img.shields.io/pypi/l/pygerrit2.png
+        .. image:: https://img.shields.io/pypi/l/pygerrit2.svg
         
-        Pygerrit2 provides a simple interface for clients to interact with
-        `Gerrit Code Review`_ via the REST API.
-        
-        Prerequisites
-        -------------
-        
-        Pygerrit2 is compatible with Python 2.6 and Python 2.7.  Support for Python 3
-        is experimental.
-        
-        Pygerrit2 depends on the `requests`_ library.
-        
-        
-        Installation
-        ------------
-        
-        To install pygerrit2, simply::
-        
-            $ pip install pygerrit2
-        
-        
-        Usage
-        -----
+        .. image:: https://travis-ci.org/dpursehouse/pygerrit2.svg?branch=master
         
-        This simple example shows how to get the user's open changes. Authentication
-        to Gerrit is done via HTTP Basic authentication, using an explicitly given
-        username and password::
+        .. image:: https://api.dependabot.com/badges/status?host=github&repo=dpursehouse/pygerrit2
         
-            >>> from requests.auth import HTTPBasicAuth
-            >>> from pygerrit2.rest import GerritRestAPI
-            >>> auth = HTTPBasicAuth('username', 'password')
-            >>> rest = GerritRestAPI(url='http://review.example.net', auth=auth)
-            >>> changes = rest.get("/changes/?q=owner:self%20status:open")
-        
-        Note that is is not necessary to add the ``/a/`` prefix on the endpoint
-        URLs. This is automatically added when the API is instantiated with an
-        authentication object.
-        
-        If the user's HTTP username and password are defined in the ``.netrc``
-        file::
-        
-            machine review.example.net login MyUsername password MyPassword
-        
-        then it is possible to authenticate with those credentials::
-        
-            >>> from pygerrit2.rest import GerritRestAPI
-            >>> from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc
-            >>> url = 'http://review.example.net'
-            >>> auth = HTTPBasicAuthFromNetrc(url=url)
-            >>> rest = GerritRestAPI(url=url, auth=auth)
-            >>> changes = rest.get("/changes/?q=owner:self%20status:open")
-        
-        Note that the HTTP password is not the same as the SSH password. For
-        instructions on how to obtain the HTTP password, refer to Gerrit's
-        `HTTP upload settings`_ documentation.
-        
-        Also note that in Gerrit version 2.14, support for HTTP Digest authentication
-        was removed and only HTTP Basic authentication is supported. When using
-        pygerrit2 against an earlier Gerrit version, it may be necessary to replace
-        the `HTTPBasic...` classes with the corresponding `HTTPDigest...` versions.
-        
-        Refer to the `example`_ script for a full working example.
+        Pygerrit2 provides a simple interface for clients to interact with
+        `Gerrit Code Review`_ via the REST API.
         
         Copyright and License
         ---------------------
@@ -89,21 +33,22 @@ Description: Pygerrit2 - Client library for interacting with Gerrit Code Review'
         license details.
         
         .. _`Gerrit Code Review`: https://gerritcodereview.com/
-        .. _`requests`: https://github.com/kennethreitz/requests
-        .. _example: https://github.com/dpursehouse/pygerrit2/blob/master/example.py
-        .. _`HTTP upload settings`: https://gerrit-documentation.storage.googleapis.com/Documentation/2.14/user-upload.html#http
         .. _LICENSE: https://github.com/dpursehouse/pygerrit2/blob/master/LICENSE
         
         
-Keywords: gerrit
-rest
-http
-Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
+Keywords: gerrit rest http api client
+Platform: POSIX, Unix, MacOS
+Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Console
 Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Information Technology
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Natural Language :: English
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: Unix
+Classifier: Operating System :: MacOS
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/Pipfile b/Pipfile
new file mode 100644
index 0000000..f1240bc
--- /dev/null
+++ b/Pipfile
@@ -0,0 +1,18 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+requests = "*"
+"e1839a8" = {path = ".", editable = true}
+
+[dev-packages]
+pbr = "*"
+pydocstyle = "*"
+flake8 = "==3.5.0"
+pyflakes = "==1.6.0"
+pytest = "*"
+testcontainers = "*"
+mock = "*"
+"e1839a8" = {path = ".", editable = true}
diff --git a/Pipfile.lock b/Pipfile.lock
new file mode 100644
index 0000000..cdea669
--- /dev/null
+++ b/Pipfile.lock
@@ -0,0 +1,274 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "a8c67af401a06f8474eb4174de9bbe58628333daae1f33291edffcd97e70a6e4"
+        },
+        "pipfile-spec": 6,
+        "requires": {},
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "certifi": {
+            "hashes": [
+                "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
+                "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
+            ],
+            "version": "==2018.11.29"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+            ],
+            "version": "==3.0.4"
+        },
+        "e1839a8": {
+            "editable": true,
+            "path": "."
+        },
+        "idna": {
+            "hashes": [
+                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
+                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+            ],
+            "version": "==2.8"
+        },
+        "pbr": {
+            "hashes": [
+                "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2",
+                "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b"
+            ],
+            "version": "==5.1.2"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
+                "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
+            ],
+            "index": "pypi",
+            "version": "==2.21.0"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
+                "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
+            ],
+            "version": "==1.24.1"
+        }
+    },
+    "develop": {
+        "atomicwrites": {
+            "hashes": [
+                "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
+                "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
+            ],
+            "version": "==1.3.0"
+        },
+        "attrs": {
+            "hashes": [
+                "sha256:10cbf6e27dbce8c30807caf056c8eb50917e0eaafe86347671b57254006c3e69",
+                "sha256:ca4be454458f9dec299268d472aaa5a11f67a4ff70093396e1ceae9c76cf4bbb"
+            ],
+            "version": "==18.2.0"
+        },
+        "blindspin": {
+            "hashes": [
+                "sha256:31c4e93d4ae2ef6765e3c42460456814c17addd5add8298dced21fe9dc50f496",
+                "sha256:b30ef7a26ef1637c1d047667b279e8c15dc2a78fcfaad6b3027d7217e752bdba"
+            ],
+            "version": "==2.0.1"
+        },
+        "certifi": {
+            "hashes": [
+                "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7",
+                "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033"
+            ],
+            "version": "==2018.11.29"
+        },
+        "chardet": {
+            "hashes": [
+                "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae",
+                "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691"
+            ],
+            "version": "==3.0.4"
+        },
+        "colorama": {
+            "hashes": [
+                "sha256:05eed71e2e327246ad6b38c540c4a3117230b19679b875190486ddd2d721422d",
+                "sha256:f8ac84de7840f5b9c4e3347b3c1eaa50f7e49c2b07596221daec5edaabbd7c48"
+            ],
+            "version": "==0.4.1"
+        },
+        "crayons": {
+            "hashes": [
+                "sha256:5e17691605e564d63482067eb6327d01a584bbaf870beffd4456a3391bd8809d",
+                "sha256:6f51241d0c4faec1c04c1c0ac6a68f1d66a4655476ce1570b3f37e5166a599cc"
+            ],
+            "version": "==0.1.2"
+        },
+        "docker": {
+            "hashes": [
+                "sha256:2840ffb9dc3ef6d00876bde476690278ab13fa1f8ba9127ef855ac33d00c3152",
+                "sha256:5831256da3477723362bc71a8df07b8cd8493e4a4a60cebd45580483edbe48ae"
+            ],
+            "version": "==3.7.0"
+        },
+        "docker-pycreds": {
+            "hashes": [
+                "sha256:6ce3270bcaf404cc4c3e27e4b6c70d3521deae82fb508767870fdbf772d584d4",
+                "sha256:7266112468627868005106ec19cd0d722702d2b7d5912a28e19b826c3d37af49"
+            ],
+            "version": "==0.4.0"
+        },
+        "e1839a8": {
+            "editable": true,
+            "path": "."
+        },
+        "flake8": {
+            "hashes": [
+                "sha256:7253265f7abd8b313e3892944044a365e3f4ac3fcdcfb4298f55ee9ddf188ba0",
+                "sha256:c7841163e2b576d435799169b78703ad6ac1bbb0f199994fc05f700b2a90ea37"
+            ],
+            "index": "pypi",
+            "version": "==3.5.0"
+        },
+        "idna": {
+            "hashes": [
+                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
+                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+            ],
+            "version": "==2.8"
+        },
+        "mccabe": {
+            "hashes": [
+                "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42",
+                "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"
+            ],
+            "version": "==0.6.1"
+        },
+        "mock": {
+            "hashes": [
+                "sha256:5ce3c71c5545b472da17b72268978914d0252980348636840bd34a00b5cc96c1",
+                "sha256:b158b6df76edd239b8208d481dc46b6afd45a846b7812ff0ce58971cf5bc8bba"
+            ],
+            "index": "pypi",
+            "version": "==2.0.0"
+        },
+        "more-itertools": {
+            "hashes": [
+                "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40",
+                "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1"
+            ],
+            "markers": "python_version > '2.7'",
+            "version": "==6.0.0"
+        },
+        "pbr": {
+            "hashes": [
+                "sha256:a7953f66e1f82e4b061f43096a4bcc058f7d3d41de9b94ac871770e8bdd831a2",
+                "sha256:d717573351cfe09f49df61906cd272abaa759b3e91744396b804965ff7bff38b"
+            ],
+            "version": "==5.1.2"
+        },
+        "pluggy": {
+            "hashes": [
+                "sha256:8ddc32f03971bfdf900a81961a48ccf2fb677cf7715108f85295c67405798616",
+                "sha256:980710797ff6a041e9a73a5787804f848996ecaa6f8a1b1e08224a5894f2074a"
+            ],
+            "version": "==0.8.1"
+        },
+        "py": {
+            "hashes": [
+                "sha256:bf92637198836372b520efcba9e020c330123be8ce527e535d185ed4b6f45694",
+                "sha256:e76826342cefe3c3d5f7e8ee4316b80d1dd8a300781612ddbc765c17ba25a6c6"
+            ],
+            "version": "==1.7.0"
+        },
+        "pycodestyle": {
+            "hashes": [
+                "sha256:682256a5b318149ca0d2a9185d365d8864a768a28db66a84a2ea946bcc426766",
+                "sha256:6c4245ade1edfad79c3446fadfc96b0de2759662dc29d07d80a6f27ad1ca6ba9"
+            ],
+            "version": "==2.3.1"
+        },
+        "pydocstyle": {
+            "hashes": [
+                "sha256:2258f9b0df68b97bf3a6c29003edc5238ff8879f1efb6f1999988d934e432bd8",
+                "sha256:5741c85e408f9e0ddf873611085e819b809fca90b619f5fd7f34bd4959da3dd4",
+                "sha256:ed79d4ec5e92655eccc21eb0c6cf512e69512b4a97d215ace46d17e4990f2039"
+            ],
+            "index": "pypi",
+            "version": "==3.0.0"
+        },
+        "pyflakes": {
+            "hashes": [
+                "sha256:08bd6a50edf8cffa9fa09a463063c425ecaaf10d1eb0335a7e8b1401aef89e6f",
+                "sha256:8d616a382f243dbf19b54743f280b80198be0bca3a5396f1d2e1fca6223e8805"
+            ],
+            "index": "pypi",
+            "version": "==1.6.0"
+        },
+        "pytest": {
+            "hashes": [
+                "sha256:067a1d4bf827ffdd56ad21bd46674703fce77c5957f6c1eef731f6146bfcef1c",
+                "sha256:9687049d53695ad45cf5fdc7bbd51f0c49f1ea3ecfc4b7f3fde7501b541f17f4"
+            ],
+            "index": "pypi",
+            "version": "==4.3.0"
+        },
+        "requests": {
+            "hashes": [
+                "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e",
+                "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b"
+            ],
+            "index": "pypi",
+            "version": "==2.21.0"
+        },
+        "six": {
+            "hashes": [
+                "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c",
+                "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73"
+            ],
+            "version": "==1.12.0"
+        },
+        "snowballstemmer": {
+            "hashes": [
+                "sha256:919f26a68b2c17a7634da993d91339e288964f93c274f1343e3bbbe2096e1128",
+                "sha256:9f3bcd3c401c3e862ec0ebe6d2c069ebc012ce142cce209c098ccb5b09136e89"
+            ],
+            "version": "==1.2.1"
+        },
+        "testcontainers": {
+            "hashes": [
+                "sha256:46d316ac19aa7257c604d9a2e89e0db4cd67007aeb5ae6d8d7975ba163ec23ff"
+            ],
+            "index": "pypi",
+            "version": "==2.3"
+        },
+        "urllib3": {
+            "hashes": [
+                "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39",
+                "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22"
+            ],
+            "version": "==1.24.1"
+        },
+        "websocket-client": {
+            "hashes": [
+                "sha256:8c8bf2d4f800c3ed952df206b18c28f7070d9e3dcbd6ca6291127574f57ee786",
+                "sha256:e51562c91ddb8148e791f0155fdb01325d99bb52c4cdbb291aee7a3563fd0849"
+            ],
+            "version": "==0.54.0"
+        },
+        "wrapt": {
+            "hashes": [
+                "sha256:4aea003270831cceb8a90ff27c4031da6ead7ec1886023b80ce0dfe0adf61533"
+            ],
+            "version": "==1.11.1"
+        }
+    }
+}
diff --git a/README.md b/README.md
index 03687ef..85195e4 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,30 @@
 # Pygerrit2 - Client library for interacting with Gerrit Code Review's REST API
 
-![Version](https://img.shields.io/pypi/v/pygerrit2.png)
-![License](https://img.shields.io/pypi/l/pygerrit2.png)
+![Version](https://img.shields.io/pypi/v/pygerrit2.svg)
+![License](https://img.shields.io/pypi/l/pygerrit2.svg)
+[![Build Status](https://travis-ci.org/dpursehouse/pygerrit2.svg?branch=master)](https://travis-ci.org/dpursehouse/pygerrit2)
+[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=dpursehouse/pygerrit2)](https://dependabot.com)
 
 Pygerrit2 provides a simple interface for clients to interact with
-[Gerrit Code Review][gerrit] via the REST API.
+[Gerrit Code Review][gerrit] via the REST API. It is based on [pygerrit][pygerrit]
+which was originally developed at Sony Mobile, but is no longer
+actively maintained.
+
+Unlike the original pygerrit, pygerrit2 does not provide any SSH
+interface. Users who require an SSH interface should continue to use
+[pygerrit][pygerrit].
 
 ## Prerequisites
 
-Pygerrit2 is compatible with Python 2.6 and Python 2.7.  Support for Python 3
-is experimental.
+Pygerrit2 is tested on the following platforms and Python versions:
 
-Pygerrit2 depends on the [requests library][requests].
+Platform | Python version(s)
+-------- | -----------------
+OSX | 3.7
+Ubuntu (trusty) | 3.5, 3.6
+Ubuntu (xenial) | 3.7
 
+Support for Python 2.x is no longer guaranteed.
 
 ## Installation
 
@@ -29,8 +41,7 @@ to Gerrit is done via HTTP Basic authentication, using an explicitly given
 username and password:
 
 ```python
-from requests.auth import HTTPBasicAuth
-from pygerrit2.rest import GerritRestAPI
+from pygerrit2 import GerritRestAPI, HTTPBasicAuth
 
 auth = HTTPBasicAuth('username', 'password')
 rest = GerritRestAPI(url='http://review.example.net', auth=auth)
@@ -51,8 +62,7 @@ machine review.example.net login MyUsername password MyPassword
 then it is possible to authenticate with those credentials:
 
 ```python
-from pygerrit2.rest import GerritRestAPI
-from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc
+from pygerrit2 import GerritRestAPI, HTTPBasicAuthFromNetrc
 
 url = 'http://review.example.net'
 auth = HTTPBasicAuthFromNetrc(url=url)
@@ -60,6 +70,10 @@ rest = GerritRestAPI(url=url, auth=auth)
 changes = rest.get("/changes/?q=owner:self%20status:open")
 ```
 
+If no `auth` parameter is specified, pygerrit2 will attempt to find
+credentials in the `netrc` and use them with HTTP basic auth. If no
+credentials are found, it will fall back to using no authentication.
+
 Note that the HTTP password is not the same as the SSH password. For
 instructions on how to obtain the HTTP password, refer to Gerrit's
 [HTTP upload settings documentation][settings].
@@ -84,7 +98,7 @@ Licensed under The MIT License.  Please refer to the [LICENSE file][license]
 for full license details.
 
 [gerrit]: https://gerritcodereview.com/
-[requests]: https://github.com/kennethreitz/requests
 [example]: https://github.com/dpursehouse/pygerrit2/blob/master/example.py
-[settings]: https://gerrit-documentation.storage.googleapis.com/Documentation/2.14/user-upload.html#http
+[settings]: https://gerrit-documentation.storage.googleapis.com/Documentation/2.15.2/user-upload.html#http
 [license]: https://github.com/dpursehouse/pygerrit2/blob/master/LICENSE
+[pygerrit]: https://github.com/sonyxperiadev/pygerrit
diff --git a/README.rst b/README.rst
deleted file mode 100644
index 00542f7..0000000
--- a/README.rst
+++ /dev/null
@@ -1,86 +0,0 @@
-Pygerrit2 - Client library for interacting with Gerrit Code Review's REST API
-=============================================================================
-
-.. image:: https://img.shields.io/pypi/v/pygerrit2.png
-
-.. image:: https://img.shields.io/pypi/l/pygerrit2.png
-
-Pygerrit2 provides a simple interface for clients to interact with
-`Gerrit Code Review`_ via the REST API.
-
-Prerequisites
--------------
-
-Pygerrit2 is compatible with Python 2.6 and Python 2.7.  Support for Python 3
-is experimental.
-
-Pygerrit2 depends on the `requests`_ library.
-
-
-Installation
-------------
-
-To install pygerrit2, simply::
-
-    $ pip install pygerrit2
-
-
-Usage
------
-
-This simple example shows how to get the user's open changes. Authentication
-to Gerrit is done via HTTP Basic authentication, using an explicitly given
-username and password::
-
-    >>> from requests.auth import HTTPBasicAuth
-    >>> from pygerrit2.rest import GerritRestAPI
-    >>> auth = HTTPBasicAuth('username', 'password')
-    >>> rest = GerritRestAPI(url='http://review.example.net', auth=auth)
-    >>> changes = rest.get("/changes/?q=owner:self%20status:open")
-
-Note that is is not necessary to add the ``/a/`` prefix on the endpoint
-URLs. This is automatically added when the API is instantiated with an
-authentication object.
-
-If the user's HTTP username and password are defined in the ``.netrc``
-file::
-
-    machine review.example.net login MyUsername password MyPassword
-
-then it is possible to authenticate with those credentials::
-
-    >>> from pygerrit2.rest import GerritRestAPI
-    >>> from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc
-    >>> url = 'http://review.example.net'
-    >>> auth = HTTPBasicAuthFromNetrc(url=url)
-    >>> rest = GerritRestAPI(url=url, auth=auth)
-    >>> changes = rest.get("/changes/?q=owner:self%20status:open")
-
-Note that the HTTP password is not the same as the SSH password. For
-instructions on how to obtain the HTTP password, refer to Gerrit's
-`HTTP upload settings`_ documentation.
-
-Also note that in Gerrit version 2.14, support for HTTP Digest authentication
-was removed and only HTTP Basic authentication is supported. When using
-pygerrit2 against an earlier Gerrit version, it may be necessary to replace
-the `HTTPBasic...` classes with the corresponding `HTTPDigest...` versions.
-
-Refer to the `example`_ script for a full working example.
-
-Copyright and License
----------------------
-
-Copyright 2011 Sony Ericsson Mobile Communications. All rights reserved.
-
-Copyright 2012 Sony Mobile Communications. All rights reserved.
-
-Copyright 2016 David Pursehouse. All rights reserved.
-
-Licensed under The MIT License.  Please refer to the `LICENSE`_ file for full
-license details.
-
-.. _`Gerrit Code Review`: https://gerritcodereview.com/
-.. _`requests`: https://github.com/kennethreitz/requests
-.. _example: https://github.com/dpursehouse/pygerrit2/blob/master/example.py
-.. _`HTTP upload settings`: https://gerrit-documentation.storage.googleapis.com/Documentation/2.14/user-upload.html#http
-.. _LICENSE: https://github.com/dpursehouse/pygerrit2/blob/master/LICENSE
diff --git a/debian/changelog b/debian/changelog
index 58d586f..98cd063 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+python-pygerrit2 (2.0.9-1) UNRELEASED; urgency=medium
+
+  * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk>  Sun, 26 Jan 2020 09:06:00 +0000
+
 python-pygerrit2 (2.0.4-2) unstable; urgency=medium
 
   * Team upload.
diff --git a/description.rst b/description.rst
new file mode 100644
index 0000000..0fee925
--- /dev/null
+++ b/description.rst
@@ -0,0 +1,28 @@
+Pygerrit2 - Client library for interacting with Gerrit Code Review's REST API
+=============================================================================
+
+.. image:: https://img.shields.io/pypi/v/pygerrit2.svg
+
+.. image:: https://img.shields.io/pypi/l/pygerrit2.svg
+
+.. image:: https://travis-ci.org/dpursehouse/pygerrit2.svg?branch=master
+
+.. image:: https://api.dependabot.com/badges/status?host=github&repo=dpursehouse/pygerrit2
+
+Pygerrit2 provides a simple interface for clients to interact with
+`Gerrit Code Review`_ via the REST API.
+
+Copyright and License
+---------------------
+
+Copyright 2011 Sony Ericsson Mobile Communications. All rights reserved.
+
+Copyright 2012 Sony Mobile Communications. All rights reserved.
+
+Copyright 2016 David Pursehouse. All rights reserved.
+
+Licensed under The MIT License.  Please refer to the `LICENSE`_ file for full
+license details.
+
+.. _`Gerrit Code Review`: https://gerritcodereview.com/
+.. _LICENSE: https://github.com/dpursehouse/pygerrit2/blob/master/LICENSE
diff --git a/docs/Makefile b/docs/Makefile
deleted file mode 100644
index 7e96dc3..0000000
--- a/docs/Makefile
+++ /dev/null
@@ -1,20 +0,0 @@
-# Minimal makefile for Sphinx documentation
-#
-
-# You can set these variables from the command line.
-SPHINXOPTS    =
-SPHINXBUILD   = python -msphinx
-SPHINXPROJ    = pygerrit2
-SOURCEDIR     = .
-BUILDDIR      = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
-	@$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option.  $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
-	@$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100644
index b25a4c8..0000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,168 +0,0 @@
-# -*- coding: utf-8 -*-
-#
-# pygerrit2 documentation build configuration file, created by
-# sphinx-quickstart on Sun Oct 22 17:32:32 2017.
-#
-# This file is execfile()d with the current directory set to its
-# containing dir.
-#
-# Note that not all possible configuration values are present in this
-# autogenerated file.
-#
-# All configuration values have a default; values that are commented out
-# serve to show the default.
-
-# If extensions (or modules to document with autodoc) are in another directory,
-# add these directories to sys.path here. If the directory is relative to the
-# documentation root, use os.path.abspath to make it absolute, like shown here.
-#
-# import os
-# import sys
-# sys.path.insert(0, os.path.abspath('.'))
-
-
-# -- General configuration ------------------------------------------------
-
-# If your documentation needs a minimal Sphinx version, state it here.
-#
-# needs_sphinx = '1.0'
-
-# Add any Sphinx extension module names here, as strings. They can be
-# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
-# ones.
-extensions = ['sphinx.ext.autodoc']
-
-# Add any paths that contain templates here, relative to this directory.
-templates_path = ['_templates']
-
-# The suffix(es) of source filenames.
-# You can specify multiple suffix as a list of string:
-#
-# source_suffix = ['.rst', '.md']
-source_suffix = '.rst'
-
-# The master toctree document.
-master_doc = 'index'
-
-# General information about the project.
-project = u'pygerrit2'
-copyright = u'2017, David Pursehouse'
-author = u'David Pursehouse'
-
-# The version info for the project you're documenting, acts as replacement for
-# |version| and |release|, also used in various other places throughout the
-# built documents.
-#
-# The short X.Y version.
-version = u''
-# The full version, including alpha/beta/rc tags.
-release = u''
-
-# The language for content autogenerated by Sphinx. Refer to documentation
-# for a list of supported languages.
-#
-# This is also used if you do content translation via gettext catalogs.
-# Usually you set "language" from the command line for these cases.
-language = None
-
-# List of patterns, relative to source directory, that match files and
-# directories to ignore when looking for source files.
-# This patterns also effect to html_static_path and html_extra_path
-exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store']
-
-# The name of the Pygments (syntax highlighting) style to use.
-pygments_style = 'sphinx'
-
-# If true, `todo` and `todoList` produce output, else they produce nothing.
-todo_include_todos = False
-
-
-# -- Options for HTML output ----------------------------------------------
-
-# The theme to use for HTML and HTML Help pages.  See the documentation for
-# a list of builtin themes.
-#
-html_theme = 'alabaster'
-
-# Theme options are theme-specific and customize the look and feel of a theme
-# further.  For a list of options available for each theme, see the
-# documentation.
-#
-# html_theme_options = {}
-
-# Add any paths that contain custom static files (such as style sheets) here,
-# relative to this directory. They are copied after the builtin static files,
-# so a file named "default.css" will overwrite the builtin "default.css".
-html_static_path = ['_static']
-
-# Custom sidebar templates, must be a dictionary that maps document names
-# to template names.
-#
-# This is required for the alabaster theme
-# refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars
-html_sidebars = {
-    '**': [
-        'about.html',
-        'navigation.html',
-        'relations.html',  # needs 'show_related': True theme option to display
-        'searchbox.html',
-        'donate.html',
-    ]
-}
-
-
-# -- Options for HTMLHelp output ------------------------------------------
-
-# Output file base name for HTML help builder.
-htmlhelp_basename = 'pygerrit2doc'
-
-
-# -- Options for LaTeX output ---------------------------------------------
-
-latex_elements = {
-    # The paper size ('letterpaper' or 'a4paper').
-    #
-    # 'papersize': 'letterpaper',
-
-    # The font size ('10pt', '11pt' or '12pt').
-    #
-    # 'pointsize': '10pt',
-
-    # Additional stuff for the LaTeX preamble.
-    #
-    # 'preamble': '',
-
-    # Latex figure (float) alignment
-    #
-    # 'figure_align': 'htbp',
-}
-
-# Grouping the document tree into LaTeX files. List of tuples
-# (source start file, target name, title,
-#  author, documentclass [howto, manual, or own class]).
-latex_documents = [
-    (master_doc, 'pygerrit2.tex', u'pygerrit2 Documentation',
-     u'David Pursehouse', 'manual'),
-]
-
-
-# -- Options for manual page output ---------------------------------------
-
-# One entry per manual page. List of tuples
-# (source start file, name, description, authors, manual section).
-man_pages = [
-    (master_doc, 'pygerrit2', u'pygerrit2 Documentation',
-     [author], 1)
-]
-
-
-# -- Options for Texinfo output -------------------------------------------
-
-# Grouping the document tree into Texinfo files. List of tuples
-# (source start file, target name, title, author,
-#  dir menu entry, description, category)
-texinfo_documents = [
-    (master_doc, 'pygerrit2', u'pygerrit2 Documentation',
-     author, 'pygerrit2', 'One line description of project.',
-     'Miscellaneous'),
-]
diff --git a/docs/index.rst b/docs/index.rst
deleted file mode 100644
index c830786..0000000
--- a/docs/index.rst
+++ /dev/null
@@ -1,20 +0,0 @@
-.. pygerrit2 documentation master file, created by
-   sphinx-quickstart on Sun Oct 22 17:32:32 2017.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
-Welcome to pygerrit2's documentation!
-=====================================
-
-.. toctree::
-   :maxdepth: 2
-   :caption: Contents:
-
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
diff --git a/docs/make.bat b/docs/make.bat
deleted file mode 100644
index 3a817b5..0000000
--- a/docs/make.bat
+++ /dev/null
@@ -1,36 +0,0 @@
-@ECHO OFF
-
-pushd %~dp0
-
-REM Command file for Sphinx documentation
-
-if "%SPHINXBUILD%" == "" (
-	set SPHINXBUILD=python -msphinx
-)
-set SOURCEDIR=.
-set BUILDDIR=_build
-set SPHINXPROJ=pygerrit2
-
-if "%1" == "" goto help
-
-%SPHINXBUILD% >NUL 2>NUL
-if errorlevel 9009 (
-	echo.
-	echo.The Sphinx module was not found. Make sure you have Sphinx installed,
-	echo.then set the SPHINXBUILD environment variable to point to the full
-	echo.path of the 'sphinx-build' executable. Alternatively you may add the
-	echo.Sphinx directory to PATH.
-	echo.
-	echo.If you don't have Sphinx installed, grab it from
-	echo.http://sphinx-doc.org/
-	exit /b 1
-)
-
-%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
-goto end
-
-:help
-%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS%
-
-:end
-popd
diff --git a/example.py b/example.py
index 2ed96e8..96c7695 100755
--- a/example.py
+++ b/example.py
@@ -29,7 +29,6 @@ import argparse
 import logging
 import sys
 
-from requests.auth import HTTPBasicAuth, HTTPDigestAuth
 from requests.exceptions import RequestException
 try:
     from requests_kerberos import HTTPKerberosAuth, OPTIONAL
@@ -37,8 +36,9 @@ try:
 except ImportError:
     _KERBEROS_SUPPORT = False
 
-from pygerrit2.rest import GerritRestAPI
-from pygerrit2.rest.auth import HTTPDigestAuthFromNetrc, HTTPBasicAuthFromNetrc
+from pygerrit2 import GerritRestAPI
+from pygerrit2 import HTTPDigestAuthFromNetrc, HTTPBasicAuthFromNetrc
+from pygerrit2 import HTTPBasicAuth, HTTPDigestAuth
 
 
 def _main():
diff --git a/livetests.py b/livetests.py
new file mode 100755
index 0000000..2d517f6
--- /dev/null
+++ b/livetests.py
@@ -0,0 +1,190 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+
+# The MIT License
+#
+# Copyright 2018 David Pursehouse. All rights reserved.
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+
+"""Live server tests."""
+
+import base64
+import pytest
+import unittest
+from pygerrit2 import GerritRestAPI, GerritReview, HTTPBasicAuth, \
+    HTTPDigestAuth
+from testcontainers.core.container import DockerContainer
+from testcontainers.core.waiting_utils import wait_container_is_ready
+
+
+TEST_TOPIC = "test-topic"
+
+
+class GerritContainer(DockerContainer):
+    """Gerrit container."""
+
+    def __init__(self, version):
+        """Construct a GerritContainer with the given version."""
+        image = "gerritcodereview/gerrit:" + version
+        super(GerritContainer, self).__init__(image)
+        self.with_exposed_ports(8080)
+
+
+@wait_container_is_ready()
+def _initialize(api):
+    api.get("/changes/")
+
+
+@pytest.fixture(scope="module",
+                params=["2.14.18", "2.15.11", "2.16.6"])
+def gerrit_api(request):
+    """Create a Gerrit container for the given version and return an API."""
+    with GerritContainer(request.param) as gerrit:
+        port = gerrit.get_exposed_port(8080)
+        url = "http://localhost:%s" % port
+        if request.param == "2.13.11":
+            auth = HTTPDigestAuth("admin", "secret")
+        else:
+            auth = HTTPBasicAuth("admin", "secret")
+        api = GerritRestAPI(url=url, auth=auth)
+        _initialize(api)
+        yield api
+
+
+class TestGerritAgainstLiveServer(object):
+    """Run tests against a live server."""
+
+    def _get_test_change(self, gerrit_api, topic=TEST_TOPIC):
+        results = gerrit_api.get("/changes/?q=topic:" + topic)
+        assert len(results) == 1
+        return results[0]
+
+    def test_put_with_json_dict(self, gerrit_api):
+        """Test a PUT request passing data as a dict to `json`.
+
+        Tests that the PUT request works as expected when data is passed
+        via the `json` argument as a `dict`.
+
+        Creates the test project which is used by subsequent tests.
+        """
+        projectinput = {"create_empty_commit": "true"}
+        gerrit_api.put("/projects/test-project", json=projectinput)
+        gerrit_api.get("/projects/test-project")
+
+    def test_put_with_data_dict(self, gerrit_api):
+        """Test a PUT request passing data as a dict to `data`.
+
+        Tests that the PUT request works as expected when data is passed
+        via the `data` argument as a `dict`.
+        """
+        description = {"description": "New Description"}
+        gerrit_api.put("/projects/test-project/description", data=description)
+        project = gerrit_api.get("/projects/test-project")
+        assert project["description"] == "New Description"
+
+    def test_post_with_data_dict_and_no_data(self, gerrit_api):
+        """Test a POST request passing data as a dict to `data`.
+
+        Tests that the POST request works as expected when data is passed
+        via the `data` argument as a `dict`.
+        """
+        changeinput = {"project": "test-project",
+                       "subject": "subject",
+                       "branch": "master",
+                       "topic": "post-with-data"}
+        result = gerrit_api.post("/changes/", data=changeinput)
+        change = self._get_test_change(gerrit_api, "post-with-data")
+        assert change["id"] == result["id"]
+
+        # Subsequent post without data or json should not have the Content-Type
+        # json header, and should succeed.
+        result = gerrit_api.post("/changes/" + change["id"] + "/abandon")
+        assert result["status"] == "ABANDONED"
+
+    def test_post_with_json_dict(self, gerrit_api):
+        """Test a POST request passing data as a dict to `json`.
+
+        Tests that the POST request works as expected when data is passed
+        via the `json` argument as a `dict`.
+
+        Creates the change which is used by subsequent tests.
+        """
+        changeinput = {"project": "test-project",
+                       "subject": "subject",
+                       "branch": "master",
+                       "topic": TEST_TOPIC}
+        result = gerrit_api.post("/changes/", json=changeinput)
+        change = self._get_test_change(gerrit_api)
+        assert change["id"] == result["id"]
+
+    def test_put_with_data_as_string(self, gerrit_api):
+        """Test a PUT request passing data as a string to `data`.
+
+        Tests that the PUT request works as expected when data is passed
+        via the `data` parameter as a string.
+
+        Creates a change edit that is checked in the subsequent test.
+        """
+        change_id = self._get_test_change(gerrit_api)["id"]
+        gerrit_api.put("/changes/" + change_id + "/edit/foo",
+                       data="Content with non base64 valid chars åäö")
+
+    def test_put_json_content(self, gerrit_api):
+        """Test a PUT request with a json file content (issue #54)."""
+        change_id = self._get_test_change(gerrit_api)["id"]
+        content = """{"foo" : "bar"}"""
+        gerrit_api.put("/changes/" + change_id + "/edit/file.json",
+                       data=content)
+
+    def test_get_base64_data(self, gerrit_api):
+        """Test a GET request on an API that returns base64 encoded response.
+
+        Tests that the headers can be overridden on the GET call, resulting
+        in the data being returned as text/plain, and that the content of the
+        response can be base64 decoded.
+        """
+        change_id = self._get_test_change(gerrit_api)["id"]
+        response = gerrit_api.get("/changes/" + change_id + "/edit/foo",
+                                  headers={'Accept': 'text/plain'})
+
+        # Will raise binascii.Error if content is not properly encoded
+        base64.b64decode(response)
+
+    def test_get_patch_zip(self, gerrit_api):
+        """Test a GET request to get a patch file (issue #19)."""
+        change_id = self._get_test_change(gerrit_api)["id"]
+        gerrit_api.get("/changes/" + change_id + "/revisions/current/patch?zip")
+
+    def test_put_with_no_content(self, gerrit_api):
+        """Test a PUT request with no content."""
+        change_id = self._get_test_change(gerrit_api)["id"]
+        gerrit_api.put("/changes/" + change_id + "/edit/foo")
+
+    def test_review(self, gerrit_api):
+        """Test that a review can be posted by the review API."""
+        change_id = self._get_test_change(gerrit_api)["id"]
+        review = GerritReview()
+        review.set_message("Review from live test")
+        review.add_labels({"Code-Review": 1})
+        gerrit_api.review(change_id, "current", review)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/pygerrit2.egg-info/PKG-INFO b/pygerrit2.egg-info/PKG-INFO
index 7c03978..3c5bc6c 100644
--- a/pygerrit2.egg-info/PKG-INFO
+++ b/pygerrit2.egg-info/PKG-INFO
@@ -1,80 +1,24 @@
 Metadata-Version: 1.1
 Name: pygerrit2
-Version: 2.0.4
+Version: 2.0.9
 Summary: Client library for interacting with Gerrit's REST API
 Home-page: https://github.com/dpursehouse/pygerrit2
 Author: David Pursehouse
 Author-email: david.pursehouse@gmail.com
 License: The MIT License
-Description-Content-Type: UNKNOWN
 Description: Pygerrit2 - Client library for interacting with Gerrit Code Review's REST API
         =============================================================================
         
-        .. image:: https://img.shields.io/pypi/v/pygerrit2.png
+        .. image:: https://img.shields.io/pypi/v/pygerrit2.svg
         
-        .. image:: https://img.shields.io/pypi/l/pygerrit2.png
+        .. image:: https://img.shields.io/pypi/l/pygerrit2.svg
         
-        Pygerrit2 provides a simple interface for clients to interact with
-        `Gerrit Code Review`_ via the REST API.
-        
-        Prerequisites
-        -------------
-        
-        Pygerrit2 is compatible with Python 2.6 and Python 2.7.  Support for Python 3
-        is experimental.
-        
-        Pygerrit2 depends on the `requests`_ library.
-        
-        
-        Installation
-        ------------
-        
-        To install pygerrit2, simply::
-        
-            $ pip install pygerrit2
-        
-        
-        Usage
-        -----
+        .. image:: https://travis-ci.org/dpursehouse/pygerrit2.svg?branch=master
         
-        This simple example shows how to get the user's open changes. Authentication
-        to Gerrit is done via HTTP Basic authentication, using an explicitly given
-        username and password::
+        .. image:: https://api.dependabot.com/badges/status?host=github&repo=dpursehouse/pygerrit2
         
-            >>> from requests.auth import HTTPBasicAuth
-            >>> from pygerrit2.rest import GerritRestAPI
-            >>> auth = HTTPBasicAuth('username', 'password')
-            >>> rest = GerritRestAPI(url='http://review.example.net', auth=auth)
-            >>> changes = rest.get("/changes/?q=owner:self%20status:open")
-        
-        Note that is is not necessary to add the ``/a/`` prefix on the endpoint
-        URLs. This is automatically added when the API is instantiated with an
-        authentication object.
-        
-        If the user's HTTP username and password are defined in the ``.netrc``
-        file::
-        
-            machine review.example.net login MyUsername password MyPassword
-        
-        then it is possible to authenticate with those credentials::
-        
-            >>> from pygerrit2.rest import GerritRestAPI
-            >>> from pygerrit2.rest.auth import HTTPBasicAuthFromNetrc
-            >>> url = 'http://review.example.net'
-            >>> auth = HTTPBasicAuthFromNetrc(url=url)
-            >>> rest = GerritRestAPI(url=url, auth=auth)
-            >>> changes = rest.get("/changes/?q=owner:self%20status:open")
-        
-        Note that the HTTP password is not the same as the SSH password. For
-        instructions on how to obtain the HTTP password, refer to Gerrit's
-        `HTTP upload settings`_ documentation.
-        
-        Also note that in Gerrit version 2.14, support for HTTP Digest authentication
-        was removed and only HTTP Basic authentication is supported. When using
-        pygerrit2 against an earlier Gerrit version, it may be necessary to replace
-        the `HTTPBasic...` classes with the corresponding `HTTPDigest...` versions.
-        
-        Refer to the `example`_ script for a full working example.
+        Pygerrit2 provides a simple interface for clients to interact with
+        `Gerrit Code Review`_ via the REST API.
         
         Copyright and License
         ---------------------
@@ -89,21 +33,22 @@ Description: Pygerrit2 - Client library for interacting with Gerrit Code Review'
         license details.
         
         .. _`Gerrit Code Review`: https://gerritcodereview.com/
-        .. _`requests`: https://github.com/kennethreitz/requests
-        .. _example: https://github.com/dpursehouse/pygerrit2/blob/master/example.py
-        .. _`HTTP upload settings`: https://gerrit-documentation.storage.googleapis.com/Documentation/2.14/user-upload.html#http
         .. _LICENSE: https://github.com/dpursehouse/pygerrit2/blob/master/LICENSE
         
         
-Keywords: gerrit
-rest
-http
-Platform: UNKNOWN
-Classifier: Development Status :: 3 - Alpha
+Keywords: gerrit rest http api client
+Platform: POSIX, Unix, MacOS
+Classifier: Development Status :: 5 - Production/Stable
 Classifier: Environment :: Console
 Classifier: Intended Audience :: Developers
+Classifier: Intended Audience :: Information Technology
 Classifier: License :: OSI Approved :: MIT License
 Classifier: Natural Language :: English
 Classifier: Programming Language :: Python
-Classifier: Programming Language :: Python :: 2.6
-Classifier: Programming Language :: Python :: 2.7
+Classifier: Programming Language :: Python :: 3.5
+Classifier: Programming Language :: Python :: 3.6
+Classifier: Programming Language :: Python :: 3.7
+Classifier: Operating System :: POSIX
+Classifier: Operating System :: Unix
+Classifier: Operating System :: MacOS
+Classifier: Topic :: Software Development :: Libraries :: Python Modules
diff --git a/pygerrit2.egg-info/SOURCES.txt b/pygerrit2.egg-info/SOURCES.txt
index 0133af6..fec6ab4 100644
--- a/pygerrit2.egg-info/SOURCES.txt
+++ b/pygerrit2.egg-info/SOURCES.txt
@@ -1,21 +1,20 @@
+.mailmap
+.travis.yml
 AUTHORS
 ChangeLog
 LICENSE
 MANIFEST.in
 Makefile
+Pipfile
+Pipfile.lock
 README.md
-README.rst
+description.rst
 example.py
+livetests.py
 requirements.txt
 setup.cfg
 setup.py
-test_requirements.txt
-tox.ini
 unittests.py
-docs/Makefile
-docs/conf.py
-docs/index.rst
-docs/make.bat
 pygerrit2/__init__.py
 pygerrit2.egg-info/PKG-INFO
 pygerrit2.egg-info/SOURCES.txt
diff --git a/pygerrit2.egg-info/pbr.json b/pygerrit2.egg-info/pbr.json
index bae1ce6..5ae819c 100644
--- a/pygerrit2.egg-info/pbr.json
+++ b/pygerrit2.egg-info/pbr.json
@@ -1 +1 @@
-{"git_version": "5eb119b", "is_release": false}
\ No newline at end of file
+{"git_version": "3f6af04", "is_release": true}
\ No newline at end of file
diff --git a/pygerrit2.egg-info/requires.txt b/pygerrit2.egg-info/requires.txt
index 9c750a8..77059b2 100644
--- a/pygerrit2.egg-info/requires.txt
+++ b/pygerrit2.egg-info/requires.txt
@@ -1,2 +1,2 @@
 pbr>=0.8.0
-requests<=2.18.4,>=2.10.0
+requests>=2.20.0
diff --git a/pygerrit2/__init__.py b/pygerrit2/__init__.py
index 829d781..008c509 100644
--- a/pygerrit2/__init__.py
+++ b/pygerrit2/__init__.py
@@ -22,6 +22,14 @@
 
 """Module to interface with Gerrit."""
 
+from .rest import GerritRestAPI, GerritReview
+from requests.auth import HTTPBasicAuth, HTTPDigestAuth
+from .rest.auth import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
+
+__all__ = ["GerritRestAPI", "GerritReview",
+           "HTTPBasicAuth", "HTTPDigestAuth",
+           "HTTPBasicAuthFromNetrc", "HTTPDigestAuthFromNetrc"]
+
 
 def from_json(json_data, key):
     """Extract values from JSON data.
diff --git a/pygerrit2/rest/__init__.py b/pygerrit2/rest/__init__.py
index e25eaea..94e543c 100644
--- a/pygerrit2/rest/__init__.py
+++ b/pygerrit2/rest/__init__.py
@@ -26,8 +26,21 @@ import json
 import logging
 import requests
 
+from .auth import HTTPBasicAuthFromNetrc
+
+logger = logging.getLogger("pygerrit2")
+fmt = "%(asctime)s-[%(name)s-%(levelname)s] %(message)s"
+datefmt = "[%y-%m-%d %H:%M:%S]"
+sh = logging.StreamHandler()
+sh.setLevel(logging.WARNING)
+sh.setFormatter(logging.Formatter(fmt, datefmt))
+if not logger.handlers:
+    logger.addHandler(sh)
+
 GERRIT_MAGIC_JSON_PREFIX = ")]}\'\n"
 GERRIT_AUTH_SUFFIX = "/a"
+DEFAULT_HEADERS = {'Accept': 'application/json',
+                   'Accept-Encoding': 'gzip'}
 
 
 def _decode_response(response):
@@ -41,11 +54,16 @@ def _decode_response(response):
         requests.HTTPError if the response contains an HTTP error status code.
 
     """
+    content_type = response.headers.get('content-type', '')
+    logger.debug("status[%s] content_type[%s] encoding[%s]" %
+                 (response.status_code, content_type, response.encoding))
+    response.raise_for_status()
     content = response.content.strip()
     if response.encoding:
         content = content.decode(response.encoding)
-    response.raise_for_status()
-    content_type = response.headers.get('content-type', '')
+    if not content:
+        logger.debug("no content in response")
+        return content
     if content_type.split(';')[0] != 'application/json':
         return content
     if content.startswith(GERRIT_MAGIC_JSON_PREFIX):
@@ -53,30 +71,10 @@ def _decode_response(response):
     try:
         return json.loads(content)
     except ValueError:
-        logging.error('Invalid json content: %s', content)
+        logger.error('Invalid json content: %s', content)
         raise
 
 
-def _merge_dict(result, overrides):
-    """Deep-merge dictionaries.
-
-    :arg dict result: The resulting dictionary
-    :arg dict overrides: Dictionay being merged into the result
-
-    :returns:
-        The resulting dictionary
-
-    """
-    for key in overrides:
-        if (key in result and
-                isinstance(result[key], dict) and
-                isinstance(overrides[key], dict)):
-            _merge_dict(result[key], overrides[key])
-        else:
-            result[key] = overrides[key]
-    return result
-
-
 class GerritRestAPI(object):
     """Interface to the Gerrit REST API.
 
@@ -92,14 +90,17 @@ class GerritRestAPI(object):
 
     def __init__(self, url, auth=None, verify=True):
         """See class docstring."""
-        headers = {'Accept': 'application/json',
-                   'Accept-Encoding': 'gzip'}
         self.kwargs = {'auth': auth,
-                       'verify': verify,
-                       'headers': headers}
+                       'verify': verify}
         self.url = url.rstrip('/')
         self.session = requests.session()
 
+        if not auth:
+            try:
+                auth = HTTPBasicAuthFromNetrc(url)
+            except ValueError:
+                pass
+
         if auth:
             if not isinstance(auth, requests.auth.AuthBase):
                 raise ValueError('Invalid auth type; must be derived '
@@ -111,6 +112,9 @@ class GerritRestAPI(object):
             if self.url.endswith(GERRIT_AUTH_SUFFIX):
                 self.url = self.url[: - len(GERRIT_AUTH_SUFFIX)]
 
+        # Keep a copy of the auth, only needed for tests
+        self.auth = auth
+
         if not self.url.endswith('/'):
             self.url += '/'
 
@@ -126,6 +130,27 @@ class GerritRestAPI(object):
         endpoint = endpoint.lstrip('/')
         return self.url + endpoint
 
+    def translate_kwargs(self, **kwargs):
+        """Translate kwargs replacing `data` with `json` if necessary."""
+        local_kwargs = self.kwargs.copy()
+        local_kwargs.update(kwargs)
+
+        if "data" in local_kwargs and "json" in local_kwargs:
+            raise ValueError("Cannot use data and json together")
+
+        if "data" in local_kwargs and isinstance(local_kwargs["data"], dict):
+            local_kwargs.update({"json": local_kwargs["data"]})
+            del local_kwargs["data"]
+
+        headers = DEFAULT_HEADERS.copy()
+        if "headers" in kwargs:
+            headers.update(kwargs["headers"])
+        if "json" in local_kwargs:
+            headers.update({"Content-Type": "application/json;charset=UTF-8"})
+        local_kwargs.update({"headers": headers})
+
+        return local_kwargs
+
     def get(self, endpoint, return_response=False, **kwargs):
         """Send HTTP GET to the endpoint.
 
@@ -139,8 +164,8 @@ class GerritRestAPI(object):
             requests.RequestException on timeout or connection error.
 
         """
-        kwargs.update(self.kwargs.copy())
-        response = self.session.get(self.make_url(endpoint), **kwargs)
+        args = self.translate_kwargs(**kwargs)
+        response = self.session.get(self.make_url(endpoint), **args)
 
         decoded_response = _decode_response(response)
 
@@ -160,18 +185,7 @@ class GerritRestAPI(object):
             requests.RequestException on timeout or connection error.
 
         """
-        args = {}
-        if ("data" in kwargs and isinstance(kwargs["data"], dict)) or \
-                "json" in kwargs:
-            _merge_dict(
-                args, {
-                    "headers": {
-                        "Content-Type": "application/json;charset=UTF-8"
-                    }
-                }
-            )
-        _merge_dict(args, self.kwargs.copy())
-        _merge_dict(args, kwargs)
+        args = self.translate_kwargs(**kwargs)
         response = self.session.put(self.make_url(endpoint), **args)
 
         decoded_response = _decode_response(response)
@@ -192,18 +206,7 @@ class GerritRestAPI(object):
             requests.RequestException on timeout or connection error.
 
         """
-        args = {}
-        if ("data" in kwargs and isinstance(kwargs["data"], dict)) or \
-                "json" in kwargs:
-            _merge_dict(
-                args, {
-                    "headers": {
-                        "Content-Type": "application/json;charset=UTF-8"
-                    }
-                }
-            )
-        _merge_dict(args, self.kwargs.copy())
-        _merge_dict(args, kwargs)
+        args = self.translate_kwargs(**kwargs)
         response = self.session.post(self.make_url(endpoint), **args)
 
         decoded_response = _decode_response(response)
@@ -224,17 +227,7 @@ class GerritRestAPI(object):
             requests.RequestException on timeout or connection error.
 
         """
-        args = {}
-        if "data" in kwargs or "json" in kwargs:
-            _merge_dict(
-                args, {
-                    "headers": {
-                        "Content-Type": "application/json;charset=UTF-8"
-                    }
-                }
-            )
-        _merge_dict(args, self.kwargs.copy())
-        _merge_dict(args, kwargs)
+        args = self.translate_kwargs(**kwargs)
         response = self.session.delete(self.make_url(endpoint), **args)
 
         decoded_response = _decode_response(response)
@@ -258,7 +251,8 @@ class GerritRestAPI(object):
 
         """
         endpoint = "changes/%s/revisions/%s/review" % (change_id, revision)
-        self.post(endpoint, data=str(review))
+        self.post(endpoint, data=str(review),
+                  headers={"Content-Type": "application/json"})
 
 
 class GerritReview(object):
diff --git a/pygerrit2/rest/auth.py b/pygerrit2/rest/auth.py
index 9416cc1..4a8d308 100644
--- a/pygerrit2/rest/auth.py
+++ b/pygerrit2/rest/auth.py
@@ -26,12 +26,16 @@ from requests.auth import HTTPDigestAuth, HTTPBasicAuth
 from requests.utils import get_netrc_auth
 
 
+def _get_netrc_auth(url):
+    return get_netrc_auth(url)
+
+
 class HTTPDigestAuthFromNetrc(HTTPDigestAuth):
     """HTTP Digest Auth with netrc credentials."""
 
     def __init__(self, url):
         """See class docstring."""
-        auth = get_netrc_auth(url)
+        auth = _get_netrc_auth(url)
         if not auth:
             raise ValueError("netrc missing or no credentials found in netrc")
         username, password = auth
@@ -43,7 +47,7 @@ class HTTPBasicAuthFromNetrc(HTTPBasicAuth):
 
     def __init__(self, url):
         """See class docstring."""
-        auth = get_netrc_auth(url)
+        auth = _get_netrc_auth(url)
         if not auth:
             raise ValueError("netrc missing or no credentials found in netrc")
         username, password = auth
diff --git a/requirements.txt b/requirements.txt
index 8419673..77059b2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,2 +1,2 @@
 pbr>=0.8.0
-requests>=2.10.0,<=2.18.4
+requests>=2.20.0
diff --git a/setup.cfg b/setup.cfg
index d191d72..c793aee 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -2,23 +2,27 @@
 name = pygerrit2
 summary = Client library for interacting with Gerrit's REST API
 author = David Pursehouse
-author_email = david.pursehouse@gmail.com
+author-email = david.pursehouse@gmail.com
 home-page = https://github.com/dpursehouse/pygerrit2
 license = The MIT License
-description-file = README.rst
-keywords = 
-	gerrit
-	rest
-	http
+description-file = description.rst
+keywords = gerrit rest http api client
+platform = POSIX, Unix, MacOS
 classifiers = 
-	Development Status :: 3 - Alpha
+	Development Status :: 5 - Production/Stable
 	Environment :: Console
 	Intended Audience :: Developers
+	Intended Audience :: Information Technology
 	License :: OSI Approved :: MIT License
 	Natural Language :: English
 	Programming Language :: Python
-	Programming Language :: Python :: 2.6
-	Programming Language :: Python :: 2.7
+	Programming Language :: Python :: 3.5
+	Programming Language :: Python :: 3.6
+	Programming Language :: Python :: 3.7
+	Operating System :: POSIX
+	Operating System :: Unix
+	Operating System :: MacOS
+	Topic :: Software Development :: Libraries :: Python Modules
 
 [egg_info]
 tag_build = 
diff --git a/test_requirements.txt b/test_requirements.txt
deleted file mode 100644
index 7a16793..0000000
--- a/test_requirements.txt
+++ /dev/null
@@ -1,3 +0,0 @@
-pydocstyle==2.1.1
-flake8==3.5.0
-pyflakes==1.6.0
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index e05b38d..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,25 +0,0 @@
-[tox]
-minversion = 2.1.1
-envlist = pep8, py35, py27, py26
-
-[testenv]
-setenv = VIRTUAL_ENV={envdir}
-         SUBUNIT_FORMATTER=tee testr_subunit_log
-         OS_STDOUT_NOCAPTURE=False
-         LANG=en_US.UTF-8
-usedevelop = True
-install_command = pip install {opts} {packages}
-deps = -r{toxinidir}/test_requirements.txt
-commands = python unittests.py
-
-[testenv:pep8]
-commands = flake8
-
-[testenv:pyflakes]
-deps = pyflakes
-commands = pyflakes pygerrit2 unittests.py example.py setup.py
-
-[flake8]
-max-line-length = 80
-show-source = True
-exclude = .venv,.tox,dist,docs,build,*.egg,.test,pygerrit2env
diff --git a/unittests.py b/unittests.py
index 548058c..14e3f7f 100755
--- a/unittests.py
+++ b/unittests.py
@@ -25,10 +25,13 @@
 
 """Unit tests for the Pygerrit2 helper methods."""
 
+import re
 import unittest
 
-from pygerrit2 import GerritReviewMessageFormatter
-from pygerrit2.rest import GerritReview, _merge_dict
+from mock import patch
+from pygerrit2 import GerritReviewMessageFormatter, GerritReview
+from pygerrit2 import HTTPBasicAuthFromNetrc, HTTPDigestAuthFromNetrc
+from pygerrit2 import GerritRestAPI
 
 EXPECTED_TEST_CASE_FIELDS = ['header', 'footer', 'paragraphs', 'result']
 
@@ -108,65 +111,6 @@ TEST_CASES = [
      'result': "* One\n* Two"}]
 
 
-class TestMergeDict(unittest.TestCase):
-    """Tests for the `_merge_dict` method."""
-
-    def test_merge_into_empty_dict(self):
-        """Test merging into an empty dict."""
-        dct = {}
-        _merge_dict(dct, {'a': 1, 'b': 2})
-        self.assertEqual(dct, {'a': 1, 'b': 2})
-
-    def test_merge_flat(self):
-        """Test merging a flat dict."""
-        dct = {'c': 3}
-        _merge_dict(dct, {'a': 1, 'b': 2})
-        self.assertEqual(dct, {'a': 1, 'b': 2, 'c': 3})
-
-    def test_merge_with_override(self):
-        """Test merging a dict and overriding values."""
-        dct = {'a': 1}
-        _merge_dict(dct, {'a': 0, 'b': 2})
-        self.assertEqual(dct, {'a': 0, 'b': 2})
-
-    def test_merge_two_levels(self):
-        """Test merging a dict with two levels."""
-        dct = {
-            'a': {
-                'A': 1,
-                'AA': 2,
-            },
-            'b': {
-                'B': 1,
-                'BB': 2,
-            },
-        }
-        overrides = {
-            'a': {
-                'AAA': 3,
-            },
-            'b': {
-                'BBB': 3,
-            },
-        }
-        _merge_dict(dct, overrides)
-        self.assertEqual(
-            dct,
-            {
-                'a': {
-                    'A': 1,
-                    'AA': 2,
-                    'AAA': 3,
-                },
-                'b': {
-                    'B': 1,
-                    'BB': 2,
-                    'BBB': 3,
-                },
-            }
-        )
-
-
 class TestGerritReviewMessageFormatter(unittest.TestCase):
     """Test that the GerritReviewMessageFormatter class behaves properly."""
 
@@ -237,5 +181,154 @@ class TestGerritReview(unittest.TestCase):
             ' "Makefile": [{"line": 15, "message": "test"}]}}')
 
 
+class TestNetrcAuth(unittest.TestCase):
+    """Test that netrc authentication works."""
+
+    def test_basic_auth_from_netrc(self):
+        """Test that the HTTP basic auth is taken from netrc."""
+        with patch('pygerrit2.rest.auth._get_netrc_auth') as mock_netrc:
+            mock_netrc.return_value = ("netrcuser", "netrcpass")
+            auth = HTTPBasicAuthFromNetrc(url="http://review.example.com")
+            assert auth.username == "netrcuser"
+            assert auth.password == "netrcpass"
+
+    def test_digest_auth_from_netrc(self):
+        """Test that the HTTP digest auth is taken from netrc."""
+        with patch('pygerrit2.rest.auth._get_netrc_auth') as mock_netrc:
+            mock_netrc.return_value = ("netrcuser", "netrcpass")
+            auth = HTTPDigestAuthFromNetrc(url="http://review.example.com")
+            assert auth.username == "netrcuser"
+            assert auth.password == "netrcpass"
+
+    def test_basic_auth_from_netrc_fails(self):
+        """Test that an exception is raised when credentials are not found."""
+        with self.assertRaises(ValueError) as exc:
+            HTTPBasicAuthFromNetrc(url="http://review.example.com")
+        assert str(exc.exception) == \
+            "netrc missing or no credentials found in netrc"
+
+    def test_digest_auth_from_netrc_fails(self):
+        """Test that an exception is raised when credentials are not found."""
+        with self.assertRaises(ValueError) as exc:
+            HTTPDigestAuthFromNetrc(url="http://review.example.com")
+        assert str(exc.exception) == \
+            "netrc missing or no credentials found in netrc"
+
+    def test_default_to_basic_auth_from_netrc(self):
+        """Test auth defaults to HTTP basic from netrc when not specified."""
+        with patch('pygerrit2.rest.auth._get_netrc_auth') as mock_netrc:
+            mock_netrc.return_value = ("netrcuser", "netrcpass")
+            api = GerritRestAPI(url="http://review.example.com")
+            assert isinstance(api.auth, HTTPBasicAuthFromNetrc)
+            assert api.url.endswith("/a/")
+
+    def test_default_to_no_auth_when_not_in_netrc(self):
+        """Test auth defaults to none when not specified and not in netrc."""
+        with patch('pygerrit2.rest.auth._get_netrc_auth') as mock_netrc:
+            mock_netrc.return_value = None
+            api = GerritRestAPI(url="http://review.example.com")
+            assert api.auth is None
+            assert not api.url.endswith("/a/")
+
+    def test_invalid_auth_type(self):
+        """Test that an exception is raised for invalid auth type."""
+        with self.assertRaises(ValueError) as exc:
+            GerritRestAPI(url="http://review.example.com", auth="foo")
+        assert re.search(r'Invalid auth type', str(exc.exception))
+
+
+class TestKwargsTranslation(unittest.TestCase):
+    """Test that kwargs translation works."""
+
+    def test_data_and_json(self):
+        """Test that `json` and `data` cannot be used at the same time."""
+        api = GerritRestAPI(url="http://review.example.com")
+        with self.assertRaises(ValueError) as exc:
+            api.translate_kwargs(data="d", json="j")
+        assert re.search(r'Cannot use data and json together',
+                         str(exc.exception))
+
+    def test_data_as_dict_converts_to_json_and_header_added(self):
+        """Test that `data` dict is converted to `json`.
+
+        Also test that a Content-Type header is added.
+        """
+        api = GerritRestAPI(url="http://review.example.com")
+        data = {"a": "a"}
+        result = api.translate_kwargs(data=data)
+        assert "json" in result
+        assert "data" not in result
+        assert "headers" in result
+        headers = result["headers"]
+        assert "Content-Type" in headers
+        assert result["json"] == {"a": "a"}
+        assert headers["Content-Type"] == "application/json;charset=UTF-8"
+
+    def test_json_is_unchanged_and_header_added(self):
+        """Test that `json` is unchanged and a Content-Type header is added."""
+        api = GerritRestAPI(url="http://review.example.com")
+        json = {"a": "a"}
+        result = api.translate_kwargs(json=json)
+        assert "json" in result
+        assert "data" not in result
+        assert "headers" in result
+        headers = result["headers"]
+        assert "Content-Type" in headers
+        assert result["json"] == {"a": "a"}
+        assert headers["Content-Type"] == "application/json;charset=UTF-8"
+
+    def test_json_no_side_effect_on_subsequent_call(self):
+        """Test that subsequent call is not polluted with results of previous.
+
+        If the translate_kwargs method is called, resulting in the content-type
+        header being added, the header should not also be added on a subsequent
+        call that does not need it.
+        """
+        api = GerritRestAPI(url="http://review.example.com")
+        json = {"a": "a"}
+        result = api.translate_kwargs(json=json)
+        assert "json" in result
+        assert "data" not in result
+        assert "headers" in result
+        headers = result["headers"]
+        assert "Content-Type" in headers
+        assert result["json"] == {"a": "a"}
+        assert headers["Content-Type"] == "application/json;charset=UTF-8"
+        kwargs = {"a": "a", "b": "b"}
+        result = api.translate_kwargs(**kwargs)
+        assert "json" not in result
+        assert "data" not in result
+        assert "a" in result
+        assert "b" in result
+        assert "headers" in result
+        headers = result["headers"]
+        assert "Content-Type" not in headers
+
+    def test_kwargs_unchanged_when_no_data_or_json(self):
+        """Test that `json` or `data` are not added when not passed."""
+        api = GerritRestAPI(url="http://review.example.com")
+        kwargs = {"a": "a", "b": "b"}
+        result = api.translate_kwargs(**kwargs)
+        assert "json" not in result
+        assert "data" not in result
+        assert "a" in result
+        assert "b" in result
+        assert "headers" in result
+        headers = result["headers"]
+        assert "Content-Type" not in headers
+
+    def test_data_as_string_is_unchanged(self):
+        """Test that `data` is unchanged when passed as a string."""
+        api = GerritRestAPI(url="http://review.example.com")
+        kwargs = {"data": "Content with non base64 valid chars åäö"}
+        result = api.translate_kwargs(**kwargs)
+        assert "json" not in result
+        assert "data" in result
+        assert result["data"] == "Content with non base64 valid chars åäö"
+        assert "headers" in result
+        headers = result["headers"]
+        assert "Content-Type" not in headers
+
+
 if __name__ == '__main__':
     unittest.main()