Repository: dbrennand/virustotal-python (language: python)

Issue:
1.0.0
# 1.0.0

Work in progress in branch [1.0.0](https://github.com/dbrennand/virustotal-python/tree/1.0.0).

This release will contain breaking changes to the library.

## Task List

- [x] Allow use of int 2 and 3 for `API_VERSION`.
- [x] Migrate away from Pipenv.
- [x] Drop support for `COMPATIBILITY_ENABLED` (breaking change).
- [x] Default to VirusTotal API version 3 (breaking change).
- [x] #31 
- [x] Check examples.
- [x] Automate library tests using GitHub actions.
- [x] Automate library publishing using GitHub actions.
- [x] Update README.md.
- [x] Update changelog in README.md.

`Updated 27/03/22`


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
new file mode 100644
index 0000000..52fe3c1
--- /dev/null
+++ b/.github/workflows/ci.yml
@@ -0,0 +1,50 @@
+name: CI
+on:
+  - pull_request
+  - push
+
+jobs:
+  Test:
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v2
+
+      - name: Set up Python
+        id: setup-python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+
+      - name: Load cached Poetry installation
+        uses: actions/cache@v2
+        with:
+          path: ~/.local
+          key: poetry-0
+
+      - name: Install Poetry
+        uses: snok/install-poetry@v1
+        with:
+          virtualenvs-create: true
+          virtualenvs-in-project: true
+          installer-parallel: true
+
+      - name: Load cached venv
+        id: cached-poetry-dependencies
+        uses: actions/cache@v2
+        with:
+          path: .venv
+          key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }}
+
+      - name: Install dependencies if cache does not exist
+        if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true'
+        run: poetry install --no-interaction --no-root
+
+      - name: Install library
+        run: poetry install --no-interaction
+
+      - name: Run tests
+        run: |
+          source .venv/bin/activate
+          black . --check
+          pytest tests/ --cov=virustotal_python
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
new file mode 100644
index 0000000..3208443
--- /dev/null
+++ b/.github/workflows/publish.yml
@@ -0,0 +1,33 @@
+name: Publish
+on:
+  push:
+    tags:
+      - "*.*.*"
+
+jobs:
+  Publish:
+    if: github.repository == 'dbrennand/virustotal-python' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags')
+    runs-on: ubuntu-latest
+    steps:
+      - name: Check out repository
+        uses: actions/checkout@v2
+
+      - name: Set up Python
+        uses: actions/setup-python@v2
+        with:
+          python-version: 3.7
+
+      - name: Load cached Poetry installation
+        uses: actions/cache@v2
+        with:
+          path: ~/.local
+          key: poetry-0
+
+      - name: Install Poetry
+        uses: snok/install-poetry@v1
+
+      - name: Configure Poetry
+        run: poetry config pypi-token.pypi "${{ secrets.PYPI_API_KEY }}"
+
+      - name: Publish virustotal-python
+        run: poetry publish --build
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..28fc71f
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,87 @@
+# Changelog
+
+## 1.0.0
+
+### Breaking Changes
+
+* The `Virustotal` class now defaults to version 3 of the VirusTotal API.
+
+* Dropped support for `COMPATIBILITY_ENABLED` parameter on the `Virustotal` class.
+
+### Docs
+
+* Updated README content including instructions for running tests and improve general readability.
+
+* Moved to using Google docstring format.
+
+* Improved type hints.
+
+* Refactored examples to favour version 3 of the VirusTotal API.
+
+* Added a proper CHANGELOG.
+
+### Tests
+
+* Added new unit tests with 95% coverage.
+
+### Misc Changes
+
+* `API_VERSION` can now accept an `int` to specify VirusTotal API version to use.
+
+* Add GitHub actions workflows for automated testing and publishing.
+
+## 0.2.0
+
+Added `large_file` parameter to `request` so a file larger than 32MB can be submitted for analysis. See [#33](https://github.com/dbrennand/virustotal-python/pull/33). Thank you @smk762.
+
+## 0.1.3
+
+Update urllib3 to 1.26.5 to address [CVE-2021-33503](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33503).
+
+## 0.1.2
+
+Update dependencies for security vulnerability. Fixed an issue with some tests failing.
+
+## 0.1.1
+
+Added Context Manager support and tests. Updated dependencies and license year.
+
+## 0.1.0
+
+Added support for the VirusTotal v3 API. Library redesign (new usage, examples, tests and more.) See [#24](https://github.com/dbrennand/virustotal-python/pull/24).
+
+## 0.0.9
+
+Update dependencies for security vulnerability.
+
+## 0.0.8
+
+Updated dependencies, removed method `file_rescan`
+
+## 0.0.7
+
+Added tests. Updated dependencies, Updated examples and README, `url_report` param `scan` now accepts `type(int)`, no longer `type(str)`
+
+## 0.0.6
+
+Fixed usage example and dependencies in README.md, Setup github.io website, updated requirements.txt.
+
+## 0.0.5
+
+Added proxy support. Via HTTP(S) or using SOCKS: See [#8](https://github.com/dbrennand/virustotal-python/pull/8).
+
+## 0.0.4
+
+README.md updated; dependencies updated.
+
+## 0.0.3
+
+Updated dependencies for `urllib3` security vulnerability.
+
+## 0.0.2
+
+Changes to file_rescan(), file_report(), url_scan(), url_report() to improve ease of use of the wrapper. See issue [#2](https://github.com/dbrennand/virustotal-python/issues/2). Examples updated for changes.
+
+## 0.0.1
+
+Initial release of virustotal-python. Covered all endpoints of the Virustotal public API.
diff --git a/Pipfile b/Pipfile
deleted file mode 100644
index ce85007..0000000
--- a/Pipfile
+++ /dev/null
@@ -1,18 +0,0 @@
-[[source]]
-name = "pypi"
-url = "https://pypi.org/simple"
-verify_ssl = true
-
-[dev-packages]
-black = "*"
-twine = "*"
-pytest = "*"
-
-[packages]
-requests = {extras = ["socks"],version = "*"}
-
-[requires]
-python_version = "3.7"
-
-[pipenv]
-allow_prereleases = true
diff --git a/Pipfile.lock b/Pipfile.lock
deleted file mode 100644
index 351df76..0000000
--- a/Pipfile.lock
+++ /dev/null
@@ -1,391 +0,0 @@
-{
-    "_meta": {
-        "hash": {
-            "sha256": "cb1f2dbc50b7024b5c1b083baab63df952a899f59eeb808e59766fd215751074"
-        },
-        "pipfile-spec": 6,
-        "requires": {
-            "python_version": "3.7"
-        },
-        "sources": [
-            {
-                "name": "pypi",
-                "url": "https://pypi.org/simple",
-                "verify_ssl": true
-            }
-        ]
-    },
-    "default": {
-        "certifi": {
-            "hashes": [
-                "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
-                "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
-            ],
-            "version": "==2021.5.30"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
-                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==4.0.0"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
-                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.10"
-        },
-        "pysocks": {
-            "hashes": [
-                "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299",
-                "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5",
-                "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"
-            ],
-            "version": "==1.7.1"
-        },
-        "requests": {
-            "extras": [
-                "socks"
-            ],
-            "hashes": [
-                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
-                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
-            ],
-            "index": "pypi",
-            "version": "==2.25.1"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
-                "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
-            "version": "==1.26.5"
-        }
-    },
-    "develop": {
-        "appdirs": {
-            "hashes": [
-                "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
-                "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
-            ],
-            "version": "==1.4.4"
-        },
-        "atomicwrites": {
-            "hashes": [
-                "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
-                "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
-            ],
-            "markers": "sys_platform == 'win32'",
-            "version": "==1.4.0"
-        },
-        "attrs": {
-            "hashes": [
-                "sha256:149e90d6d8ac20db7a955ad60cf0e6881a3f20d37096140088356da6c716b0b1",
-                "sha256:ef6aaac3ca6cd92904cdd0d83f629a15f18053ec84e6432106f7a4d04ae4f5fb"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==21.2.0"
-        },
-        "black": {
-            "hashes": [
-                "sha256:dc132348a88d103016726fe360cb9ede02cecf99b76e3660ce6c596be132ce04",
-                "sha256:dfb8c5a069012b2ab1e972e7b908f5fb42b6bbabcba0a788b86dc05067c7d9c7"
-            ],
-            "index": "pypi",
-            "version": "==21.6b0"
-        },
-        "bleach": {
-            "hashes": [
-                "sha256:6123ddc1052673e52bab52cdc955bcb57a015264a1c57d37bea2f6b817af0125",
-                "sha256:98b3170739e5e83dd9dc19633f074727ad848cbedb6026708c8ac2d3b697a433"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==3.3.0"
-        },
-        "certifi": {
-            "hashes": [
-                "sha256:2bbf76fd432960138b3ef6dda3dde0544f27cbf8546c458e60baf371917ba9ee",
-                "sha256:50b1e4f8446b06f41be7dd6338db18e0990601dce795c2b1686458aa7e8fa7d8"
-            ],
-            "version": "==2021.5.30"
-        },
-        "chardet": {
-            "hashes": [
-                "sha256:0d6f53a15db4120f2b08c94f11e7d93d2c911ee118b6b30a04ec3ee8310179fa",
-                "sha256:f864054d66fd9118f2e67044ac8981a54775ec5b67aed0441892edb553d21da5"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==4.0.0"
-        },
-        "click": {
-            "hashes": [
-                "sha256:8c04c11192119b1ef78ea049e0a6f0463e4c48ef00a30160c704337586f3ad7a",
-                "sha256:fba402a4a47334742d782209a7c79bc448911afe1149d07bdabdf480b3e2f4b6"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==8.0.1"
-        },
-        "colorama": {
-            "hashes": [
-                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
-                "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
-            ],
-            "markers": "sys_platform == 'win32' and platform_system == 'Windows'",
-            "version": "==0.4.4"
-        },
-        "docutils": {
-            "hashes": [
-                "sha256:686577d2e4c32380bb50cbb22f575ed742d58168cee37e99117a854bcd88f125",
-                "sha256:cf316c8370a737a022b72b56874f6602acf974a37a9fba42ec2876387549fc61"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
-            "version": "==0.17.1"
-        },
-        "idna": {
-            "hashes": [
-                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
-                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==2.10"
-        },
-        "importlib-metadata": {
-            "hashes": [
-                "sha256:833b26fb89d5de469b24a390e9df088d4e52e4ba33b01dc5e0e4f41b81a16c00",
-                "sha256:b142cc1dd1342f31ff04bb7d022492b09920cb64fed867cd3ea6f80fe3ebd139"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==4.5.0"
-        },
-        "iniconfig": {
-            "hashes": [
-                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
-                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
-            ],
-            "version": "==1.1.1"
-        },
-        "keyring": {
-            "hashes": [
-                "sha256:045703609dd3fccfcdb27da201684278823b72af515aedec1a8515719a038cb8",
-                "sha256:8f607d7d1cc502c43a932a275a56fe47db50271904513a379d39df1af277ac48"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==23.0.1"
-        },
-        "mypy-extensions": {
-            "hashes": [
-                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
-                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
-            ],
-            "version": "==0.4.3"
-        },
-        "packaging": {
-            "hashes": [
-                "sha256:5b327ac1320dc863dca72f4514ecc086f31186744b84a230374cc1fd776feae5",
-                "sha256:67714da7f7bc052e064859c05c595155bd1ee9f69f76557e21f051443c20947a"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==20.9"
-        },
-        "pathspec": {
-            "hashes": [
-                "sha256:86379d6b86d75816baba717e64b1a3a3469deb93bb76d613c9ce79edc5cb68fd",
-                "sha256:aa0cb481c4041bf52ffa7b0d8fa6cd3e88a2ca4879c533c9153882ee2556790d"
-            ],
-            "version": "==0.8.1"
-        },
-        "pkginfo": {
-            "hashes": [
-                "sha256:029a70cb45c6171c329dfc890cde0879f8c52d6f3922794796e06f577bb03db4",
-                "sha256:9fdbea6495622e022cc72c2e5e1b735218e4ffb2a2a69cde2694a6c1f16afb75"
-            ],
-            "version": "==1.7.0"
-        },
-        "pluggy": {
-            "hashes": [
-                "sha256:265a94bf44ca13662f12fcd1b074c14d4b269a712f051b6f644ef7e705d6735f",
-                "sha256:467f0219e89bb5061a8429c6fc5cf055fa3983a0e68e84a1d205046306b37d9e"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.0.0.dev0"
-        },
-        "py": {
-            "hashes": [
-                "sha256:21b81bda15b66ef5e1a777a21c4dcd9c20ad3efd0b3f817e7a809035269e1bd3",
-                "sha256:3b80836aa6d1feeaa108e046da6423ab8f6ceda6468545ae8d02d9d58d18818a"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.10.0"
-        },
-        "pygments": {
-            "hashes": [
-                "sha256:a18f47b506a429f6f4b9df81bb02beab9ca21d0a5fee38ed15aef65f0545519f",
-                "sha256:d66e804411278594d764fc69ec36ec13d9ae9147193a1740cd34d272ca383b8e"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==2.9.0"
-        },
-        "pyparsing": {
-            "hashes": [
-                "sha256:1c6409312ce2ce2997896af5756753778d5f1603666dba5587804f09ad82ed27",
-                "sha256:f4896b4cc085a1f8f8ae53a1a90db5a86b3825ff73eb974dffee3d9e701007f4"
-            ],
-            "markers": "python_version >= '3.5'",
-            "version": "==3.0.0b2"
-        },
-        "pytest": {
-            "hashes": [
-                "sha256:50bcad0a0b9c5a72c8e4e7c9855a3ad496ca6a881a3641b4260605450772c54b",
-                "sha256:91ef2131a9bd6be8f76f1f08eac5c5317221d6ad1e143ae03894b862e8976890"
-            ],
-            "index": "pypi",
-            "version": "==6.2.4"
-        },
-        "pywin32-ctypes": {
-            "hashes": [
-                "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
-                "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
-            ],
-            "markers": "sys_platform == 'win32'",
-            "version": "==0.2.0"
-        },
-        "readme-renderer": {
-            "hashes": [
-                "sha256:63b4075c6698fcfa78e584930f07f39e05d46f3ec97f65006e430b595ca6348c",
-                "sha256:92fd5ac2bf8677f310f3303aa4bce5b9d5f9f2094ab98c29f13791d7b805a3db"
-            ],
-            "version": "==29.0"
-        },
-        "regex": {
-            "hashes": [
-                "sha256:01afaf2ec48e196ba91b37451aa353cb7eda77efe518e481707e0515025f0cd5",
-                "sha256:11d773d75fa650cd36f68d7ca936e3c7afaae41b863b8c387a22aaa78d3c5c79",
-                "sha256:18c071c3eb09c30a264879f0d310d37fe5d3a3111662438889ae2eb6fc570c31",
-                "sha256:1e1c20e29358165242928c2de1482fb2cf4ea54a6a6dea2bd7a0e0d8ee321500",
-                "sha256:281d2fd05555079448537fe108d79eb031b403dac622621c78944c235f3fcf11",
-                "sha256:314d66636c494ed9c148a42731b3834496cc9a2c4251b1661e40936814542b14",
-                "sha256:32e65442138b7b76dd8173ffa2cf67356b7bc1768851dded39a7a13bf9223da3",
-                "sha256:339456e7d8c06dd36a22e451d58ef72cef293112b559010db3d054d5560ef439",
-                "sha256:3916d08be28a1149fb97f7728fca1f7c15d309a9f9682d89d79db75d5e52091c",
-                "sha256:3a9cd17e6e5c7eb328517969e0cb0c3d31fd329298dd0c04af99ebf42e904f82",
-                "sha256:47bf5bf60cf04d72bf6055ae5927a0bd9016096bf3d742fa50d9bf9f45aa0711",
-                "sha256:4c46e22a0933dd783467cf32b3516299fb98cfebd895817d685130cc50cd1093",
-                "sha256:4c557a7b470908b1712fe27fb1ef20772b78079808c87d20a90d051660b1d69a",
-                "sha256:52ba3d3f9b942c49d7e4bc105bb28551c44065f139a65062ab7912bef10c9afb",
-                "sha256:563085e55b0d4fb8f746f6a335893bda5c2cef43b2f0258fe1020ab1dd874df8",
-                "sha256:598585c9f0af8374c28edd609eb291b5726d7cbce16be6a8b95aa074d252ee17",
-                "sha256:619d71c59a78b84d7f18891fe914446d07edd48dc8328c8e149cbe0929b4e000",
-                "sha256:67bdb9702427ceddc6ef3dc382455e90f785af4c13d495f9626861763ee13f9d",
-                "sha256:6d1b01031dedf2503631d0903cb563743f397ccaf6607a5e3b19a3d76fc10480",
-                "sha256:741a9647fcf2e45f3a1cf0e24f5e17febf3efe8d4ba1281dcc3aa0459ef424dc",
-                "sha256:7c2a1af393fcc09e898beba5dd59196edaa3116191cc7257f9224beaed3e1aa0",
-                "sha256:7d9884d86dd4dd489e981d94a65cd30d6f07203d90e98f6f657f05170f6324c9",
-                "sha256:90f11ff637fe8798933fb29f5ae1148c978cccb0452005bf4c69e13db951e765",
-                "sha256:919859aa909429fb5aa9cf8807f6045592c85ef56fdd30a9a3747e513db2536e",
-                "sha256:96fcd1888ab4d03adfc9303a7b3c0bd78c5412b2bfbe76db5b56d9eae004907a",
-                "sha256:97f29f57d5b84e73fbaf99ab3e26134e6687348e95ef6b48cfd2c06807005a07",
-                "sha256:980d7be47c84979d9136328d882f67ec5e50008681d94ecc8afa8a65ed1f4a6f",
-                "sha256:a91aa8619b23b79bcbeb37abe286f2f408d2f2d6f29a17237afda55bb54e7aac",
-                "sha256:ade17eb5d643b7fead300a1641e9f45401c98eee23763e9ed66a43f92f20b4a7",
-                "sha256:b9c3db21af35e3b3c05764461b262d6f05bbca08a71a7849fd79d47ba7bc33ed",
-                "sha256:bd28bc2e3a772acbb07787c6308e00d9626ff89e3bfcdebe87fa5afbfdedf968",
-                "sha256:bf5824bfac591ddb2c1f0a5f4ab72da28994548c708d2191e3b87dd207eb3ad7",
-                "sha256:c0502c0fadef0d23b128605d69b58edb2c681c25d44574fc673b0e52dce71ee2",
-                "sha256:c38c71df845e2aabb7fb0b920d11a1b5ac8526005e533a8920aea97efb8ec6a4",
-                "sha256:ce15b6d103daff8e9fee13cf7f0add05245a05d866e73926c358e871221eae87",
-                "sha256:d3029c340cfbb3ac0a71798100ccc13b97dddf373a4ae56b6a72cf70dfd53bc8",
-                "sha256:e512d8ef5ad7b898cdb2d8ee1cb09a8339e4f8be706d27eaa180c2f177248a10",
-                "sha256:e8e5b509d5c2ff12f8418006d5a90e9436766133b564db0abaec92fd27fcee29",
-                "sha256:ee54ff27bf0afaf4c3b3a62bcd016c12c3fdb4ec4f413391a90bd38bc3624605",
-                "sha256:fa4537fb4a98fe8fde99626e4681cc644bdcf2a795038533f9f711513a862ae6",
-                "sha256:fd45ff9293d9274c5008a2054ecef86a9bfe819a67c7be1afb65e69b405b3042"
-            ],
-            "version": "==2021.4.4"
-        },
-        "requests": {
-            "extras": [
-                "socks"
-            ],
-            "hashes": [
-                "sha256:27973dd4a904a4f13b263a19c866c13b92a39ed1c964655f025f3f8d3d75b804",
-                "sha256:c210084e36a42ae6b9219e00e48287def368a26d03a048ddad7bfee44f75871e"
-            ],
-            "index": "pypi",
-            "version": "==2.25.1"
-        },
-        "requests-toolbelt": {
-            "hashes": [
-                "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f",
-                "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"
-            ],
-            "version": "==0.9.1"
-        },
-        "rfc3986": {
-            "hashes": [
-                "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835",
-                "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"
-            ],
-            "version": "==1.5.0"
-        },
-        "six": {
-            "hashes": [
-                "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926",
-                "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==1.16.0"
-        },
-        "toml": {
-            "hashes": [
-                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
-                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
-            ],
-            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==0.10.2"
-        },
-        "tqdm": {
-            "hashes": [
-                "sha256:24be966933e942be5f074c29755a95b315c69a91f839a29139bf26ffffe2d3fd",
-                "sha256:aa0c29f03f298951ac6318f7c8ce584e48fa22ec26396e6411e43d038243bdb2"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
-            "version": "==4.61.1"
-        },
-        "twine": {
-            "hashes": [
-                "sha256:16f706f2f1687d7ce30e7effceee40ed0a09b7c33b9abb5ef6434e5551565d83",
-                "sha256:a56c985264b991dc8a8f4234eb80c5af87fa8080d0c224ad8f2cd05a2c22e83b"
-            ],
-            "index": "pypi",
-            "version": "==3.4.1"
-        },
-        "urllib3": {
-            "hashes": [
-                "sha256:753a0374df26658f99d826cfe40394a686d05985786d946fbe4165b5148f5a7c",
-                "sha256:a7acd0977125325f516bda9735fa7142b909a8d01e8b2e4c8108d0984e6e0098"
-            ],
-            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4' and python_version < '4'",
-            "version": "==1.26.5"
-        },
-        "webencodings": {
-            "hashes": [
-                "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78",
-                "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"
-            ],
-            "version": "==0.5.1"
-        },
-        "zipp": {
-            "hashes": [
-                "sha256:3607921face881ba3e026887d8150cca609d517579abe052ac81fc5aeffdbd76",
-                "sha256:51cb66cc54621609dd593d1787f286ee42a5c0adbb4b29abea5a63edc3e03098"
-            ],
-            "markers": "python_version >= '3.6'",
-            "version": "==3.4.1"
-        }
-    }
-}
diff --git a/README.md b/README.md
index 1a9f366..3cf34f1 100644
--- a/README.md
+++ b/README.md
@@ -1,260 +1,170 @@
 # virustotal-python 🐍
 ![PyPI](https://img.shields.io/pypi/v/virustotal-python.svg?style=flat-square)
+[![CI](https://github.com/dbrennand/virustotal-python/actions/workflows/ci.yml/badge.svg)](https://github.com/dbrennand/virustotal-python/actions/workflows/ci.yml)
+[![Publish](https://github.com/dbrennand/virustotal-python/actions/workflows/publish.yml/badge.svg)](https://github.com/dbrennand/virustotal-python/actions/workflows/publish.yml)
 
-A Python library to interact with the public VirusTotal v2 and v3 APIs.
+A Python library to interact with the public VirusTotal v3 and v2 APIs.
 
-> [!NOTE]
->
 > This library is intended to be used with the public VirusTotal APIs. However, it *could* be used to interact with premium API endpoints as well.
 >
 > It is highly recommended that you use the VirusTotal v3 API as it is the "default and encouraged way to programmatically interact with VirusTotal".
 
-# Dependencies and installation
-
-> [!NOTE]
->
-> This library should work with Python versions >= 3.7.
+## Installation 🛠
 
+```bash
+# PyPi
+pip install virustotal-python
+# Manually
+pip install .
+# Poetry
+poetry install --no-dev
 ```
-[dev-packages]
-black = "*"
-twine = "*"
-pytest = "*"
-
-[packages]
-requests = {extras = ["socks"],version = "*"}
-```
-
-Install `virustotal-python` using either:
-* `pip3 install virustotal-python`, `pipenv install`, `pip3 install -r requirements.txt`, `python setup.py install`.
 
-## Usage examples
+## Get a VirusTotal API Key 🔑
 
-> [!NOTE]
->
-> See the [examples](examples) directory for several usage examples.
->
-> Furthermore, check [`virustotal_python/virustotal.py`](virustotal_python/virustotal.py) for docstrings containing full parameter descriptions.
+[Sign up](https://www.virustotal.com/gui/join-us) for a VirusTotal account. Then, view your VirusTotal API key.
 
-Authenticate using your VirusTotal API key:
+![VirusTotal view API key](images/APIKey.png)
 
-> ![NOTE]
->
-> To obtain a VirusTotal API key, [sign up](https://www.virustotal.com/gui/join-us) for a VirusTotal account.
->
-> Then, view your VirusTotal API key.
->
-> ![VirusTotal view API key](images/APIKey.png)
+## Getting Started
 
 ```python
-from virustotal_python import Virustotal
-
-# v2 example
-vtotal = Virustotal(API_KEY="Insert API key here.")
+import virustotal_python
 
-# v3 example
-vtotal = Virustotal(API_KEY="Insert API key here.", API_VERSION="v3")
+with virustotal_python.Virustotal("<VirusTotal API Key>") as vtotal:
+    # Your code here...
 
-# You can provide True to the `COMPATIBILITY_ENABLED` parameter to preserve the old response format of virustotal-python versions prior to 0.1.0
-vtotal = Virustotal(API_KEY="Insert API key here.", API_VERSION="v3", COMPATIBILITY_ENABLED=True)
+# Use the (old) VirusTotal version 2 API
+with virustotal_python.Virustotal(
+    API_KEY="<VirusTotal API Key>", API_VERSION=2
+) as vtotal:
+    # Your code here...
 
 # You can also set proxies and timeouts for requests made by the library
-vtotal = Virustotal(
-    API_KEY="Insert API key here.",
-    API_VERSION="v3",
-    PROXIES={"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"},
-    TIMEOUT=5.0)
-
-# As of version 0.1.1, the Virustotal class can be invoked as a Context Manager!
-## v2 example
-with Virustotal(API_KEY="Insert API key here.") as vtotal:
-    # Your code here
-
-## v3 example
-with Virustotal(API_KEY="Insert API key here.", API_VERSION="v3") as vtotal:
-    # Your code here
-```
-
-Additionally, it is possible to provide an API key via the environment variable `VIRUSTOTAL_API_KEY`.
-
-Bash example:
-
-```bash
-export VIRUSTOTAL_API_KEY="Insert API key here."
-```
-
-PowerShell example:
-
-```powershell
-$Env:VIRUSTOTAL_API_KEY = "Insert API key here."
+# NOTE: To use proxies, you must have the PySocks extra installed
+with virustotal_python.Virustotal(
+    API_KEY="<VirusTotal API Key>",
+    PROXIES={"http": "http://10.10.1.10:3128", "https": "https://10.10.1.10:1080"},
+    TIMEOUT=5.0,
+) as vtotal:
+    # Your code here...
+
+# You can also omit the API_KEY parameter and provide your
+# API key via the environment variable VIRUSTOTAL_API_KEY
+# Bash: export VIRUSTOTAL_API_KEY="<VirusTotal API Key>"
+# PowerShell: $Env:VIRUSTOTAL_API_KEY = "<VirusTotal API Key>"
+# Then...
+with virustotal_python.Virustotal() as vtotal:
+    # Your code here...
 ```
 
-Now, initialise the `Virustotal` class:
+## Code Snippets
 
-```python
-from virustotal_python import Virustotal
-
-# v2 example
-vtotal = Virustotal()
-
-# v3 example
-vtotal = Virustotal(API_VERSION="v3")
-```
+> Further usage examples can be found in [examples](examples).
 
-Send a file for analysis:
+### Send a file for analysis 🔎
 
 ```python
+import virustotal_python
 import os.path
 from pprint import pprint
 
-# Declare PATH to file
 FILE_PATH = "/path/to/file/to/scan.txt"
 
 # Create dictionary containing the file to send for multipart encoding upload
 files = {"file": (os.path.basename(FILE_PATH), open(os.path.abspath(FILE_PATH), "rb"))}
 
-# v2 example
-resp = vtotal.request("file/scan", files=files, method="POST")
-
-# The v2 API returns a response_code
-# This property retrieves it from the JSON response
-print(resp.response_code)
-# Print JSON response from the API
-pprint(resp.json())
-
-# v3 example
-resp = vtotal.request("files", files=files, method="POST")
-
-# The v3 API returns the JSON response inside the 'data' key
-# https://developers.virustotal.com/v3.0/reference#api-responses
-# This property retrieves the structure inside 'data' from the JSON response
-pprint(resp.data)
-# Or if you provided COMPATIBILITY_ENABLED=True to the Virustotal class
-pprint(resp["json_resp"])
+with virustotal_python.Virustotal("<VirusTotal API Key>") as vtotal:
+    resp = vtotal.request("files", files=files, method="POST")
+    pprint(resp.json())
 ```
 
-Retrieve information about a file:
+### Get information about a file 📁
 
 ```python
+import virustotal_python
 from pprint import pprint
 
-# The ID (either SHA-256, SHA-1 or MD5) identifying the file
+# The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file
 FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
 
-# v2 example
-resp = vtotal.request("file/report", {"resource": FILE_ID})
-
-print(resp.response_code)
-pprint(resp.json())
-
-# v3 example
-resp = vtotal.request(f"files/{FILE_ID}")
-
-pprint(resp.data)
+with virustotal_python.Virustotal("<VirusTotal API Key>") as vtotal:
+    resp = vtotal.request(f"files/{FILE_ID}")
+    pprint(resp.data)
 ```
 
-Send a URL for analysis, retrieve the analysis report and catch any potential exceptions that may occur (Non 200 HTTP status codes):
+### Send a URL 🔗 for analysis and get the report 📄
 
 ```python
-from virustotal_python import VirustotalError
+import virustotal_python
 from pprint import pprint
 from base64 import urlsafe_b64encode
 
 url = "ihaveaproblem.info"
 
-# v2 example
-try:
-    # Send a URL to VirusTotal for analysis
-    resp = vtotal.request("url/scan", params={"url": url}, method="POST")
-    url_resp = resp.json()
-    # Obtain scan_id
-    scan_id = url_resp["scan_id"]
-    # Request report for URL analysis
-    analysis_resp = vtotal.request("url/report", params={"resource": scan_id})
-    print(analysis_resp.response_code)
-    pprint(analysis_resp.json())
-except VirustotalError as err:
-    print(f"An error occurred: {err}\nCatching and continuing with program.")
-
-# v3 example
-try:
-    # Send URL to VirusTotal for analysis
-    resp = vtotal.request("urls", data={"url": url}, method="POST")
-    # URL safe encode URL in base64 format
-    # https://developers.virustotal.com/v3.0/reference#url
-    url_id = urlsafe_b64encode(url.encode()).decode().strip("=")
-    # Obtain the analysis results for the URL using the url_id
-    analysis_resp = vtotal.request(f"urls/{url_id}")
-    pprint(analysis_resp.object_type)
-    pprint(analysis_resp.data)
-except VirustotalError as err:
-    print(f"An error occurred: {err}\nCatching and continuing with program.")
+with virustotal_python.Virustotal("<VirusTotal API Key>") as vtotal:
+    try:
+        resp = vtotal.request("urls", data={"url": url}, method="POST")
+        # Safe encode URL in base64 format
+        # https://developers.virustotal.com/reference/url
+        url_id = urlsafe_b64encode(url.encode()).decode().strip("=")
+        report = vtotal.request(f"urls/{url_id}")
+        pprint(report.object_type)
+        pprint(report.data)
+    except virustotal_python.VirustotalError as err:
+        print(f"Failed to send URL: {url} for analysis and get the report: {err}")
 ```
 
-Retrieve information about a domain:
+### Get information about a domain:
 
 ```python
+import virustotal_python
 from pprint import pprint
 
 domain = "virustotal.com"
 
-# v2 example
-resp = vtotal.request("domain/report", params={"domain": domain})
-
-print(resp.response_code)
-pprint(resp.json())
-
-# v3 example
-resp = vtotal.request(f"domains/{domain}")
-
-pprint(resp.data)
+with virustotal_python.Virustotal("<VirusTotal API Key>") as vtotal:
+    resp = vtotal.request(f"domains/{domain}")
+    pprint(resp.data)
 ```
 
-## Running the tests
-
-To run the tests, perform the following steps:
-
-1. Ensure pytest is installed using: `pip install pytest`
-
-2. Export your API key to the environment variable `VIRUSTOTAL_API_KEY` (instructions above).
-
-3. From the root directory of the project run `pytest -s .\virustotal_python\tests.py`
-
-## Changelog
-
-* 0.2.0 - Added `large_file` parameter to `request` so a file larger than 32MB can be submitted for analysis. See [#33](https://github.com/dbrennand/virustotal-python/pull/33). Thank you @smk762.
-
-* 0.1.3 - Update urllib3 to 1.26.5 to address [CVE-2021-33503](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33503).
+## Development
 
-* 0.1.2 - Update dependencies for security vulnerability. Fixed an issue with some tests failing.
+[Black](https://github.com/psf/black) is used for code formatting.
 
-* 0.1.1 - Added Context Manager support and tests. Updated dependencies and license year.
+### Unit Tests
 
-* 0.1.0 - Added support for the VirusTotal v3 API. Library redesign (new usage, examples, tests and more.) See [#24](https://github.com/dbrennand/virustotal-python/pull/24).
+Install the development dependencies using Poetry:
 
-* 0.0.9 - Update dependencies for security vulnerability.
-
-* 0.0.8 - Updated dependencies, removed method `file_rescan`
-
-* 0.0.7 - Added tests. Updated dependencies, Updated examples and README, `url_report` param `scan` now accepts `type(int)`, **no** longer `type(str)`
-
-* 0.0.6 - Fixed usage example and dependencies in README.md, Setup github.io website, updated requirements.txt.
-
-* 0.0.5 - Added Proxy support. Via HTTP(S) or using SOCKS: See [#8](https://github.com/dbrennand/virustotal-python/pull/8).
+```bash
+poetry install && poetry shell
+```
 
-* 0.0.4 - README.md updated; dependencies updated.
+To run the unit tests, run `pytest` from the root of the project:
 
-* 0.0.3 - Updated dependencies for urllib3 security vulnerability.
+```bash
+pytest --cov=virustotal_python
+```
 
-* 0.0.2 - Changes to file_rescan(), file_report(), url_scan(), url_report() to improve ease of use of the wrapper. See issue [#2](https://github.com/dbrennand/virustotal-python/issues/2). Examples updated for changes.
+### Publishing a new release
 
-* 0.0.1 - Initial release of virustotal-python. Covered all endpoints of the Virustotal public API.
+```bash
+# Run from the master branch
+export VERSION=x.x.x
+git commit --allow-empty -m "Publish $VERSION"
+git tag -a $VERSION -m "Version $VERSION"
+git push --tags
+```
 
-## Authors -- Contributors
+## Authors & Contributors
 
 * [**dbrennand**](https://github.com/dbrennand) - *Author*
 
 * [**smk762**](https://github.com/smk762) - *Contributor*
 
+## Changelog
+
+See the [CHANGELOG](CHANGELOG.md) for details.
+
 ## License
 This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details.
diff --git a/examples/comments.py b/examples/comments.py
index fef6fc6..6d6e9dd 100644
--- a/examples/comments.py
+++ b/examples/comments.py
@@ -5,114 +5,100 @@
 
 Documentation:
 
-    * v2 documentation - https://developers.virustotal.com/reference#comments-get
+    * v3 documentation
 
-        * https://developers.virustotal.com/reference#comments-put
+        https://developers.virustotal.com/reference/comments
+        https://developers.virustotal.com/reference/get-comments
+        https://developers.virustotal.com/reference/get-comment
+        https://developers.virustotal.com/reference/comment-id-patch
+        https://developers.virustotal.com/reference/comment-id-delete
+        https://developers.virustotal.com/reference/vote-comment
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#comments-1
+    * v2 documentation
+
+        https://developers.virustotal.com/v2.0/reference/comments-get
+        https://developers.virustotal.com/v2.0/reference/comments-put
 """
 from virustotal_python import Virustotal
 from base64 import urlsafe_b64encode
-from pprint import pprint
-
-API_KEY = "Insert API key here."
 
-# The ID (either SHA-256, SHA-1 or MD5) identifying the file
+API_KEY = "<VirusTotal API Key>"
+# The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file
 FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
-
-# URL/domain identifier
 DOMAIN = "google.com"
-
-# Obtain the URL ID
-URL_ID = urlsafe_b64encode(DOMAIN.encode()).decode().strip("=")
-
-# Example IP address (Google DNS)
+# Get the domain ID
+URL_ID = urlsafe_b64encode("https://github.com/home".encode()).decode().strip("=")
+# (Google DNS)
 IP = "8.8.8.8"
-
-# Example ID of a graph
-## NOTE: There are no comments on this graph so an empty list is returned
+# There are no comments on this graph so an empty list is returned
 GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d"
-
-# Example comment ID
-COMMENT_ID = "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-07457619"
-
-# v2 examples
-vtotal = Virustotal(API_KEY=API_KEY)
-
-# Retrieve comments for a given file ID
-resp = vtotal.request("comments/get", params={"resource": FILE_ID})
-
-pprint(resp.json())
-
-# Create a comment for a given file ID
-resp = vtotal.request("comments/put", params={"resource": FILE_ID, "comment": "Wow, this looks like a #malicious file!"}, method="POST")
-
-pprint(resp.json())
+COMMENT_ID = (
+    "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-07457619"
+)
 
 # v3 examples
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
-
-## Retriving comments for resources
-### Retrieve 10 comments for a file
+vtotal = Virustotal(API_KEY=API_KEY)
+# Get comments for resources
+# Get 10 comments for a file
 resp = vtotal.request(f"files/{FILE_ID}/comments", params={"limit": 10})
-### Retrieve 2 comments for a URL
+# Get 2 comments for a URL
 resp = vtotal.request(f"urls/{URL_ID}/comments", params={"limit": 2})
-### Retrieve 2 comments for a domain
+# Get 2 comments for a domain
 resp = vtotal.request(f"domains/{DOMAIN}/comments", params={"limit": 2})
-### Retrieve 5 comments for an IP address
+# Get 5 comments for an IP address
 resp = vtotal.request(f"ip_addresses/{IP}/comments", params={"limit": 5})
-### Retrieve 3 comments for a graph
+# Get 3 comments for a graph
 resp = vtotal.request(f"graphs/{GRAPH_ID}/comments", params={"limit": 3})
 
-## Submit a comment on a file, URL, domain, IP address or graph.
-## Prepare comment JSON
 comment = {
     "data": {
         "type": "comment",
-        "attributes": {
-            "text": "Watchout! This looks dangerous!"
-        }
+        "attributes": {"text": "Watchout! This looks dangerous!"},
     }
 }
 
-## Submit comments on a resource
-### Submit a comment on a file
+# Submit comments on a resource
+# Submit a comment on a file
 resp = vtotal.request(f"files/{FILE_ID}/comments", json=comment, method="POST")
-### Submit a comment on a URL
+# Submit a comment on a URL
 resp = vtotal.request(f"urls/{URL_ID}/comments", json=comment, method="POST")
-### Submit a comment on a domain
+# Submit a comment on a domain
 resp = vtotal.request(f"domains/{DOMAIN}/comments", json=comment, method="POST")
-### Submit a comment on a IP address
+# Submit a comment on a IP address
 resp = vtotal.request(f"ip_addresses/{IP}/comments", json=comment, method="POST")
-### Submit a comment on a graph
+# Submit a comment on a graph
 resp = vtotal.request(f"graphs/{GRAPH_ID}/comments", json=comment, method="POST")
 
-## Retrieve the latest comments added to VirusTotal
-### Retrieve the 10 latest comments added to VirusTotal with no filter
+# Get the  10 latest comments added to VirusTotal
 resp = vtotal.request("comments", params={"limit": 10})
-### Retrieve the 10 latest comments added to VirusTotal, filtering for Remote Access Trojan (RAT)
-#### When testing, for some reason there are comments that are returned which don't contain the tag 🤔
+# Get the 10 latest comments added to VirusTotal, filtering for Remote Access Trojan (RAT)
 resp = vtotal.request("comments", params={"limit": 10, "filter": "rat"})
-### Retrieve a specific comment based on the ID
+# Get a comment based on the ID
 resp = vtotal.request(f"comments/{COMMENT_ID}")
-### Edit a specific comment based on the ID
-#### Prepare comment JSON
-# Old comment was '#watchout, this looks very malicious!'
+
 edited_comment = {
     "data": {
         "type": "comment",
-        "attributes": {
-            "text": "#watchout, this looks quite malicious!"
-        }
+        "attributes": {"text": "#watchout, this looks quite malicious!"},
     }
 }
+# Edit a comment based on the ID
 resp = vtotal.request(f"comments/{COMMENT_ID}", json=edited_comment, method="PATCH")
-### Delete a comment based on the ID
+# Delete a comment based on the ID
 resp = vtotal.request(f"comments/{COMMENT_ID}", method="DELETE")
-### Submit a vote for a comment
-#### Vote options can be either positive, negative or abuse
-### Submit a positive vote on a comment based on the comment ID
-# This is what I got working
-## I found the documentation on this endpoint confusing... :-D
-### https://developers.virustotal.com/v3.0/reference#vote-comment
-resp = vtotal.request(f"comments/{COMMENT_ID}/vote", json={"data": "positive"}, method="POST")
+# Submit a vote for a comment
+# Vote options can be either positive, negative or abuse
+resp = vtotal.request(
+    f"comments/{COMMENT_ID}/vote", json={"data": "positive"}, method="POST"
+)
+
+# v2 examples
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2)
+# Get comments for a given file ID
+resp = vtotal.request("comments/get", params={"resource": FILE_ID})
+# Create a comment for a given file ID
+resp = vtotal.request(
+    "comments/put",
+    params={"resource": FILE_ID, "comment": "Wow, this looks like a #malicious file!"},
+    method="POST",
+)
diff --git a/examples/cursor.py b/examples/cursor.py
index 51e39b0..85a7f1b 100644
--- a/examples/cursor.py
+++ b/examples/cursor.py
@@ -5,31 +5,29 @@
 
 Documentation:
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#collections
+    * v3 documentation - https://developers.virustotal.com/reference/collections
 """
 from virustotal_python import Virustotal
-from pprint import pprint
 
-API_KEY = "Insert API key here."
-
-# Example IP address (Google DNS)
+API_KEY = "<VirusTotal API Key>"
+#  (Google DNS)
 IP = "8.8.8.8"
 
 # v3 example
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+vtotal = Virustotal(API_KEY=API_KEY)
 
-# Retrieve communicating_files related to the IP address with a limit of 5
+# Get communicating_files related to the IP address with a limit of 2
 resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 2})
 
-# Initialise count variable
 count = 0
-
 # While a cursor is present, keep collecting results!
 while resp.cursor:
-    print(count)
-    print(f"This is the current: {resp.cursor}")
-    # Get more results with cursor
-    resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 2, "cursor": resp.cursor})
+    print(f"Current count: {count} - Cursor: {resp.cursor}")
+    # Get more results using the cursor
+    resp = vtotal.request(
+        f"ip_addresses/{IP}/communicating_files",
+        params={"limit": 2, "cursor": resp.cursor},
+    )
     # Do something with the resp here
     # Add to the count to show how many times we have retrieved another cursor
     count += 1
diff --git a/examples/domain_info.py b/examples/domain_info.py
index 9be647f..5ee2698 100644
--- a/examples/domain_info.py
+++ b/examples/domain_info.py
@@ -5,28 +5,24 @@
 
 Documentation:
 
-    * v2 documentation - https://developers.virustotal.com/reference#domain-report
+    * v3 documentation - https://developers.virustotal.com/reference/domain-info
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#domain-info
+    * v2 documentation - https://developers.virustotal.com/v2.0/reference/domain-report
 """
 from virustotal_python import Virustotal
 from pprint import pprint
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
 domain = "virustotal.com"
 
-# v2 example
+# v3 example
 vtotal = Virustotal(API_KEY=API_KEY)
+resp = vtotal.request(f"domains/{domain}")
+pprint(resp.data)
 
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2)
 resp = vtotal.request("domain/report", params={"domain": domain})
-
 print(resp.response_code)
 pprint(resp.json())
-
-# v3 example
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
-
-resp = vtotal.request(f"domains/{domain}")
-
-pprint(resp.data)
diff --git a/examples/file_info.py b/examples/file_info.py
index 67554e2..1901204 100644
--- a/examples/file_info.py
+++ b/examples/file_info.py
@@ -5,29 +5,25 @@
 
 Documentation:
 
-    * v2 documentation - https://developers.virustotal.com/reference#file-report
+    * v3 documentation - https://developers.virustotal.com/reference/file-info
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#file-info
+    * v2 documentation - https://developers.virustotal.com/v2.0/reference/file-report
 """
 from virustotal_python import Virustotal
 from pprint import pprint
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
-# The ID (either SHA-256, SHA-1 or MD5) identifying the file
+# The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file
 FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
 
-# v2 example
+# v3 example
 vtotal = Virustotal(API_KEY=API_KEY)
+resp = vtotal.request(f"files/{FILE_ID}")
+pprint(resp.data)
 
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2)
 resp = vtotal.request("file/report", {"resource": FILE_ID})
-
 print(resp.response_code)
 pprint(resp.json())
-
-# v3 example
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
-
-resp = vtotal.request(f"files/{FILE_ID}")
-
-pprint(resp.data)
diff --git a/examples/graphs.py b/examples/graphs.py
index 1aff2f1..71b5e28 100644
--- a/examples/graphs.py
+++ b/examples/graphs.py
@@ -5,25 +5,31 @@
 
 Documentation:
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#graphs-1
+    * v3 documentation - https://developers.virustotal.com/reference/graphs
 """
 from virustotal_python import Virustotal
-from pprint import pprint
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
 # Example ID of a graph
 GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d"
 
 # v3 examples
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+vtotal = Virustotal(API_KEY=API_KEY)
 
-## Retrieve 3 graphs from the VirusTotal v3 API
+# Get 3 graphs from the VirusTotal API
 resp = vtotal.request("graphs", params={"limit": 3})
-## Retrieve 3 graphs from the VirusTotal v3 API filtering by owner, order and attributes
-resp = vtotal.request("graphs", params={"limit": 2, "filter": "owner:hugoklugman", "order": "views_count", "attributes": "graph_data"})
-### Retrieve a graph using the graph's ID
+# Get 3 graphs from the VirusTotal API filtering by owner, order and attributes
+resp = vtotal.request(
+    "graphs",
+    params={
+        "limit": 2,
+        "filter": "owner:hugoklugman",
+        "order": "views_count",
+        "attributes": "graph_data",
+    },
+)
+# Get a graph using the graph's ID
 resp = vtotal.request(f"graphs/{GRAPH_ID}")
 
-# For more graph endpints, see https://developers.virustotal.com/v3.0/reference#graphs-1
 # To create a graph, head to https://www.virustotal.com/graph/
diff --git a/examples/ip.py b/examples/ip.py
index 00e657a..afc141d 100644
--- a/examples/ip.py
+++ b/examples/ip.py
@@ -5,44 +5,42 @@
 
 Documentation:
 
-    * v2 documentation - https://developers.virustotal.com/reference#ip-address-report
+    * v3 documentation
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#ip-addresses
+    https://developers.virustotal.com/reference/ip-info
+    https://developers.virustotal.com/reference/ip-object#relationships
+    https://developers.virustotal.com/reference/ip-votes-post
+
+    * v2 documentation - https://developers.virustotal.com/v2.0/reference/ip-address-report
 """
 from virustotal_python import Virustotal
 from pprint import pprint
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
-# Example IP address (Google DNS)
+# (Google DNS)
 IP = "8.8.8.8"
 
-# v2 examples
-vtotal = Virustotal(API_KEY=API_KEY)
-
-## Retrieve information about an IP address
-resp = vtotal.request("ip-address/report", params={"ip": IP})
-
-pprint(resp.json())
-
 # v3 examples
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+vtotal = Virustotal(API_KEY=API_KEY)
 
-# Retrieve information about an IP address
+# Get information about an IP address
 resp = vtotal.request(f"ip_addresses/{IP}")
-# Retrieve objects (relationships) related to an IP address
-# Retrieve historical_whois relationship to the IP address
-# For other relationships, see the table at: https://developers.virustotal.com/v3.0/reference#ip-relationships
+# Get objects (relationships) related to an IP address
+# Get historical_whois relationship to the IP address
 resp = vtotal.request(f"ip_addresses/{IP}/historical_whois")
-# Retrieve communicating_files related to the IP address with a limit of 5
-resp = vtotal.request(
-    f"ip_addresses/{IP}/communicating_files", params={"limit": 5})
+# Get communicating_files related to the IP address with a limit of 5
+resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 5})
 
-# Retrieve votes for an IP address
+# Get votes for an IP address
 resp = vtotal.request(f"ip_addresses/{IP}/votes")
-# Send a vote for an IP address
-# Create vote JSON
-## Verdict can be either harmless or malicious
-### https://developers.virustotal.com/v3.0/reference#ip-votes-post
+# Add a vote for an IP address
+# Verdict can be either harmless or malicious
 vote = {"data": {"type": "vote", "attributes": {"verdict": "harmless"}}}
 resp = vtotal.request(f"ip_addresses/{IP}/votes", json=vote, method="POST")
+
+# v2 examples
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2)
+# Get information about an IP address
+resp = vtotal.request("ip-address/report", params={"ip": IP})
+pprint(resp.json())
diff --git a/virustotal_python/oldexamples.py b/examples/old_examples.py
similarity index 96%
rename from virustotal_python/oldexamples.py
rename to examples/old_examples.py
index 5b6f32e..db41b20 100644
--- a/virustotal_python/oldexamples.py
+++ b/examples/old_examples.py
@@ -3,7 +3,7 @@
 
 Examples for versions >=0.1.0 head to /examples directory.
 """
-from virustotal import Virustotal
+from virustotal_python import Virustotal
 from pprint import pprint
 import os.path
 
@@ -66,7 +66,7 @@
 
 # Retrieve url report(s)
 # A list containing the url of the report to be retrieved
-resp = vtotal.url_report(["ihaveaproblem.info"]) # Query a single url
+resp = vtotal.url_report(["ihaveaproblem.info"])  # Query a single url
 # A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate)
 # The first object in the list is a scan_id
 resp = vtotal.url_report(
diff --git a/examples/scan_file.py b/examples/scan_file.py
index 10fb606..3fda471 100644
--- a/examples/scan_file.py
+++ b/examples/scan_file.py
@@ -5,42 +5,31 @@
 
 Documentation:
 
-    * v2 documentation - https://developers.virustotal.com/reference#file-scan
+    * v3 documentation
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#files-scan
+        https://developers.virustotal.com/reference/files-scan
+        https://developers.virustotal.com/reference/files-upload-url
 
-        * https://developers.virustotal.com/reference/files-upload-url
+    * v2 documentation - https://developers.virustotal.com/v2.0/reference/file-scan
 """
 from virustotal_python import Virustotal
 import os.path
 from pprint import pprint
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
-# Declare PATH to file
 FILE_PATH = "/path/to/file/to/scan.txt"
 
 # Create dictionary containing the file to send for multipart encoding upload
 files = {"file": (os.path.basename(FILE_PATH), open(os.path.abspath(FILE_PATH), "rb"))}
 
-# v2 example
-vtotal = Virustotal(API_KEY=API_KEY)
-
-resp = vtotal.request("file/scan", files=files, method="POST")
-
-print(resp.response_code)
-pprint(resp.json())
-
 # v3 example
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
-
+vtotal = Virustotal(API_KEY=API_KEY)
 resp = vtotal.request("files", files=files, method="POST")
-
 pprint(resp.data)
 
 # v3 example for uploading a file larger than 32MB in size
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
-
+vtotal = Virustotal(API_KEY=API_KEY)
 # Create dictionary containing the large file to send for multipart encoding upload
 large_file = {
     "file": (
@@ -50,6 +39,12 @@
 }
 # Get URL to send a large file
 upload_url = vtotal.request("files/upload_url").data
-# Submit large file to VirusTotal v3 API for analysis
+# Submit large file to VirusTotal for analysis
 resp = vtotal.request(upload_url, files=large_file, method="POST", large_file=True)
 pprint(resp.data)
+
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2)
+resp = vtotal.request("file/scan", files=files, method="POST")
+print(resp.response_code)
+pprint(resp.json())
diff --git a/examples/scan_urls.py b/examples/scan_urls.py
index 9ffee16..df23ce6 100644
--- a/examples/scan_urls.py
+++ b/examples/scan_urls.py
@@ -5,48 +5,46 @@
 
 Documentation:
 
-    * v2 documentation - https://developers.virustotal.com/reference#url-scan
+    * v3 documentation
 
-        * https://developers.virustotal.com/reference#url-report
+        https://developers.virustotal.com/reference/scan-url
+        https://developers.virustotal.com/reference/url-info
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#urls
+    * v2 documentation
 
-        * https://developers.virustotal.com/v3.0/reference#url-info
+        https://developers.virustotal.com/v2.0/reference/url-scan
+        https://developers.virustotal.com/v2.0/reference/url-report
 """
 from virustotal_python import Virustotal
-import os.path
 from pprint import pprint
 from base64 import urlsafe_b64encode
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
-URLS = ["google.com", "wikipedia.com", "github.com", "ihaveaproblem.info"]
+URLS = ["google.com", "wikipedia.com", "github.com"]
 
-# v2 example
+# v3 example
 vtotal = Virustotal(API_KEY=API_KEY)
 
-# Send the URLs to VirusTotal for analysis
+for url in URLS:
+    resp = vtotal.request("urls", data={"url": url}, method="POST")
+    # Safe encode URL in base64 format
+    # https://developers.virustotal.com/reference/url
+    url_id = urlsafe_b64encode(url.encode()).decode().strip("=")
+    print(f"URL: {url} ID: {url_id}")
+    report = vtotal.request(f"urls/{url_id}")
+    print(report.object_type)
+    pprint(report.data)
+
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2)
+
 # A maximum of 4 URLs can be sent at once for a v2 API request
-resp = vtotal.request("url/scan", params={"url": "\n".join(url)}, method="POST")
+resp = vtotal.request("url/scan", params={"url": "\n".join(URLS)}, method="POST")
 for url_resp in resp.json():
-    # Obtain scan_id
     scan_id = url_resp["scan_id"]
+    print(scan_id)
     # Request report for URL analysis
     analysis_resp = vtotal.request("url/report", params={"resource": scan_id})
     print(analysis_resp.response_code)
     pprint(analysis_resp.json())
-
-# v3 example
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
-
-for url in URLS:
-    # Send the URL to VirusTotal for analysis
-    resp = vtotal.request("urls", data={"url": url}, method="POST")
-    # URL safe encode URL in base64 format
-    # https://developers.virustotal.com/v3.0/reference#url
-    url_id = urlsafe_b64encode(url.encode()).decode().strip("=")
-    print(f"URL: {url} ID: {url_id}")
-    # Obtain the analysis results for the URL using the url_id
-    analysis_resp = vtotal.request(f"urls/{url_id}")
-    print(analysis_resp.object_type)
-    pprint(analysis_resp.data)
diff --git a/examples/searchmetadata.py b/examples/searchmetadata.py
index c1c38ab..b1f34b4 100644
--- a/examples/searchmetadata.py
+++ b/examples/searchmetadata.py
@@ -7,33 +7,33 @@
 
 Documentation:
 
-    * v3 documentation - https://developers.virustotal.com/v3.0/reference#search-1
+    * v3 documentation
 
-        * https://developers.virustotal.com/v3.0/reference#metadata
+        https://developers.virustotal.com/reference/search-1
+        https://developers.virustotal.com/reference/metadata
 """
 from virustotal_python import Virustotal
-from pprint import pprint
 
-API_KEY = "Insert API key here."
+API_KEY = "<VirusTotal API Key>"
 
-# The ID (either SHA-256, SHA-1 or MD5) identifying the file
+# The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file
 FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
 
 # v3 examples
-vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+vtotal = Virustotal(API_KEY=API_KEY)
 
-## Search the VirusTotal API for google.com
+# Search the VirusTotal API for google.com
 resp = vtotal.request("search", params={"query": "google.com"})
-## Search the VirusTotal API for information related to Google's DNS (8.8.8.8)
+# Search the VirusTotal API for information related to Google's DNS (8.8.8.8)
 resp = vtotal.request("search", params={"query": "8.8.8.8"})
-## Search the VirusTotal API for a file ID
+# Search the VirusTotal API for a file ID
 resp = vtotal.request("search", params={"query": FILE_ID})
-## Search the VirusTotal API for the tag comment '#malicious'
+# Search the VirusTotal API for the tag comment '#malicious'
 resp = vtotal.request("search", params={"query": "#malicious"})
 
-## Retrieve VirusTotal metadata
+# Retrieve VirusTotal metadata
 resp = vtotal.request("metadata")
-## Print out a list of VirusTotal's supported engines
+# Print out a list of VirusTotal's supported engines
 resp = vtotal.request("metadata")
 engines_dict = resp.data["engines"]
 print(engines_dict.keys())
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..b1dffbe
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,923 @@
+[[package]]
+name = "atomicwrites"
+version = "1.4.0"
+description = "Atomic file writes."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "attrs"
+version = "21.4.0"
+description = "Classes Without Boilerplate"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[package.extras]
+dev = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "furo", "sphinx", "sphinx-notfound-page", "pre-commit", "cloudpickle"]
+docs = ["furo", "sphinx", "zope.interface", "sphinx-notfound-page"]
+tests = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "zope.interface", "cloudpickle"]
+tests_no_zope = ["coverage[toml] (>=5.0.2)", "hypothesis", "pympler", "pytest (>=4.3.0)", "six", "mypy", "pytest-mypy-plugins", "cloudpickle"]
+
+[[package]]
+name = "black"
+version = "22.3.0"
+description = "The uncompromising code formatter."
+category = "dev"
+optional = false
+python-versions = ">=3.6.2"
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""}
+typed-ast = {version = ">=1.4.2", markers = "python_version < \"3.8\" and implementation_name == \"cpython\""}
+typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""}
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "bleach"
+version = "4.1.0"
+description = "An easy safelist-based HTML-sanitizing tool."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+packaging = "*"
+six = ">=1.9.0"
+webencodings = "*"
+
+[[package]]
+name = "certifi"
+version = "2021.10.8"
+description = "Python package for providing Mozilla's CA Bundle."
+category = "main"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "cffi"
+version = "1.15.0"
+description = "Foreign Function Interface for Python calling C code."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+pycparser = "*"
+
+[[package]]
+name = "charset-normalizer"
+version = "2.0.12"
+description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
+category = "main"
+optional = false
+python-versions = ">=3.5.0"
+
+[package.extras]
+unicode_backport = ["unicodedata2"]
+
+[[package]]
+name = "click"
+version = "8.1.2"
+description = "Composable command line interface toolkit"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+importlib-metadata = {version = "*", markers = "python_version < \"3.8\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.4"
+description = "Cross-platform colored terminal text."
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "coverage"
+version = "6.3.2"
+description = "Code coverage measurement for Python"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+tomli = {version = "*", optional = true, markers = "extra == \"toml\""}
+
+[package.extras]
+toml = ["tomli"]
+
+[[package]]
+name = "cryptography"
+version = "36.0.2"
+description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cffi = ">=1.12"
+
+[package.extras]
+docs = ["sphinx (>=1.6.5,!=1.8.0,!=3.1.0,!=3.1.1)", "sphinx-rtd-theme"]
+docstest = ["pyenchant (>=1.6.11)", "twine (>=1.12.0)", "sphinxcontrib-spelling (>=4.0.1)"]
+pep8test = ["black", "flake8", "flake8-import-order", "pep8-naming"]
+sdist = ["setuptools_rust (>=0.11.4)"]
+ssh = ["bcrypt (>=3.1.5)"]
+test = ["pytest (>=6.2.0)", "pytest-cov", "pytest-subtests", "pytest-xdist", "pretend", "iso8601", "pytz", "hypothesis (>=1.11.4,!=3.79.2)"]
+
+[[package]]
+name = "docutils"
+version = "0.18.1"
+description = "Docutils -- Python Documentation Utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "idna"
+version = "3.3"
+description = "Internationalized Domain Names in Applications (IDNA)"
+category = "main"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "importlib-metadata"
+version = "4.11.3"
+description = "Read metadata from Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""}
+zipp = ">=0.5"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=9)", "rst.linker (>=1.9)"]
+perf = ["ipython"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "packaging", "pyfakefs", "flufl.flake8", "pytest-perf (>=0.9.2)", "pytest-black (>=0.3.7)", "pytest-mypy (>=0.9.1)", "importlib-resources (>=1.3)"]
+
+[[package]]
+name = "iniconfig"
+version = "1.1.1"
+description = "iniconfig: brain-dead simple config-ini parsing"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "jeepney"
+version = "0.7.1"
+description = "Low-level, pure Python DBus protocol wrapper."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+test = ["pytest", "pytest-trio", "pytest-asyncio", "testpath", "trio", "async-timeout"]
+trio = ["trio", "async-generator"]
+
+[[package]]
+name = "keyring"
+version = "23.5.0"
+description = "Store and access your passwords safely."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+importlib-metadata = ">=3.6"
+jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""}
+pywin32-ctypes = {version = "<0.1.0 || >0.1.0,<0.1.1 || >0.1.1", markers = "sys_platform == \"win32\""}
+SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""}
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "jaraco.tidelift (>=1.4)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[[package]]
+name = "mypy-extensions"
+version = "0.4.3"
+description = "Experimental type system extensions for programs checked with the mypy typechecker."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "packaging"
+version = "21.3"
+description = "Core utilities for Python packages"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+pyparsing = ">=2.0.2,<3.0.5 || >3.0.5"
+
+[[package]]
+name = "pathspec"
+version = "0.9.0"
+description = "Utility library for gitignore style pattern matching of file paths."
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7"
+
+[[package]]
+name = "pkginfo"
+version = "1.8.2"
+description = "Query metadatdata from sdists / bdists / installed packages."
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.extras]
+testing = ["coverage", "nose"]
+
+[[package]]
+name = "platformdirs"
+version = "2.5.1"
+description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"."
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"]
+test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"]
+
+[[package]]
+name = "pluggy"
+version = "1.0.0"
+description = "plugin and hook calling mechanisms for python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+
+[package.extras]
+dev = ["pre-commit", "tox"]
+testing = ["pytest", "pytest-benchmark"]
+
+[[package]]
+name = "py"
+version = "1.11.0"
+description = "library with cross-python path, ini-parsing, io, code, log facilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*"
+
+[[package]]
+name = "pycparser"
+version = "2.21"
+description = "C parser in Python"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pygments"
+version = "2.11.2"
+description = "Pygments is a syntax highlighting package written in Python."
+category = "dev"
+optional = false
+python-versions = ">=3.5"
+
+[[package]]
+name = "pyparsing"
+version = "3.0.7"
+description = "Python parsing module"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.extras]
+diagrams = ["jinja2", "railroad-diagrams"]
+
+[[package]]
+name = "pysocks"
+version = "1.7.1"
+description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*"
+
+[[package]]
+name = "pytest"
+version = "6.2.5"
+description = "pytest: simple powerful testing with Python"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""}
+attrs = ">=19.2.0"
+colorama = {version = "*", markers = "sys_platform == \"win32\""}
+importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""}
+iniconfig = "*"
+packaging = "*"
+pluggy = ">=0.12,<2.0"
+py = ">=1.8.2"
+toml = "*"
+
+[package.extras]
+testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"]
+
+[[package]]
+name = "pytest-cov"
+version = "3.0.0"
+description = "Pytest plugin for measuring coverage."
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+coverage = {version = ">=5.2.1", extras = ["toml"]}
+pytest = ">=4.6"
+
+[package.extras]
+testing = ["fields", "hunter", "process-tests", "six", "pytest-xdist", "virtualenv"]
+
+[[package]]
+name = "pytest-mock"
+version = "3.7.0"
+description = "Thin-wrapper around the mock package for easier use with pytest"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.dependencies]
+pytest = ">=5.0"
+
+[package.extras]
+dev = ["pre-commit", "tox", "pytest-asyncio"]
+
+[[package]]
+name = "pywin32-ctypes"
+version = "0.2.0"
+description = ""
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "readme-renderer"
+version = "34.0"
+description = "readme_renderer is a library for rendering \"readme\" descriptions for Warehouse"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+bleach = ">=2.1.0"
+docutils = ">=0.13.1"
+Pygments = ">=2.5.1"
+
+[package.extras]
+md = ["cmarkgfm (>=0.8.0)"]
+
+[[package]]
+name = "requests"
+version = "2.27.1"
+description = "Python HTTP for Humans."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*"
+
+[package.dependencies]
+certifi = ">=2017.4.17"
+charset-normalizer = {version = ">=2.0.0,<2.1.0", markers = "python_version >= \"3\""}
+idna = {version = ">=2.5,<4", markers = "python_version >= \"3\""}
+PySocks = {version = ">=1.5.6,<1.5.7 || >1.5.7", optional = true, markers = "extra == \"socks\""}
+urllib3 = ">=1.21.1,<1.27"
+
+[package.extras]
+socks = ["PySocks (>=1.5.6,!=1.5.7)", "win-inet-pton"]
+use_chardet_on_py3 = ["chardet (>=3.0.2,<5)"]
+
+[[package]]
+name = "requests-mock"
+version = "1.9.3"
+description = "Mock out responses from the requests package"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=2.3,<3"
+six = "*"
+
+[package.extras]
+fixture = ["fixtures"]
+test = ["fixtures", "mock", "purl", "pytest", "sphinx", "testrepository (>=0.0.18)", "testtools"]
+
+[[package]]
+name = "requests-toolbelt"
+version = "0.9.1"
+description = "A utility belt for advanced users of python-requests"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[package.dependencies]
+requests = ">=2.0.1,<3.0.0"
+
+[[package]]
+name = "rfc3986"
+version = "2.0.0"
+description = "Validating URI References per RFC 3986"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+idna2008 = ["idna"]
+
+[[package]]
+name = "secretstorage"
+version = "3.3.1"
+description = "Python bindings to FreeDesktop.org Secret Service API"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+cryptography = ">=2.0"
+jeepney = ">=0.6"
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+category = "dev"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "toml"
+version = "0.10.2"
+description = "Python Library for Tom's Obvious, Minimal Language"
+category = "dev"
+optional = false
+python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*"
+
+[[package]]
+name = "tomli"
+version = "1.2.3"
+description = "A lil' TOML parser"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "tqdm"
+version = "4.63.1"
+description = "Fast, Extensible Progress Meter"
+category = "dev"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,>=2.7"
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[package.extras]
+dev = ["py-make (>=0.1.0)", "twine", "wheel"]
+notebook = ["ipywidgets (>=6)"]
+telegram = ["requests"]
+
+[[package]]
+name = "twine"
+version = "3.8.0"
+description = "Collection of utilities for publishing packages on PyPI"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[package.dependencies]
+colorama = ">=0.4.3"
+importlib-metadata = ">=3.6"
+keyring = ">=15.1"
+pkginfo = ">=1.8.1"
+readme-renderer = ">=21.0"
+requests = ">=2.20"
+requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0"
+rfc3986 = ">=1.4.0"
+tqdm = ">=4.14"
+urllib3 = ">=1.26.0"
+
+[[package]]
+name = "typed-ast"
+version = "1.5.2"
+description = "a fork of Python 2 and 3 ast modules with type comment support"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "typing-extensions"
+version = "4.1.1"
+description = "Backported and Experimental Type Hints for Python 3.6+"
+category = "dev"
+optional = false
+python-versions = ">=3.6"
+
+[[package]]
+name = "urllib3"
+version = "1.26.9"
+description = "HTTP library with thread-safe connection pooling, file post, and more."
+category = "main"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, <4"
+
+[package.extras]
+brotli = ["brotlicffi (>=0.8.0)", "brotli (>=1.0.9)", "brotlipy (>=0.6.0)"]
+secure = ["pyOpenSSL (>=0.14)", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "certifi", "ipaddress"]
+socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"]
+
+[[package]]
+name = "webencodings"
+version = "0.5.1"
+description = "Character encoding aliases for legacy web content"
+category = "dev"
+optional = false
+python-versions = "*"
+
+[[package]]
+name = "zipp"
+version = "3.7.0"
+description = "Backport of pathlib-compatible object wrapper for zip files"
+category = "dev"
+optional = false
+python-versions = ">=3.7"
+
+[package.extras]
+docs = ["sphinx", "jaraco.packaging (>=8.2)", "rst.linker (>=1.9)"]
+testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-flake8", "pytest-cov", "pytest-enabler (>=1.0.1)", "jaraco.itertools", "func-timeout", "pytest-black (>=0.3.7)", "pytest-mypy"]
+
+[extras]
+socks = []
+
+[metadata]
+lock-version = "1.1"
+python-versions = "^3.7"
+content-hash = "8bd2b5ff4c449497cb9c14679ac7529d08dd1c762342b9bf239738fe027c005e"
+
+[metadata.files]
+atomicwrites = [
+    {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"},
+    {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"},
+]
+attrs = [
+    {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"},
+    {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"},
+]
+black = [
+    {file = "black-22.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:2497f9c2386572e28921fa8bec7be3e51de6801f7459dffd6e62492531c47e09"},
+    {file = "black-22.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5795a0375eb87bfe902e80e0c8cfaedf8af4d49694d69161e5bd3206c18618bb"},
+    {file = "black-22.3.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e3556168e2e5c49629f7b0f377070240bd5511e45e25a4497bb0073d9dda776a"},
+    {file = "black-22.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67c8301ec94e3bcc8906740fe071391bce40a862b7be0b86fb5382beefecd968"},
+    {file = "black-22.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:fd57160949179ec517d32ac2ac898b5f20d68ed1a9c977346efbac9c2f1e779d"},
+    {file = "black-22.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:cc1e1de68c8e5444e8f94c3670bb48a2beef0e91dddfd4fcc29595ebd90bb9ce"},
+    {file = "black-22.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6d2fc92002d44746d3e7db7cf9313cf4452f43e9ea77a2c939defce3b10b5c82"},
+    {file = "black-22.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:a6342964b43a99dbc72f72812bf88cad8f0217ae9acb47c0d4f141a6416d2d7b"},
+    {file = "black-22.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:328efc0cc70ccb23429d6be184a15ce613f676bdfc85e5fe8ea2a9354b4e9015"},
+    {file = "black-22.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06f9d8846f2340dfac80ceb20200ea5d1b3f181dd0556b47af4e8e0b24fa0a6b"},
+    {file = "black-22.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:ad4efa5fad66b903b4a5f96d91461d90b9507a812b3c5de657d544215bb7877a"},
+    {file = "black-22.3.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8477ec6bbfe0312c128e74644ac8a02ca06bcdb8982d4ee06f209be28cdf163"},
+    {file = "black-22.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:637a4014c63fbf42a692d22b55d8ad6968a946b4a6ebc385c5505d9625b6a464"},
+    {file = "black-22.3.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:863714200ada56cbc366dc9ae5291ceb936573155f8bf8e9de92aef51f3ad0f0"},
+    {file = "black-22.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10dbe6e6d2988049b4655b2b739f98785a884d4d6b85bc35133a8fb9a2233176"},
+    {file = "black-22.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:cee3e11161dde1b2a33a904b850b0899e0424cc331b7295f2a9698e79f9a69a0"},
+    {file = "black-22.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5891ef8abc06576985de8fa88e95ab70641de6c1fca97e2a15820a9b69e51b20"},
+    {file = "black-22.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:30d78ba6bf080eeaf0b7b875d924b15cd46fec5fd044ddfbad38c8ea9171043a"},
+    {file = "black-22.3.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ee8f1f7228cce7dffc2b464f07ce769f478968bfb3dd1254a4c2eeed84928aad"},
+    {file = "black-22.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ee227b696ca60dd1c507be80a6bc849a5a6ab57ac7352aad1ffec9e8b805f21"},
+    {file = "black-22.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:9b542ced1ec0ceeff5b37d69838106a6348e60db7b8fdd245294dc1d26136265"},
+    {file = "black-22.3.0-py3-none-any.whl", hash = "sha256:bc58025940a896d7e5356952228b68f793cf5fcb342be703c3a2669a1488cb72"},
+    {file = "black-22.3.0.tar.gz", hash = "sha256:35020b8886c022ced9282b51b5a875b6d1ab0c387b31a065b84db7c33085ca79"},
+]
+bleach = [
+    {file = "bleach-4.1.0-py2.py3-none-any.whl", hash = "sha256:4d2651ab93271d1129ac9cbc679f524565cc8a1b791909c4a51eac4446a15994"},
+    {file = "bleach-4.1.0.tar.gz", hash = "sha256:0900d8b37eba61a802ee40ac0061f8c2b5dee29c1927dd1d233e075ebf5a71da"},
+]
+certifi = [
+    {file = "certifi-2021.10.8-py2.py3-none-any.whl", hash = "sha256:d62a0163eb4c2344ac042ab2bdf75399a71a2d8c7d47eac2e2ee91b9d6339569"},
+    {file = "certifi-2021.10.8.tar.gz", hash = "sha256:78884e7c1d4b00ce3cea67b44566851c4343c120abd683433ce934a68ea58872"},
+]
+cffi = [
+    {file = "cffi-1.15.0-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:c2502a1a03b6312837279c8c1bd3ebedf6c12c4228ddbad40912d671ccc8a962"},
+    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_i686.whl", hash = "sha256:23cfe892bd5dd8941608f93348c0737e369e51c100d03718f108bf1add7bd6d0"},
+    {file = "cffi-1.15.0-cp27-cp27m-manylinux1_x86_64.whl", hash = "sha256:41d45de54cd277a7878919867c0f08b0cf817605e4eb94093e7516505d3c8d14"},
+    {file = "cffi-1.15.0-cp27-cp27m-win32.whl", hash = "sha256:4a306fa632e8f0928956a41fa8e1d6243c71e7eb59ffbd165fc0b41e316b2474"},
+    {file = "cffi-1.15.0-cp27-cp27m-win_amd64.whl", hash = "sha256:e7022a66d9b55e93e1a845d8c9eba2a1bebd4966cd8bfc25d9cd07d515b33fa6"},
+    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_i686.whl", hash = "sha256:14cd121ea63ecdae71efa69c15c5543a4b5fbcd0bbe2aad864baca0063cecf27"},
+    {file = "cffi-1.15.0-cp27-cp27mu-manylinux1_x86_64.whl", hash = "sha256:d4d692a89c5cf08a8557fdeb329b82e7bf609aadfaed6c0d79f5a449a3c7c023"},
+    {file = "cffi-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0104fb5ae2391d46a4cb082abdd5c69ea4eab79d8d44eaaf79f1b1fd806ee4c2"},
+    {file = "cffi-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:91ec59c33514b7c7559a6acda53bbfe1b283949c34fe7440bcf917f96ac0723e"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:f5c7150ad32ba43a07c4479f40241756145a1f03b43480e058cfd862bf5041c7"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00c878c90cb53ccfaae6b8bc18ad05d2036553e6d9d1d9dbcf323bbe83854ca3"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:abb9a20a72ac4e0fdb50dae135ba5e77880518e742077ced47eb1499e29a443c"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a5263e363c27b653a90078143adb3d076c1a748ec9ecc78ea2fb916f9b861962"},
+    {file = "cffi-1.15.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f54a64f8b0c8ff0b64d18aa76675262e1700f3995182267998c31ae974fbc382"},
+    {file = "cffi-1.15.0-cp310-cp310-win32.whl", hash = "sha256:c21c9e3896c23007803a875460fb786118f0cdd4434359577ea25eb556e34c55"},
+    {file = "cffi-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:5e069f72d497312b24fcc02073d70cb989045d1c91cbd53979366077959933e0"},
+    {file = "cffi-1.15.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:64d4ec9f448dfe041705426000cc13e34e6e5bb13736e9fd62e34a0b0c41566e"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2756c88cbb94231c7a147402476be2c4df2f6078099a6f4a480d239a8817ae39"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3b96a311ac60a3f6be21d2572e46ce67f09abcf4d09344c49274eb9e0bf345fc"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75e4024375654472cc27e91cbe9eaa08567f7fbdf822638be2814ce059f58032"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:59888172256cac5629e60e72e86598027aca6bf01fa2465bdb676d37636573e8"},
+    {file = "cffi-1.15.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:27c219baf94952ae9d50ec19651a687b826792055353d07648a5695413e0c605"},
+    {file = "cffi-1.15.0-cp36-cp36m-win32.whl", hash = "sha256:4958391dbd6249d7ad855b9ca88fae690783a6be9e86df65865058ed81fc860e"},
+    {file = "cffi-1.15.0-cp36-cp36m-win_amd64.whl", hash = "sha256:f6f824dc3bce0edab5f427efcfb1d63ee75b6fcb7282900ccaf925be84efb0fc"},
+    {file = "cffi-1.15.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:06c48159c1abed75c2e721b1715c379fa3200c7784271b3c46df01383b593636"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c2051981a968d7de9dd2d7b87bcb9c939c74a34626a6e2f8181455dd49ed69e4"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:fd8a250edc26254fe5b33be00402e6d287f562b6a5b2152dec302fa15bb3e997"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:91d77d2a782be4274da750752bb1650a97bfd8f291022b379bb8e01c66b4e96b"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45db3a33139e9c8f7c09234b5784a5e33d31fd6907800b316decad50af323ff2"},
+    {file = "cffi-1.15.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:263cc3d821c4ab2213cbe8cd8b355a7f72a8324577dc865ef98487c1aeee2bc7"},
+    {file = "cffi-1.15.0-cp37-cp37m-win32.whl", hash = "sha256:17771976e82e9f94976180f76468546834d22a7cc404b17c22df2a2c81db0c66"},
+    {file = "cffi-1.15.0-cp37-cp37m-win_amd64.whl", hash = "sha256:3415c89f9204ee60cd09b235810be700e993e343a408693e80ce7f6a40108029"},
+    {file = "cffi-1.15.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4238e6dab5d6a8ba812de994bbb0a79bddbdf80994e4ce802b6f6f3142fcc880"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:0808014eb713677ec1292301ea4c81ad277b6cdf2fdd90fd540af98c0b101d20"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:57e9ac9ccc3101fac9d6014fba037473e4358ef4e89f8e181f8951a2c0162024"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b6c2ea03845c9f501ed1313e78de148cd3f6cad741a75d43a29b43da27f2e1e"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:10dffb601ccfb65262a27233ac273d552ddc4d8ae1bf93b21c94b8511bffe728"},
+    {file = "cffi-1.15.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:786902fb9ba7433aae840e0ed609f45c7bcd4e225ebb9c753aa39725bb3e6ad6"},
+    {file = "cffi-1.15.0-cp38-cp38-win32.whl", hash = "sha256:da5db4e883f1ce37f55c667e5c0de439df76ac4cb55964655906306918e7363c"},
+    {file = "cffi-1.15.0-cp38-cp38-win_amd64.whl", hash = "sha256:181dee03b1170ff1969489acf1c26533710231c58f95534e3edac87fff06c443"},
+    {file = "cffi-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:45e8636704eacc432a206ac7345a5d3d2c62d95a507ec70d62f23cd91770482a"},
+    {file = "cffi-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:31fb708d9d7c3f49a60f04cf5b119aeefe5644daba1cd2a0fe389b674fd1de37"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6dc2737a3674b3e344847c8686cf29e500584ccad76204efea14f451d4cc669a"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74fdfdbfdc48d3f47148976f49fab3251e550a8720bebc99bf1483f5bfb5db3e"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ffaa5c925128e29efbde7301d8ecaf35c8c60ffbcd6a1ffd3a552177c8e5e796"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3f7d084648d77af029acb79a0ff49a0ad7e9d09057a9bf46596dac9514dc07df"},
+    {file = "cffi-1.15.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef1f279350da2c586a69d32fc8733092fd32cc8ac95139a00377841f59a3f8d8"},
+    {file = "cffi-1.15.0-cp39-cp39-win32.whl", hash = "sha256:2a23af14f408d53d5e6cd4e3d9a24ff9e05906ad574822a10563efcef137979a"},
+    {file = "cffi-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:3773c4d81e6e818df2efbc7dd77325ca0dcb688116050fb2b3011218eda36139"},
+    {file = "cffi-1.15.0.tar.gz", hash = "sha256:920f0d66a896c2d99f0adbb391f990a84091179542c205fa53ce5787aff87954"},
+]
+charset-normalizer = [
+    {file = "charset-normalizer-2.0.12.tar.gz", hash = "sha256:2857e29ff0d34db842cd7ca3230549d1a697f96ee6d3fb071cfa6c7393832597"},
+    {file = "charset_normalizer-2.0.12-py3-none-any.whl", hash = "sha256:6881edbebdb17b39b4eaaa821b438bf6eddffb4468cf344f09f89def34a8b1df"},
+]
+click = [
+    {file = "click-8.1.2-py3-none-any.whl", hash = "sha256:24e1a4a9ec5bf6299411369b208c1df2188d9eb8d916302fe6bf03faed227f1e"},
+    {file = "click-8.1.2.tar.gz", hash = "sha256:479707fe14d9ec9a0757618b7a100a0ae4c4e236fac5b7f80ca68028141a1a72"},
+]
+colorama = [
+    {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"},
+    {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"},
+]
+coverage = [
+    {file = "coverage-6.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9b27d894748475fa858f9597c0ee1d4829f44683f3813633aaf94b19cb5453cf"},
+    {file = "coverage-6.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:37d1141ad6b2466a7b53a22e08fe76994c2d35a5b6b469590424a9953155afac"},
+    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9987b0354b06d4df0f4d3e0ec1ae76d7ce7cbca9a2f98c25041eb79eec766f1"},
+    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:26e2deacd414fc2f97dd9f7676ee3eaecd299ca751412d89f40bc01557a6b1b4"},
+    {file = "coverage-6.3.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4dd8bafa458b5c7d061540f1ee9f18025a68e2d8471b3e858a9dad47c8d41903"},
+    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:46191097ebc381fbf89bdce207a6c107ac4ec0890d8d20f3360345ff5976155c"},
+    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6f89d05e028d274ce4fa1a86887b071ae1755082ef94a6740238cd7a8178804f"},
+    {file = "coverage-6.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:58303469e9a272b4abdb9e302a780072c0633cdcc0165db7eec0f9e32f901e05"},
+    {file = "coverage-6.3.2-cp310-cp310-win32.whl", hash = "sha256:2fea046bfb455510e05be95e879f0e768d45c10c11509e20e06d8fcaa31d9e39"},
+    {file = "coverage-6.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:a2a8b8bcc399edb4347a5ca8b9b87e7524c0967b335fbb08a83c8421489ddee1"},
+    {file = "coverage-6.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:f1555ea6d6da108e1999b2463ea1003fe03f29213e459145e70edbaf3e004aaa"},
+    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e5f4e1edcf57ce94e5475fe09e5afa3e3145081318e5fd1a43a6b4539a97e518"},
+    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7a15dc0a14008f1da3d1ebd44bdda3e357dbabdf5a0b5034d38fcde0b5c234b7"},
+    {file = "coverage-6.3.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:21b7745788866028adeb1e0eca3bf1101109e2dc58456cb49d2d9b99a8c516e6"},
+    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:8ce257cac556cb03be4a248d92ed36904a59a4a5ff55a994e92214cde15c5bad"},
+    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:b0be84e5a6209858a1d3e8d1806c46214e867ce1b0fd32e4ea03f4bd8b2e3359"},
+    {file = "coverage-6.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:acf53bc2cf7282ab9b8ba346746afe703474004d9e566ad164c91a7a59f188a4"},
+    {file = "coverage-6.3.2-cp37-cp37m-win32.whl", hash = "sha256:8bdde1177f2311ee552f47ae6e5aa7750c0e3291ca6b75f71f7ffe1f1dab3dca"},
+    {file = "coverage-6.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:b31651d018b23ec463e95cf10070d0b2c548aa950a03d0b559eaa11c7e5a6fa3"},
+    {file = "coverage-6.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:07e6db90cd9686c767dcc593dff16c8c09f9814f5e9c51034066cad3373b914d"},
+    {file = "coverage-6.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2c6dbb42f3ad25760010c45191e9757e7dce981cbfb90e42feef301d71540059"},
+    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c76aeef1b95aff3905fb2ae2d96e319caca5b76fa41d3470b19d4e4a3a313512"},
+    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8cf5cfcb1521dc3255d845d9dca3ff204b3229401994ef8d1984b32746bb45ca"},
+    {file = "coverage-6.3.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8fbbdc8d55990eac1b0919ca69eb5a988a802b854488c34b8f37f3e2025fa90d"},
+    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ec6bc7fe73a938933d4178c9b23c4e0568e43e220aef9472c4f6044bfc6dd0f0"},
+    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:9baff2a45ae1f17c8078452e9e5962e518eab705e50a0aa8083733ea7d45f3a6"},
+    {file = "coverage-6.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:fd9e830e9d8d89b20ab1e5af09b32d33e1a08ef4c4e14411e559556fd788e6b2"},
+    {file = "coverage-6.3.2-cp38-cp38-win32.whl", hash = "sha256:f7331dbf301b7289013175087636bbaf5b2405e57259dd2c42fdcc9fcc47325e"},
+    {file = "coverage-6.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:68353fe7cdf91f109fc7d474461b46e7f1f14e533e911a2a2cbb8b0fc8613cf1"},
+    {file = "coverage-6.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b78e5afb39941572209f71866aa0b206c12f0109835aa0d601e41552f9b3e620"},
+    {file = "coverage-6.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:4e21876082ed887baed0146fe222f861b5815455ada3b33b890f4105d806128d"},
+    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:34626a7eee2a3da12af0507780bb51eb52dca0e1751fd1471d0810539cefb536"},
+    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1ebf730d2381158ecf3dfd4453fbca0613e16eaa547b4170e2450c9707665ce7"},
+    {file = "coverage-6.3.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd6fe30bd519694b356cbfcaca9bd5c1737cddd20778c6a581ae20dc8c04def2"},
+    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:96f8a1cb43ca1422f36492bebe63312d396491a9165ed3b9231e778d43a7fca4"},
+    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:dd035edafefee4d573140a76fdc785dc38829fe5a455c4bb12bac8c20cfc3d69"},
+    {file = "coverage-6.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5ca5aeb4344b30d0bec47481536b8ba1181d50dbe783b0e4ad03c95dc1296684"},
+    {file = "coverage-6.3.2-cp39-cp39-win32.whl", hash = "sha256:f5fa5803f47e095d7ad8443d28b01d48c0359484fec1b9d8606d0e3282084bc4"},
+    {file = "coverage-6.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:9548f10d8be799551eb3a9c74bbf2b4934ddb330e08a73320123c07f95cc2d92"},
+    {file = "coverage-6.3.2-pp36.pp37.pp38-none-any.whl", hash = "sha256:18d520c6860515a771708937d2f78f63cc47ab3b80cb78e86573b0a760161faf"},
+    {file = "coverage-6.3.2.tar.gz", hash = "sha256:03e2a7826086b91ef345ff18742ee9fc47a6839ccd517061ef8fa1976e652ce9"},
+]
+cryptography = [
+    {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:4e2dddd38a5ba733be6a025a1475a9f45e4e41139d1321f412c6b360b19070b6"},
+    {file = "cryptography-36.0.2-cp36-abi3-macosx_10_10_x86_64.whl", hash = "sha256:4881d09298cd0b669bb15b9cfe6166f16fc1277b4ed0d04a22f3d6430cb30f1d"},
+    {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:ea634401ca02367c1567f012317502ef3437522e2fc44a3ea1844de028fa4b84"},
+    {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:7be666cc4599b415f320839e36367b273db8501127b38316f3b9f22f17a0b815"},
+    {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8241cac0aae90b82d6b5c443b853723bcc66963970c67e56e71a2609dc4b5eaf"},
+    {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b2d54e787a884ffc6e187262823b6feb06c338084bbe80d45166a1cb1c6c5bf"},
+    {file = "cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl", hash = "sha256:c2c5250ff0d36fd58550252f54915776940e4e866f38f3a7866d92b32a654b86"},
+    {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:ec6597aa85ce03f3e507566b8bcdf9da2227ec86c4266bd5e6ab4d9e0cc8dab2"},
+    {file = "cryptography-36.0.2-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:ca9f686517ec2c4a4ce930207f75c00bf03d94e5063cbc00a1dc42531511b7eb"},
+    {file = "cryptography-36.0.2-cp36-abi3-win32.whl", hash = "sha256:f64b232348ee82f13aac22856515ce0195837f6968aeaa94a3d0353ea2ec06a6"},
+    {file = "cryptography-36.0.2-cp36-abi3-win_amd64.whl", hash = "sha256:53e0285b49fd0ab6e604f4c5d9c5ddd98de77018542e88366923f152dbeb3c29"},
+    {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:32db5cc49c73f39aac27574522cecd0a4bb7384e71198bc65a0d23f901e89bb7"},
+    {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b3d199647468d410994dbeb8cec5816fb74feb9368aedf300af709ef507e3e"},
+    {file = "cryptography-36.0.2-pp37-pypy37_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:da73d095f8590ad437cd5e9faf6628a218aa7c387e1fdf67b888b47ba56a17f0"},
+    {file = "cryptography-36.0.2-pp38-pypy38_pp73-macosx_10_10_x86_64.whl", hash = "sha256:0a3bf09bb0b7a2c93ce7b98cb107e9170a90c51a0162a20af1c61c765b90e60b"},
+    {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:8897b7b7ec077c819187a123174b645eb680c13df68354ed99f9b40a50898f77"},
+    {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82740818f2f240a5da8dfb8943b360e4f24022b093207160c77cadade47d7c85"},
+    {file = "cryptography-36.0.2-pp38-pypy38_pp73-manylinux_2_24_x86_64.whl", hash = "sha256:1f64a62b3b75e4005df19d3b5235abd43fa6358d5516cfc43d87aeba8d08dd51"},
+    {file = "cryptography-36.0.2-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:e167b6b710c7f7bc54e67ef593f8731e1f45aa35f8a8a7b72d6e42ec76afd4b3"},
+    {file = "cryptography-36.0.2.tar.gz", hash = "sha256:70f8f4f7bb2ac9f340655cbac89d68c527af5bb4387522a8413e841e3e6628c9"},
+]
+docutils = [
+    {file = "docutils-0.18.1-py2.py3-none-any.whl", hash = "sha256:23010f129180089fbcd3bc08cfefccb3b890b0050e1ca00c867036e9d161b98c"},
+    {file = "docutils-0.18.1.tar.gz", hash = "sha256:679987caf361a7539d76e584cbeddc311e3aee937877c87346f31debc63e9d06"},
+]
+idna = [
+    {file = "idna-3.3-py3-none-any.whl", hash = "sha256:84d9dd047ffa80596e0f246e2eab0b391788b0503584e8945f2368256d2735ff"},
+    {file = "idna-3.3.tar.gz", hash = "sha256:9d643ff0a55b762d5cdb124b8eaa99c66322e2157b69160bc32796e824360e6d"},
+]
+importlib-metadata = [
+    {file = "importlib_metadata-4.11.3-py3-none-any.whl", hash = "sha256:1208431ca90a8cca1a6b8af391bb53c1a2db74e5d1cef6ddced95d4b2062edc6"},
+    {file = "importlib_metadata-4.11.3.tar.gz", hash = "sha256:ea4c597ebf37142f827b8f39299579e31685c31d3a438b59f469406afd0f2539"},
+]
+iniconfig = [
+    {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
+    {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
+]
+jeepney = [
+    {file = "jeepney-0.7.1-py3-none-any.whl", hash = "sha256:1b5a0ea5c0e7b166b2f5895b91a08c14de8915afda4407fb5022a195224958ac"},
+    {file = "jeepney-0.7.1.tar.gz", hash = "sha256:fa9e232dfa0c498bd0b8a3a73b8d8a31978304dcef0515adc859d4e096f96f4f"},
+]
+keyring = [
+    {file = "keyring-23.5.0-py3-none-any.whl", hash = "sha256:b0d28928ac3ec8e42ef4cc227822647a19f1d544f21f96457965dc01cf555261"},
+    {file = "keyring-23.5.0.tar.gz", hash = "sha256:9012508e141a80bd1c0b6778d5c610dd9f8c464d75ac6774248500503f972fb9"},
+]
+mypy-extensions = [
+    {file = "mypy_extensions-0.4.3-py2.py3-none-any.whl", hash = "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d"},
+    {file = "mypy_extensions-0.4.3.tar.gz", hash = "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"},
+]
+packaging = [
+    {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"},
+    {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"},
+]
+pathspec = [
+    {file = "pathspec-0.9.0-py2.py3-none-any.whl", hash = "sha256:7d15c4ddb0b5c802d161efc417ec1a2558ea2653c2e8ad9c19098201dc1c993a"},
+    {file = "pathspec-0.9.0.tar.gz", hash = "sha256:e564499435a2673d586f6b2130bb5b95f04a3ba06f81b8f895b651a3c76aabb1"},
+]
+pkginfo = [
+    {file = "pkginfo-1.8.2-py2.py3-none-any.whl", hash = "sha256:c24c487c6a7f72c66e816ab1796b96ac6c3d14d49338293d2141664330b55ffc"},
+    {file = "pkginfo-1.8.2.tar.gz", hash = "sha256:542e0d0b6750e2e21c20179803e40ab50598d8066d51097a0e382cba9eb02bff"},
+]
+platformdirs = [
+    {file = "platformdirs-2.5.1-py3-none-any.whl", hash = "sha256:bcae7cab893c2d310a711b70b24efb93334febe65f8de776ee320b517471e227"},
+    {file = "platformdirs-2.5.1.tar.gz", hash = "sha256:7535e70dfa32e84d4b34996ea99c5e432fa29a708d0f4e394bbcb2a8faa4f16d"},
+]
+pluggy = [
+    {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"},
+    {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"},
+]
+py = [
+    {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"},
+    {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"},
+]
+pycparser = [
+    {file = "pycparser-2.21-py2.py3-none-any.whl", hash = "sha256:8ee45429555515e1f6b185e78100aea234072576aa43ab53aefcae078162fca9"},
+    {file = "pycparser-2.21.tar.gz", hash = "sha256:e644fdec12f7872f86c58ff790da456218b10f863970249516d60a5eaca77206"},
+]
+pygments = [
+    {file = "Pygments-2.11.2-py3-none-any.whl", hash = "sha256:44238f1b60a76d78fc8ca0528ee429702aae011c265fe6a8dd8b63049ae41c65"},
+    {file = "Pygments-2.11.2.tar.gz", hash = "sha256:4e426f72023d88d03b2fa258de560726ce890ff3b630f88c21cbb8b2503b8c6a"},
+]
+pyparsing = [
+    {file = "pyparsing-3.0.7-py3-none-any.whl", hash = "sha256:a6c06a88f252e6c322f65faf8f418b16213b51bdfaece0524c1c1bc30c63c484"},
+    {file = "pyparsing-3.0.7.tar.gz", hash = "sha256:18ee9022775d270c55187733956460083db60b37d0d0fb357445f3094eed3eea"},
+]
+pysocks = [
+    {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"},
+    {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"},
+    {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"},
+]
+pytest = [
+    {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"},
+    {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"},
+]
+pytest-cov = [
+    {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"},
+    {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"},
+]
+pytest-mock = [
+    {file = "pytest-mock-3.7.0.tar.gz", hash = "sha256:5112bd92cc9f186ee96e1a92efc84969ea494939c3aead39c50f421c4cc69534"},
+    {file = "pytest_mock-3.7.0-py3-none-any.whl", hash = "sha256:6cff27cec936bf81dc5ee87f07132b807bcda51106b5ec4b90a04331cba76231"},
+]
+pywin32-ctypes = [
+    {file = "pywin32-ctypes-0.2.0.tar.gz", hash = "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942"},
+    {file = "pywin32_ctypes-0.2.0-py2.py3-none-any.whl", hash = "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"},
+]
+readme-renderer = [
+    {file = "readme_renderer-34.0-py3-none-any.whl", hash = "sha256:262510fe6aae81ed4e94d8b169077f325614c0b1a45916a80442c6576264a9c2"},
+    {file = "readme_renderer-34.0.tar.gz", hash = "sha256:dfb4d17f21706d145f7473e0b61ca245ba58e810cf9b2209a48239677f82e5b0"},
+]
+requests = [
+    {file = "requests-2.27.1-py2.py3-none-any.whl", hash = "sha256:f22fa1e554c9ddfd16e6e41ac79759e17be9e492b3587efa038054674760e72d"},
+    {file = "requests-2.27.1.tar.gz", hash = "sha256:68d7c56fd5a8999887728ef304a6d12edc7be74f1cfa47714fc8b414525c9a61"},
+]
+requests-mock = [
+    {file = "requests-mock-1.9.3.tar.gz", hash = "sha256:8d72abe54546c1fc9696fa1516672f1031d72a55a1d66c85184f972a24ba0eba"},
+    {file = "requests_mock-1.9.3-py2.py3-none-any.whl", hash = "sha256:0a2d38a117c08bb78939ec163522976ad59a6b7fdd82b709e23bb98004a44970"},
+]
+requests-toolbelt = [
+    {file = "requests-toolbelt-0.9.1.tar.gz", hash = "sha256:968089d4584ad4ad7c171454f0a5c6dac23971e9472521ea3b6d49d610aa6fc0"},
+    {file = "requests_toolbelt-0.9.1-py2.py3-none-any.whl", hash = "sha256:380606e1d10dc85c3bd47bf5a6095f815ec007be7a8b69c878507068df059e6f"},
+]
+rfc3986 = [
+    {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"},
+    {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"},
+]
+secretstorage = [
+    {file = "SecretStorage-3.3.1-py3-none-any.whl", hash = "sha256:422d82c36172d88d6a0ed5afdec956514b189ddbfb72fefab0c8a1cee4eaf71f"},
+    {file = "SecretStorage-3.3.1.tar.gz", hash = "sha256:fd666c51a6bf200643495a04abb261f83229dcb6fd8472ec393df7ffc8b6f195"},
+]
+six = [
+    {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+    {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+toml = [
+    {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"},
+    {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"},
+]
+tomli = [
+    {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"},
+    {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"},
+]
+tqdm = [
+    {file = "tqdm-4.63.1-py2.py3-none-any.whl", hash = "sha256:6461b009d6792008d0000e1b0c7ca50195ec78c0e808a3a6b668a56a3236c3a5"},
+    {file = "tqdm-4.63.1.tar.gz", hash = "sha256:4230a49119a416c88cc47d0d2d32d5d90f1a282d5e497d49801950704e49863d"},
+]
+twine = [
+    {file = "twine-3.8.0-py3-none-any.whl", hash = "sha256:d0550fca9dc19f3d5e8eadfce0c227294df0a2a951251a4385797c8a6198b7c8"},
+    {file = "twine-3.8.0.tar.gz", hash = "sha256:8efa52658e0ae770686a13b675569328f1fba9837e5de1867bfe5f46a9aefe19"},
+]
+typed-ast = [
+    {file = "typed_ast-1.5.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:183b183b7771a508395d2cbffd6db67d6ad52958a5fdc99f450d954003900266"},
+    {file = "typed_ast-1.5.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:676d051b1da67a852c0447621fdd11c4e104827417bf216092ec3e286f7da596"},
+    {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc2542e83ac8399752bc16e0b35e038bdb659ba237f4222616b4e83fb9654985"},
+    {file = "typed_ast-1.5.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:74cac86cc586db8dfda0ce65d8bcd2bf17b58668dfcc3652762f3ef0e6677e76"},
+    {file = "typed_ast-1.5.2-cp310-cp310-win_amd64.whl", hash = "sha256:18fe320f354d6f9ad3147859b6e16649a0781425268c4dde596093177660e71a"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:31d8c6b2df19a777bc8826770b872a45a1f30cfefcfd729491baa5237faae837"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:963a0ccc9a4188524e6e6d39b12c9ca24cc2d45a71cfdd04a26d883c922b4b78"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:0eb77764ea470f14fcbb89d51bc6bbf5e7623446ac4ed06cbd9ca9495b62e36e"},
+    {file = "typed_ast-1.5.2-cp36-cp36m-win_amd64.whl", hash = "sha256:294a6903a4d087db805a7656989f613371915fc45c8cc0ddc5c5a0a8ad9bea4d"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:26a432dc219c6b6f38be20a958cbe1abffcc5492821d7e27f08606ef99e0dffd"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c7407cfcad702f0b6c0e0f3e7ab876cd1d2c13b14ce770e412c0c4b9728a0f88"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f30ddd110634c2d7534b2d4e0e22967e88366b0d356b24de87419cc4410c41b7"},
+    {file = "typed_ast-1.5.2-cp37-cp37m-win_amd64.whl", hash = "sha256:8c08d6625bb258179b6e512f55ad20f9dfef019bbfbe3095247401e053a3ea30"},
+    {file = "typed_ast-1.5.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:90904d889ab8e81a956f2c0935a523cc4e077c7847a836abee832f868d5c26a4"},
+    {file = "typed_ast-1.5.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:bbebc31bf11762b63bf61aaae232becb41c5bf6b3461b80a4df7e791fabb3aca"},
+    {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c29dd9a3a9d259c9fa19d19738d021632d673f6ed9b35a739f48e5f807f264fb"},
+    {file = "typed_ast-1.5.2-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:58ae097a325e9bb7a684572d20eb3e1809802c5c9ec7108e85da1eb6c1a3331b"},
+    {file = "typed_ast-1.5.2-cp38-cp38-win_amd64.whl", hash = "sha256:da0a98d458010bf4fe535f2d1e367a2e2060e105978873c04c04212fb20543f7"},
+    {file = "typed_ast-1.5.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:33b4a19ddc9fc551ebabca9765d54d04600c4a50eda13893dadf67ed81d9a098"},
+    {file = "typed_ast-1.5.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1098df9a0592dd4c8c0ccfc2e98931278a6c6c53cb3a3e2cf7e9ee3b06153344"},
+    {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42c47c3b43fe3a39ddf8de1d40dbbfca60ac8530a36c9b198ea5b9efac75c09e"},
+    {file = "typed_ast-1.5.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f290617f74a610849bd8f5514e34ae3d09eafd521dceaa6cf68b3f4414266d4e"},
+    {file = "typed_ast-1.5.2-cp39-cp39-win_amd64.whl", hash = "sha256:df05aa5b241e2e8045f5f4367a9f6187b09c4cdf8578bb219861c4e27c443db5"},
+    {file = "typed_ast-1.5.2.tar.gz", hash = "sha256:525a2d4088e70a9f75b08b3f87a51acc9cde640e19cc523c7e41aa355564ae27"},
+]
+typing-extensions = [
+    {file = "typing_extensions-4.1.1-py3-none-any.whl", hash = "sha256:21c85e0fe4b9a155d0799430b0ad741cdce7e359660ccbd8b530613e8df88ce2"},
+    {file = "typing_extensions-4.1.1.tar.gz", hash = "sha256:1a9462dcc3347a79b1f1c0271fbe79e844580bb598bafa1ed208b94da3cdcd42"},
+]
+urllib3 = [
+    {file = "urllib3-1.26.9-py2.py3-none-any.whl", hash = "sha256:44ece4d53fb1706f667c9bd1c648f5469a2ec925fcf3a776667042d645472c14"},
+    {file = "urllib3-1.26.9.tar.gz", hash = "sha256:aabaf16477806a5e1dd19aa41f8c2b7950dd3c746362d7e3223dbe6de6ac448e"},
+]
+webencodings = [
+    {file = "webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78"},
+    {file = "webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923"},
+]
+zipp = [
+    {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"},
+    {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"},
+]
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..7f9d305
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,43 @@
+[tool.poetry]
+name = "virustotal-python"
+version = "1.0.0"
+description = "A Python library to interact with the public VirusTotal v3 and v2 APIs."
+authors = ["dbrennand"]
+license = "MIT"
+readme = "README.md"
+homepage = "https://github.com/dbrennand/virustotal-python"
+repository = "https://github.com/dbrennand/virustotal-python"
+keywords = [
+    "VirusTotal",
+    "Wrapper",
+    "Public API",
+    "Library",
+    "v3",
+    "v2"
+]
+classifiers = [
+    "Programming Language :: Python :: 3",
+    "Programming Language :: Python :: 3.7",
+    "Programming Language :: Python :: 3.8",
+    "Programming Language :: Python :: 3.9",
+]
+exclude = [".gitignore"]
+
+[tool.poetry.dependencies]
+python = "^3.7"
+requests = {version = "^2.26.0", extras = ["socks"]}
+
+[tool.poetry.extras]
+socks = ["PySocks^1.7.1"]
+
+[tool.poetry.dev-dependencies]
+black = "22.3.0"
+twine = "^3.5.0"
+pytest = "^6.2.5"
+pytest-mock = "^3.6.1"
+requests-mock = "^1.9.3"
+pytest-cov = "^3.0.0"
+
+[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
diff --git a/requirements.txt b/requirements.txt
deleted file mode 100644
index dd64c99..0000000
Binary files a/requirements.txt and /dev/null differ
diff --git a/setup.py b/setup.py
deleted file mode 100644
index d05cfa4..0000000
--- a/setup.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from setuptools import setup, find_packages
-
-with open("README.md", "r", encoding="utf8") as readme:
-    long_description = readme.read()
-
-setup(
-    name="virustotal-python",
-    version="0.2.0",
-    author="dbrennand",
-    description="A Python library to interact with the public VirusTotal v2 and v3 APIs.",
-    long_description=long_description,
-    long_description_content_type="text/markdown",
-    url="https://github.com/dbrennand/virustotal-python",
-    packages=find_packages(),
-    classifiers=[
-        "License :: OSI Approved :: MIT License",
-        "Programming Language :: Python :: 3",
-        "Programming Language :: Python :: 3.7",
-        "Programming Language :: Python :: 3.8",
-        "Programming Language :: Python :: 3.9",
-    ],
-    keywords="VirusTotal Wrapper Public API Library v2 v3",
-)
diff --git a/virustotal_python/__init__.py b/virustotal_python/__init__.py
index 32823b0..0ce3e5d 100644
--- a/virustotal_python/__init__.py
+++ b/virustotal_python/__init__.py
@@ -1,3 +1,11 @@
+"""A Python library to interact with the public VirusTotal v3 and v2 APIs.
+"""
 from virustotal_python.virustotal import Virustotal
+from virustotal_python.virustotal import VirustotalResponse
 from virustotal_python.virustotal import VirustotalError
+
 name = "virustotal-python"
+
+__all__ = ["Virustotal", "VirustotalResponse", "VirustotalError"]
+__author__ = "dbrennand"
+__version__ = "1.0.0"
diff --git a/virustotal_python/virustotal.py b/virustotal_python/virustotal.py
index 0ba9bc4..26dd242 100644
--- a/virustotal_python/virustotal.py
+++ b/virustotal_python/virustotal.py
@@ -23,41 +23,46 @@
 """
 import requests
 import os
-from typing import Tuple
+from typing import Union
 from json.decoder import JSONDecodeError
 
 
 class VirustotalError(Exception):
-    """
-    Class for VirusTotal API errors.
-    """
+    """Class for VirusTotal API errors."""
 
-    def __init__(self, response: requests.Response):
-        """
-        Initalisation for VirustotalError class.
+    def __init__(self, response: requests.Response) -> None:
+        """Initialisation for `VirustotalError`.
 
-        :param response: A requests.Response object from a failed API request to the VirusTotal API.
+        Args:
+            response (requests.Response): From a failed API request to the VirusTotal API.
         """
         self.response = response
 
-    def __str__(self):
-        return f"Error {self.error().get('code', 'unknown')} ({self.response.status_code}): {self.error().get('message', 'No message')}"
+    def __str__(self) -> str:
+        """String dunder method for `VirustotalError`.
 
-    def error(self) -> dict:
+        Returns:
+            str: Containing the error code, HTTP status code and error message from a failed VirusTotal API request.
         """
-        Retrieve the error that occurred from a VirusTotal API request.
+        error = self.error()
+        return f"Error {error.get('code', 'Unknown')} ({self.response.status_code}): {error.get('message', 'No message')}"
 
-        [v3 documentation](https://developers.virustotal.com/v3.0/reference#errors)
+    def error(self) -> dict:
+        """Retrieve the error that occurred from a failed VirusTotal API request.
 
-        [v2 documentation](https://developers.virustotal.com/reference#api-responses)
+        https://developers.virustotal.com/v2.0/reference/api-responses
 
-        :returns: A dictionary containing the error code and message returned from the VirusTotal API (if any) otherwise, returns an empty dictionary.
+        https://developers.virustotal.com/reference/errors
+
+        Returns:
+            dict: Containing the error code and message returned from the VirusTotal API (if any) otherwise, returns an empty dictionary.
         """
         # Attempt to decode JSON as the v3 VirusTotal API returns the error message as JSON
+        # Fallback to an empty dict if error is somehow missing
         try:
             return self.response.json().get("error", dict())
         except ValueError:
-            # Catch exception if there is no JSON to be deserialized
+            # Catch ValueError if JSON fails to be deserialized or there is no JSON
             # Most likely using the v2 VirusTotal API
             # Check there is response text, if not, return an empty dict
             if self.response.text:
@@ -67,148 +72,148 @@ def error(self) -> dict:
 
 
 class VirustotalResponse(object):
-    """
-    Response class for VirusTotal API requests.
-    """
+    """Response class for VirusTotal API requests."""
 
     def __init__(self, response: requests.Response):
-        """
-        Initalisation for VirustotalResponse class.
+        """Initialisation for `VirustotalResponse`.
 
-        :param response: A requests.Response object from a successfull API request to the VirusTotal API.
+        Args:
+            response (requests.Response): From a successful API request to the VirusTotal API.
         """
         self.response = response
 
     @property
     def headers(self) -> dict:
-        """
-        Retrieve the HTTP headers of a VirusTotal API request.
+        """Retrieve the HTTP headers of a VirusTotal API request.
 
-        :returns: The HTTP headers of the requests.Response object.
+        Returns:
+            dict: The HTTP headers of the requests.Response object.
         """
         return self.response.headers
 
     @property
     def status_code(self) -> int:
-        """
-        Retrieve the HTTP status code of a VirusTotal API request.
+        """Retrieve the HTTP status code of a VirusTotal API request.
 
-        :returns: The HTTP status code of the requests.Response object.
+        Returns:
+            int: The HTTP status code of the requests.Response object.
         """
         return self.response.status_code
 
     @property
     def text(self) -> str:
-        """
-        Retrieve the HTTP text response of a VirusTotal API request.
+        """Retrieve the HTTP text response of a VirusTotal API request.
 
-        :returns: The HTTP text response of the requests.Response object.
+        Returns:
+            str: The HTTP text response of the requests.Response object.
         """
         return self.response.text
 
     @property
     def requests_response(self) -> requests.Response:
-        """
-        Retrieve the HTTP requests.Response object of a VirusTotal API request.
+        """Retrieve the `requests.Response` object of a VirusTotal API request.
         You may want to access this property if you wanted to read other aspects of the response such as cookies.
 
-        :returns: A requests.Response object.
+        Returns:
+            requests.Response: From a successful API request to the VirusTotal API.
         """
         return self.response
 
     @property
-    def links(self) -> Tuple[dict, None]:
-        """
-        Retrieve the value of the key 'links' in the JSON response from a VirusTotal API request.
+    def links(self) -> Union[dict, None]:
+        """Retrieve the value of the key 'links' in the JSON response from a VirusTotal API request.
 
-        NOTE: Links are not retrieved for objects inside 'data'.
+        https://developers.virustotal.com/reference/collections
 
-        [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+        NOTE: Links are not retrieved for objects inside 'data'.
 
-        :returns: A dictionary containing the links used to retrieve the next set of objects (if any), otherwise, returns None.
+        Returns:
+            Union[dict, None]: The links used to retrieve the next set of objects (if any), otherwise, returns `None`.
         """
         return self.json().get("links", None)
 
     @property
-    def meta(self) -> Tuple[dict, None]:
-        """
-        Retrieve the value of the key 'meta' in the JSON response from a VirusTotal API request.
+    def meta(self) -> Union[dict, None]:
+        """Retrieve the value of the key 'meta' in the JSON response from a VirusTotal API request.
 
-        [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+        https://developers.virustotal.com/reference/collections
 
-        :returns: A dictionary containing metadata about the object(s) (if any), otherwise, returns None.
+        Returns:
+            Union[dict, None]: The metadata about the object(s) (if any), otherwise, returns `None`.
         """
         return self.json().get("meta", None)
 
     @property
-    def cursor(self) -> Tuple[str, None]:
-        """
-        Retrieve the value of the key 'cursor' in the JSON response value 'meta' from a VirusTotal API request.
+    def cursor(self) -> Union[str, None]:
+        """Retrieve the value of the key 'cursor' in the JSON response value 'meta' from a VirusTotal API request.
 
-        [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+        https://developers.virustotal.com/reference/collections
 
-        :returns: A string representing the cursor used to retrieve additional related object(s), otherwise, returns None.
+        Returns:
+            Union[str, None]: The cursor used to retrieve related object(s), otherwise, returns `None`.
         """
         try:
             return self.meta.get("cursor", None)
-        # Catch AttributeError that occurs when attemping to call attribute 'get' on None
-        # which is returned if the 'meta' key is not present in the JSON response
+        # Catch AttributeError that occurs when attempting to call attribute 'get' on None
+        # which is raised if the 'meta' key is not present in the JSON response
         except AttributeError:
             return None
 
     @property
-    def data(self) -> Tuple[dict, list, None]:
-        """
-        Retrieve the value of the key 'data' in the JSON response from a VirusTotal API request.
+    def data(self) -> Union[dict, list, str, None]:
+        """Retrieve the value of the key 'data' in the JSON response from a VirusTotal API request.
 
-        [v3 documentation](https://developers.virustotal.com/v3.0/reference#objects)
+        https://developers.virustotal.com/reference/objects
 
-        :returns: A dictionary or list depending on the number of objects returned from the VirusTotal API (if any) otherwise, returns None.
+        Returns:
+            Union[dict, list, str, None]: The data from a VirusTotal API request (if any) otherwise, returns `None`.
         """
         return self.json().get("data", None)
 
     @property
-    def object_type(self) -> Tuple[list, str, None]:
-        """
-        Retrieve the object type(s) in the JSON response from a VirusTotal API request.
+    def object_type(self) -> Union[list, str, None]:
+        """Retrieve the object type(s) in the JSON response from a VirusTotal API request.
 
-        [v3 documentation](https://developers.virustotal.com/v3.0/reference#objects)
+        https://developers.virustotal.com/reference/objects
 
-        [More v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+        https://developers.virustotal.com/reference/collections
 
-        :returns: A list or string depending on the number of objects returned from the VirusTotal API (if any) otherwise, returns None.
+        Returns:
+            Union[list, str, None]: A list or string depending on the number of objects returned (if any) otherwise, returns `None`.
         """
         data = self.data
-        # Check if data is more than one object
+        # Check if data contains more than one object
         if isinstance(data, list):
             object_list = []
             for data_object in data:
                 data_object_type = data_object.get("type", None)
                 object_list.append(data_object_type)
             return object_list
+        # Data contains only one object
         elif isinstance(data, dict):
             return data.get("type", None)
         else:
             return None
 
     @property
-    def response_code(self) -> Tuple[int, None]:
-        """
-        Retrieve the value of the key 'response_code' in the JSON response from a VirusTotal v2 API request.
+    def response_code(self) -> Union[int, None]:
+        """Retrieve the value of the key 'response_code' in the JSON response from a VirusTotal v2 API request.
 
-        [v2 documentation](https://developers.virustotal.com/reference#api-responses)
+        https://developers.virustotal.com/v2.0/reference/api-responses
 
-        :returns: An int of the response_code from the VirusTotal API (if any), otherwise, returns None.
+        Returns:
+            Union[int, None]: The response_code from the VirusTotal API (if any), otherwise, returns `None`.
         """
         return self.json().get("response_code", None)
 
-    def json(self, **kwargs) -> Tuple[dict, list]:
-        """
-        Retrieve the JSON response of a VirusTotal API request.
+    def json(self, **kwargs) -> dict:
+        """Retrieve the JSON response from a VirusTotal API request.
+
+        Args:
+            **kwargs: Identical to `json.loads(**kwargs)`.
 
-        :param **kwargs: Parameters to pass to json. Identical to `json.loads(**kwargs)`.
-        :returns: JSON response of the requests.Response object.
-        :raises ValueError: Raises ValueError when the response body contains invalid JSON.
+        Returns:
+            dict: JSON response from a VirusTotal API request.
         """
         try:
             return self.response.json(**kwargs)
@@ -217,73 +222,72 @@ def json(self, **kwargs) -> Tuple[dict, list]:
 
 
 class Virustotal(object):
-    """
-    Interact with the public VirusTotal v2 and v3 APIs.
+    """Interact with the public VirusTotal v3 and v2 APIs.
 
-    [v2 documentation](https://www.virustotal.com/en/documentation/public-api/)
+    https://developers.virustotal.com/v3.0/reference
 
-    [v3 documentation](https://developers.virustotal.com/v3.0/reference)
+    https://developers.virustotal.com/v2.0/reference
     """
 
     def __init__(
         self,
-        API_KEY: str = os.environ.get("VIRUSTOTAL_API_KEY", None),
-        API_VERSION: str = "v2",
-        COMPATIBILITY_ENABLED: bool = False,
+        API_KEY: str = None,
+        API_VERSION: Union[int, str] = 3,
         PROXIES: dict = None,
         TIMEOUT: float = None,
     ):
+        """Interact with the public VirusTotal v3 and v2 APIs.
+
+        Args:
+            API_KEY (str, optional): The API key used to interact with the VirusTotal v3 and v2 APIs.
+                Alternatively, the environment variable `VIRUSTOTAL_API_KEY` can be provided.
+            API_VERSION (Union[int, str], optional): The version to use when interacting with the VirusTotal API.
+                Defaults to 3.
+            PROXIES (dict, optional): Proxies to use when making requests.
+                E.g. `{"http": "http://10.10.1.10:3128", "https": "https://10.10.1.10:1080"}`
+                Defaults to `None`.
+            TIMEOUT (float, optional): The amount of time (in seconds) to wait for the HTTP request before timing out.
+                Defaults to `None`.
+
+        Raises:
+            ValueError: When no `API_KEY` is provided or the `API_VERSION` is invalid.
         """
-        Initalisation function for the Virustotal class.
-
-        :param API_KEY: The API key used to interact with the VirusTotal v2 and v3 APIs. Alternatively, the environment variable `VIRUSTOTAL_API_KEY` can be provided.
-        :param API_VERSION: The version to use when interacting with the VirusTotal API. This parameter defaults to 'v2' for backwards compatibility.
-        :param COMPATIBILITY_ENABLED: Preserve the old response format of virustotal-python versions prior to 0.1.0 for backwards compatibility.
-        :param PROXIES: A dictionary containing proxies used when making requests.
-        :param TIMEOUT: A float for the amount of time to wait in seconds for the HTTP request before timing out.
-        :raises ValueError: Raises ValueError when no API_KEY is provided or the API_VERSION is invalid.
-        """
-        self.VERSION = "0.2.0"
         if API_KEY is None:
-            raise ValueError(
-                "An API key is required to interact with the VirusTotal API.\nProvide one to the API_KEY parameter or by setting the environment variable 'VIRUSTOTAL_API_KEY'."
-            )
+            API_KEY = os.environ.get("VIRUSTOTAL_API_KEY", None)
+            if API_KEY is None:
+                raise ValueError(
+                    "An API key is required to interact with the VirusTotal API.\nProvide one to the API_KEY parameter or by setting the environment variable 'VIRUSTOTAL_API_KEY'."
+                )
         self.API_KEY = API_KEY
-        self.COMPATIBILITY_ENABLED = COMPATIBILITY_ENABLED
         self.PROXIES = PROXIES
         self.TIMEOUT = TIMEOUT
         # Declare appropriate variables depending on the API_VERSION provided
-        if API_VERSION == "v2":
+        if (API_VERSION == "v2") or (API_VERSION == 2):
             self.API_VERSION = API_VERSION
             self.BASEURL = "https://www.virustotal.com/vtapi/v2/"
             self.HEADERS = {
                 "Accept-Encoding": "gzip, deflate",
-                "User-Agent": f"gzip,  virustotal-python {self.VERSION}",
+                "User-Agent": f"gzip, virustotal-python 1.0.0",
             }
-        elif API_VERSION == "v3":
+        elif (API_VERSION == "v3") or (API_VERSION == 3):
             self.API_VERSION = API_VERSION
             self.BASEURL = "https://www.virustotal.com/api/v3/"
             self.HEADERS = {
                 "Accept-Encoding": "gzip, deflate",
-                "User-Agent": f"gzip, virustotal-python {self.VERSION}",
+                "User-Agent": f"gzip, virustotal-python 1.0.0",
                 "x-apikey": f"{self.API_KEY}",
             }
         else:
             raise ValueError(
-                f"The API version '{API_VERSION}' is not a valid VirusTotal API version.\nValid API versions are 'v2' or 'v3'."
+                f"The API version '{API_VERSION}' is not a valid VirusTotal API version.\nValid API versions are 'v2', 2, 'v3' and 3."
             )
 
-    # Context Manager support
     def __enter__(self):
-        """
-        Context Manager enter function.
-        """
+        """Context Manager enter function."""
         return self
 
     def __exit__(self, type, value, traceback):
-        """
-        Context Manager exit function.
-        """
+        """Context Manager exit function."""
         return
 
     def request(
@@ -295,28 +299,31 @@ def request(
         files: dict = None,
         method: str = "GET",
         large_file: bool = False,
-    ) -> Tuple[dict, VirustotalResponse]:
-        """
-        Make a request to the VirusTotal API.
-
-        :param resource: A valid VirusTotal API endpoint. (E.g. 'files/{id}')
-        :param params: A dictionary containing API endpoint query parameters.
-        :param data: A dictionary containing the data to send in the body of the request.
-        :param json: A dictionary containing the JSON payload to send with the request.
-        :param files: A dictionary containing the file for multipart encoding upload. (E.g: {'file': ('filename', open('filename.txt', 'rb'))})
-        :param method: The request method to use.
-        :param large_file: If a file is larger than 32MB, a custom generated upload URL is required.
-            If this param is set to `True`, this URL can be set via the resource param.
-        :returns: A dictionary containing the HTTP response code (resp_code) and JSON response (json_resp) if self.COMPATIBILITY_ENABLED is True.
-            Otherwise, a VirustotalResponse class object is returned. If a HTTP status not equal to 200 occurs. Then a VirustotalError class object is returned.
-        :raises Exception: Raise Exception when an unsupported method is provided.
+    ) -> VirustotalResponse:
+        """Make a request to the VirusTotal API.
+
+        Args:
+            resource (str): A valid VirusTotal API endpoint. E.g. `f'files/{id}'`.
+            params (dict, optional): API endpoint query parameters. Defaults to `{}`.
+            data (dict, optional): Data to send in the body of the request. Defaults to `None`.
+            json (dict, optional): JSON payload to send with the request. Defaults to `None`.
+            files (dict, optional): File(s) for multipart encoding upload. Defaults to `None`.
+            method (str, optional): The HTTP request method to use. Defaults to `"GET"`.
+            large_file (bool, optional): If a file is larger than 32MB, a custom generated upload URL is required.
+                If this param is set to `True`, this URL can be set via the resource param. Defaults to `False`.
+
+        Raises:
+            NotImplementedError: When a unsupported HTTP method is provided.
+            VirustotalError: When a failed VirusTotal API response occurs.
+
+        Returns:
+            VirustotalResponse: A `VirustotalResponse` class object.
         """
-        # Create API endpoint
         endpoint = f"{self.BASEURL}{resource}"
         if large_file:
             endpoint = resource
-        # If API version being used is v2, add the API key to params
-        if self.API_VERSION == "v2":
+        # VirusTotal API version 2 requires API_KEY to be in the request parameters
+        if (self.API_VERSION == "v2") or (self.API_VERSION == 2):
             params["apikey"] = self.API_KEY
         if method == "GET":
             response = requests.get(
@@ -363,40 +370,10 @@ def request(
                 timeout=self.TIMEOUT,
             )
         else:
-            raise Exception(f"The request method '{method}' is not supported.")
-        # Validate response and return it
-        return self.validate_response(response)
-
-    def validate_response(
-        self, response: requests.Response
-    ) -> Tuple[dict, VirustotalResponse]:
-        """
-        Helper function to validate the request response.
-
-        :param response: A requests.Response object for an API request made to the VirusTotal API.
-        :returns: A dictionary containing the HTTP response code (resp_code) and JSON response (json_resp) if self.COMPATIBILITY_ENABLED is True otherwise, a VirustotalResponse class object is returned.
-        :raises VirustotalError: Raises VirustotalError when an HTTP status code other than 200 (successfull) occurs.
-        """
-        if self.COMPATIBILITY_ENABLED:
-            if response.status_code == 200:
-                json_resp = response.json()
-                return dict(status_code=response.status_code, json_resp=json_resp)
-            else:
-                # An error has occurred
-                # The v3 API returns the error as JSON, attempt to retrieve it
-                try:
-                    error_json = response.json()
-                except ValueError:
-                    # API version being used is likely to be v2. Catch the raised ValueError and continue
-                    pass
-                return dict(
-                    status_code=response.status_code,
-                    # Provide JSON error message if retrieved successfully, otherwise fallback on response.text
-                    error=(error_json if error_json else response.text),
-                    resp=response.content,
-                )
+            raise NotImplementedError(
+                f"The request method '{method}' is not implemented."
+            )
+        if response.status_code != 200:
+            raise VirustotalError(response)
         else:
-            if response.status_code != 200:
-                raise VirustotalError(response)
-            else:
-                return VirustotalResponse(response)
+            return VirustotalResponse(response)


Test patch:
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/example.json b/tests/example.json
new file mode 100644
index 0000000..5fb4f59
--- /dev/null
+++ b/tests/example.json
@@ -0,0 +1,204 @@
+{
+    "response_code": 1,
+    "meta": {
+        "count": 37,
+        "cursor": "CtIBChEKBGRhdGUSCQiv86vhnf_sAhK4AWoRc352aXJ1c3RvdGFsY2xvdWRyogELEgZTYW1wbGUiQDlmMTAxNDgzNjYyZmMwNzFiN2MxMGY4MWM2NGJiMzQ0OTFjYTRhODc3MTkxZDQ2NGZmNDZmZDk0YzcyNDcxMTUMCxIHQ29tbWVudCJJOWYxMDE0ODM2NjJmYzA3MWI3YzEwZjgxYzY0YmIzNDQ5MWNhNGE4NzcxOTFkNDY0ZmY0NmZkOTRjNzI0NzExNS01NTM3YjRjOAwYACAB"
+    },
+    "data": [
+        {
+            "attributes": {
+                "date": 1632291032,
+                "text": "Watchout! This looks dangerous!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Watchout! This looks dangerous!",
+                "tags": []
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-b32ca808",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-b32ca808"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1632291027,
+                "text": "Wow, this looks like a #malicious file!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Wow, this looks like a #malicious file!",
+                "tags": [
+                    "malicious"
+                ]
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-55f30362",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-55f30362"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1631698602,
+                "text": "Watchout! This looks dangerous!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Watchout! This looks dangerous!",
+                "tags": []
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c8f29681",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c8f29681"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1628487995,
+                "text": "Watchout! This looks dangerous!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Watchout! This looks dangerous!",
+                "tags": []
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-f868ec62",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-f868ec62"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1628487990,
+                "text": "Wow, this looks like a #malicious file!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Wow, this looks like a #malicious file!",
+                "tags": [
+                    "malicious"
+                ]
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-75d8015c",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-75d8015c"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1624463998,
+                "text": "Watchout! This looks dangerous!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Watchout! This looks dangerous!",
+                "tags": []
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c92f1477",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c92f1477"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1624463994,
+                "text": "Wow, this looks like a #malicious file!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "Wow, this looks like a #malicious file!",
+                "tags": [
+                    "malicious"
+                ]
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-1499e7d7",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-1499e7d7"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1620216813,
+                "text": "YARA Signature Match - THOR APT Scanner\n\nRULE: SUSP_Encoded_EnvUserProfile\nRULE_SET: Livehunt - Suspicious1 Indicators\nRULE_TYPE: Valhalla Rule Feed Only\nDESCRIPTION: Detects encoded keyword - $env:userprofile\nRULE_AUTHOR: Florian Roth\n\nDetection Timestamp: 2021-05-05 12:17\nAV Detection Ratio: 38 / 60\n\nUse these tags to search for similar matches: #livehunt-suspicious1indicators #susp_encoded_envuserprofile\nMore information: https://www.nextron-systems.com/notes-on-virustotal-matches/\n",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "YARA Signature Match - THOR APT Scanner<br /><br />RULE: SUSP_Encoded_EnvUserProfile<br />RULE_SET: Livehunt - Suspicious1 Indicators<br />RULE_TYPE: Valhalla Rule Feed Only<br />DESCRIPTION: Detects encoded keyword - $env:userprofile<br />RULE_AUTHOR: Florian Roth<br /><br />Detection Timestamp: 2021-05-05 12:17<br />AV Detection Ratio: 38 / 60<br /><br />Use these tags to search for similar matches: #livehunt-suspicious1indicators #susp_encoded_envuserprofile<br />More information: https://www.nextron-systems.com/notes-on-virustotal-matches/<br />",
+                "tags": [
+                    "livehunt-suspicious1indicators",
+                    "susp_encoded_envuserprofile"
+                ]
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-3b6e7a36",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-3b6e7a36"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1606741727,
+                "text": "This file looks much more malicious - c56d9d52e9318dbd341e72cc90002e48ade97352b013b5d2f28f9325a56564bd",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "This file looks much more malicious - c56d9d52e9318dbd341e72cc90002e48ade97352b013b5d2f28f9325a56564bd",
+                "tags": []
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-ad12bfc6",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-ad12bfc6"
+            }
+        },
+        {
+            "attributes": {
+                "date": 1605260605,
+                "text": "#watchout, this looks very malicious!",
+                "votes": {
+                    "positive": 0,
+                    "abuse": 0,
+                    "negative": 0
+                },
+                "html": "#watchout, this looks very malicious!",
+                "tags": [
+                    "watchout"
+                ]
+            },
+            "type": "comment",
+            "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-5537b4c8",
+            "links": {
+                "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-5537b4c8"
+            }
+        }
+    ],
+    "links": {
+        "self": "https://www.virustotal.com/api/v3/files/9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115/comments?limit=10",
+        "next": "https://www.virustotal.com/api/v3/files/9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115/comments?cursor=CtIBChEKBGRhdGUSCQiv86vhnf_sAhK4AWoRc352aXJ1c3RvdGFsY2xvdWRyogELEgZTYW1wbGUiQDlmMTAxNDgzNjYyZmMwNzFiN2MxMGY4MWM2NGJiMzQ0OTFjYTRhODc3MTkxZDQ2NGZmNDZmZDk0YzcyNDcxMTUMCxIHQ29tbWVudCJJOWYxMDE0ODM2NjJmYzA3MWI3YzEwZjgxYzY0YmIzNDQ5MWNhNGE4NzcxOTFkNDY0ZmY0NmZkOTRjNzI0NzExNS01NTM3YjRjOAwYACAB&limit=10"
+    }
+}
diff --git a/tests/test_virustotal_python.py b/tests/test_virustotal_python.py
new file mode 100644
index 0000000..97b7a61
--- /dev/null
+++ b/tests/test_virustotal_python.py
@@ -0,0 +1,374 @@
+"""Tests for virustotal-python.
+"""
+import virustotal_python
+import json
+import requests
+import pytest
+import pytest_mock
+import requests_mock as req_mock
+
+with open("tests/example.json") as json_file:
+    example_json = json.dumps(json.load(json_file))
+
+
+@pytest.fixture
+def mock_http_request(requests_mock: req_mock.Mocker) -> None:
+    """Fixture to mock HTTP requests made by the `Virustotal.request()` method.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        req_mock.ANY,
+        req_mock.ANY,
+        status_code=200,
+        text=example_json,
+        headers={"test": "test"},
+    )
+
+
+def test_virustotal_apikey_env(mocker: pytest_mock.MockerFixture) -> None:
+    """Test `Virustotal` environment variable `VIRUSTOTAL_API_KEY`.
+
+    Args:
+        mocker (pytest_mock.MockerFixture): A pytest_mock.MockerFixture providing a
+            thin-wrapper around the patching API from the mock library.
+    """
+    mocker.patch.dict("os.environ", dict(VIRUSTOTAL_API_KEY="API key"))
+    vtotal = virustotal_python.Virustotal()
+    assert vtotal.API_KEY == "API key"
+
+
+def test_virustotal(mocker: pytest_mock.MockerFixture) -> None:
+    """Test `Virustotal` parameters.
+
+    Args:
+        mocker (pytest_mock.MockerFixture): A pytest_mock.MockerFixture providing a
+            thin-wrapper around the patching API from the mock library.
+    """
+    keyword_arguments = {
+        "API_KEY": "test",
+        "API_VERSION": 3,
+        "PROXIES": {"http": "http://10.10.1.10:3128"},
+        "TIMEOUT": 5.0,
+    }
+    mock_vtotal = mocker.patch("virustotal_python.Virustotal")
+    virustotal_python.Virustotal(**keyword_arguments)
+    mock_vtotal.assert_called_with(**keyword_arguments)
+
+
+def test_virustotal_context_manager() -> None:
+    """Test `Virustotal` context manager support."""
+    with virustotal_python.Virustotal(API_KEY="test") as vtotal:
+        vtotal.__exit__("", "", "")
+
+
+def test_virustotal_api_key_value_error() -> None:
+    """Test `Virustotal` raises `ValueError` when no `API_KEY` is provided."""
+    with pytest.raises(ValueError) as execinfo:
+        virustotal_python.Virustotal()
+    assert (
+        "An API key is required to interact with the VirusTotal API.\nProvide one to the API_KEY parameter or by setting the environment variable 'VIRUSTOTAL_API_KEY'."
+        == str(execinfo.value)
+    )
+
+
+def test_virustotal_api_version_value_error() -> None:
+    """Test `Virustotal` raises `ValueError` when an unsupported `API_VERSION` is provided."""
+    with pytest.raises(ValueError) as execinfo:
+        virustotal_python.Virustotal(API_KEY="test", API_VERSION="test")
+    assert (
+        "The API version 'test' is not a valid VirusTotal API version.\nValid API versions are 'v2', 2, 'v3' and 3."
+        == str(execinfo.value)
+    )
+
+
+def test_request_notimplemented_error() -> None:
+    """Test `Virustotal.request()` raises `NotImplementedError` when an unsupported `method` is provided."""
+    with pytest.raises(NotImplementedError) as execinfo:
+        vtotal = virustotal_python.Virustotal(API_KEY="test")
+        vtotal.request("test", method="test")
+    assert "The request method 'test' is not implemented." == str(execinfo.value)
+
+
+def test_request_large_file(requests_mock: req_mock.Mocker) -> None:
+    """Test `Virustotal.request()` `large_file` parameter.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        "GET",
+        "https://www.virustotal.com/api/v3/files/upload_url",
+        status_code=200,
+        json={
+            "data": "http://www.virustotal.com/_ah/upload/AMmfu6b-_DXUeFe36Sb3b0F4B8mH9Nb-CHbRoUNVOPwG/"
+        },
+    )
+    requests_mock.register_uri(
+        "POST",
+        "http://www.virustotal.com/_ah/upload/AMmfu6b-_DXUeFe36Sb3b0F4B8mH9Nb-CHbRoUNVOPwG/",
+        status_code=200,
+        json={"data": {"type": "analysis", "id": "test=="}},
+    )
+    with virustotal_python.Virustotal(API_KEY="test", API_VERSION=3) as vtotal:
+        resp = vtotal.request("files/upload_url")
+        large_upload_url = resp.data
+        large_upload_resp = vtotal.request(
+            large_upload_url, method="POST", large_file=True
+        )
+    assert large_upload_resp.data == {"type": "analysis", "id": "test=="}
+
+
+def test_virustotal_response_headers(mock_http_request) -> None:
+    """Test `VirustotalResponse.headers` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.headers == {"test": "test"}
+
+
+def test_virustotal_response_status_code(mock_http_request) -> None:
+    """Test `VirustotalResponse.status_code` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.status_code == 200
+
+
+def test_virustotal_response_text(mock_http_request) -> None:
+    """Test `VirustotalResponse.text` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.text == example_json
+
+
+def test_virustotal_response_requests_response(mock_http_request) -> None:
+    """Test `VirustotalResponse.requests_response` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert type(resp.requests_response) == requests.Response
+
+
+def test_virustotal_response_links(mock_http_request) -> None:
+    """Test `VirustotalResponse.links` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.links == json.loads(example_json)["links"]
+
+
+def test_virustotal_response_meta(mock_http_request) -> None:
+    """Test `VirustotalResponse.meta` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.meta == json.loads(example_json)["meta"]
+
+
+def test_virustotal_response_cursor(mock_http_request) -> None:
+    """Test `VirustotalResponse.cursor` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.cursor == json.loads(example_json)["meta"]["cursor"]
+
+
+def test_virustotal_response_cursor_none(requests_mock: req_mock.Mocker) -> None:
+    """Test `VirustotalResponse.cursor` property returns `None` if `meta` key
+    is not present in the JSON response.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        req_mock.ANY,
+        req_mock.ANY,
+        status_code=200,
+    )
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.cursor == None
+
+
+def test_virustotal_response_data(mock_http_request) -> None:
+    """Test `VirustotalResponse.data` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.data == json.loads(example_json)["data"]
+
+
+def test_virustotal_response_object_type_list(mock_http_request) -> None:
+    """Test `VirustotalResponse.object_type` property returns a `list` of all the object types.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    obj_types = []
+    for comment in resp.data:
+        obj_types.append(comment["type"])
+    assert resp.object_type == obj_types
+
+
+def test_virustotal_response_object_type_str(requests_mock: req_mock.Mocker) -> None:
+    """Test `VirustotalResponse.object_type` property returns a `str` of a single
+    object type.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        req_mock.ANY,
+        req_mock.ANY,
+        status_code=200,
+        json={"data": {"type": "test type"}},
+    )
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.object_type == "test type"
+
+
+def test_virustotal_response_object_type_none(requests_mock: req_mock.Mocker) -> None:
+    """Test `VirustotalResponse.object_type` property returns `None` when the data property
+    is neither a `list` or `dict`.
+
+    Example endpoint: https://developers.virustotal.com/reference/files-upload-url
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        req_mock.ANY,
+        req_mock.ANY,
+        status_code=200,
+        json={
+            "data": "http://www.virustotal.com/_ah/upload/AMmfu6b-_DXUeFe36Sb3b0F4B8mH9Nb-CHbRoUNVOPwG/"
+        },
+    )
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.object_type == None
+
+
+def test_virustotal_response_response_code(mock_http_request) -> None:
+    """Test `VirustotalResponse.response_code` property.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.response_code == json.loads(example_json)["response_code"]
+
+
+def test_virustotal_response_json(mock_http_request) -> None:
+    """Test `VirustotalResponse.json()` method.
+
+    Args:
+        mock_http_request (`mock_http_request()`): A pytest fixture to
+            to mock HTTP requests made by the `Virustotal.request()` method.
+    """
+    with virustotal_python.Virustotal("test") as vtotal:
+        resp = vtotal.request("test")
+    assert resp.json() == json.loads(example_json)
+
+
+def test_virustotal_error(requests_mock: req_mock.Mocker) -> None:
+    """Test `VirustotalError.error()` method and string dunder.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        req_mock.ANY,
+        req_mock.ANY,
+        status_code=404,
+        json={
+            "error": {
+                "code": "NotFoundError",
+                "message": 'URL "thisurlidmakesnosenseatall" not found',
+            }
+        },
+    )
+    with pytest.raises(virustotal_python.VirustotalError) as execinfo:
+        vtotal = virustotal_python.Virustotal("test")
+        vtotal.request("test")
+    assert (
+        'Error NotFoundError (404): URL "thisurlidmakesnosenseatall" not found'
+        == str(execinfo.value)
+    )
+
+
+def test_virustotal_error_no_code_message(requests_mock: req_mock.Mocker) -> None:
+    """Test `VirustotalError.error()` method and string dunder with no code or message.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(req_mock.ANY, req_mock.ANY, status_code=400)
+    with pytest.raises(virustotal_python.VirustotalError) as execinfo:
+        vtotal = virustotal_python.Virustotal("test")
+        vtotal.request("test")
+    assert "Error Unknown (400): No message" == str(execinfo.value)
+
+
+def test_virustotal_error_text_only(requests_mock: req_mock.Mocker) -> None:
+    """Test `VirustotalError.error()` method and string dunder with `requests.Response.text` present.
+
+    Args:
+        requests_mock (req_mock.Mocker): A req_mock.Mocker providing a
+            thin-wrapper around patching the `requests` library.
+    """
+    requests_mock.register_uri(
+        req_mock.ANY, req_mock.ANY, status_code=400, text="Request failed"
+    )
+    with pytest.raises(virustotal_python.VirustotalError) as execinfo:
+        vtotal = virustotal_python.Virustotal("test")
+        vtotal.request("test")
+    assert "Error Unknown (400): Request failed" == str(execinfo.value)
diff --git a/virustotal_python/tests.py b/virustotal_python/tests.py
deleted file mode 100644
index d9c03f2..0000000
--- a/virustotal_python/tests.py
+++ /dev/null
@@ -1,380 +0,0 @@
-import virustotal_python
-import pytest
-import os.path
-import subprocess
-from time import sleep
-from base64 import urlsafe_b64encode
-
-# Declare variables for tests
-# Create dictionary containing the file to send for multipart encoding upload
-FILES = {
-    "file": (
-        os.path.basename("virustotal_python/oldexamples.py"),
-        open(os.path.abspath("virustotal_python/oldexamples.py"), "rb"),
-    )
-}
-# The ID (either SHA-256, SHA-1 or MD5) identifying the file
-FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
-# Example IP address (Google DNS)
-IP = "8.8.8.8"
-# Example ID of a graph
-## NOTE: There are no comments on this graph so an empty list is returned
-GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d"
-# URL/domain identifier
-URL_DOMAIN = "google.com"
-# Example comment ID
-COMMENT_ID = "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-07457619"
-
-
-@pytest.fixture()
-def large_file_fixture(request):
-    """Setup and teardown fixture for `test_large_file_v2` and `test_large_file_v3`."""
-    # Create a large file of 33MB to submit to the VirusTotal API for analysis
-    subprocess.run(
-        ["dd", "if=/dev/urandom", "of=dummy.dat", "bs=33M", "count=1"], check=True
-    )
-
-    def teardown():
-        """Delete the large file created by the fixture."""
-        subprocess.run(["rm", "dummy.dat"], check=True)
-
-    # Add finalizer function
-    request.addfinalizer(teardown)
-
-    return {
-        "file": (
-            os.path.basename("dummy.dat"),
-            open(os.path.abspath("dummy.dat"), "rb"),
-        )
-    }
-
-
-@pytest.fixture()
-def vtotal_v2(request):
-    yield virustotal_python.Virustotal()
-
-    def fin():
-        """
-        Helper function which sleeps for 15 seconds between each test. This is to avoid VirusTotal 403 rate quota limits.
-        """
-        print("Sleeping for 15 seconds to avoid VirusTotal 403 rate quota limits...")
-        sleep(15)
-
-    request.addfinalizer(fin)
-
-
-@pytest.fixture()
-def vtotal_v3(request):
-    yield virustotal_python.Virustotal(API_VERSION="v3")
-
-    def fin():
-        """
-        Helper function which sleeps for 15 seconds between each test. This is to avoid VirusTotal 403 rate quota limits.
-        """
-        print("Sleeping for 15 seconds to avoid VirusTotal 403 rate quota limits...")
-        sleep(15)
-
-    request.addfinalizer(fin)
-
-
-def test_file_scan_v2(vtotal_v2):
-    """
-    Test for sending a file to the VirusTotal v2 API for analysis.
-    """
-    resp = vtotal_v2.request("file/scan", files=FILES, method="POST")
-    data = resp.json()
-    assert resp.response_code == 1
-    assert data["scan_id"]
-    assert data["permalink"]
-
-
-def test_file_scan_v3(vtotal_v3):
-    """
-    Test for sending a file to the VirusTotal v3 API for analysis.
-    """
-    resp = vtotal_v3.request("files", files=FILES, method="POST")
-    assert resp.status_code == 200
-    data = resp.data
-    assert data["id"]
-    assert data["type"] == "analysis"
-
-
-def test_file_info_v2(vtotal_v2):
-    """
-    Test for retrieving information about a file from the VirusTotal v2 API.
-    """
-    resp = vtotal_v2.request("file/report", {"resource": FILE_ID})
-    assert resp.response_code == 1
-    assert resp.json()["scans"]
-
-
-def test_file_info_v3(vtotal_v3):
-    """
-    Test for retrieving information about a file from the VirusTotal v3 API.
-    """
-    resp = vtotal_v3.request(f"files/{FILE_ID}")
-    assert resp.status_code == 200
-    assert resp.object_type == "file"
-    assert resp.data["attributes"]
-    assert resp.data["attributes"]["last_analysis_results"]
-
-
-def test_compatibility():
-    """
-    Test COMPATIBILITY_ENABLED parameter on Virustotal class.
-    """
-    vtotal = virustotal_python.Virustotal(API_VERSION="v3", COMPATIBILITY_ENABLED=True)
-    resp = vtotal.request(f"files/{FILE_ID}")
-    assert resp["status_code"] == 200
-    assert resp["json_resp"]["data"]["type"] == "file"
-    assert resp["json_resp"]["data"]["attributes"]
-
-
-def test_scan_url_info_v2(vtotal_v2):
-    """
-    Test scanning URL and retrieving the scan results from the VirusTotal v2 API.
-    """
-    # Send the URLs to VirusTotal for analysis
-    resp = vtotal_v2.request("url/scan", params={"url": URL_DOMAIN}, method="POST")
-    assert resp.status_code == 200
-    data = resp.json()
-    # Obtain scan_id
-    scan_id = data["scan_id"]
-    # Request report for URL analysis
-    analysis_resp = vtotal_v2.request("url/report", params={"resource": scan_id})
-    assert analysis_resp.status_code == 200
-    assert analysis_resp.response_code == 1
-    data = analysis_resp.json()
-    assert data["scan_id"]
-    assert data["verbose_msg"]
-    assert data["url"] == f"http://{URL_DOMAIN}/"
-    assert data["scan_date"]
-
-
-def test_scan_url_info_v3(vtotal_v3):
-    """
-    Test scanning URL and retrieving the scan results from the VirusTotal v3 API.
-    """
-    resp = vtotal_v3.request("urls", data={"url": URL_DOMAIN}, method="POST")
-    assert resp.status_code == 200
-    assert resp.data["id"]
-    # URL safe encode URL in base64 format
-    # https://developers.virustotal.com/v3.0/reference#url
-    url_id = urlsafe_b64encode(URL_DOMAIN.encode()).decode().strip("=")
-    print(f"URL: {URL_DOMAIN} ID: {url_id}")
-    # Obtain the analysis results for the URL using the url_id
-    analysis_resp = vtotal_v3.request(f"urls/{url_id}")
-    assert analysis_resp.status_code == 200
-    assert analysis_resp.object_type == "url"
-    assert analysis_resp.data["attributes"]
-
-
-def test_domain_info_v2(vtotal_v2):
-    """
-    Test for retrieving domain information from the VirusTotal v2 API.
-    """
-    resp = vtotal_v2.request("domain/report", params={"domain": URL_DOMAIN})
-    assert resp.response_code == 1
-    json = resp.json()
-    assert json["Alexa rank"]
-    assert json["Alexa domain info"]
-    assert json["Webutation domain info"]["Verdict"]
-    assert json["whois_timestamp"]
-
-
-def test_domain_info_v3(vtotal_v3):
-    """
-    Test for retrieving domain information from the VirusTotal v3 API.
-    """
-    resp = vtotal_v3.request(f"domains/{URL_DOMAIN}")
-    assert resp.status_code == 200
-    assert resp.object_type == "domain"
-    data = resp.data
-    assert isinstance(data["links"], dict)
-    assert data["attributes"]["last_analysis_results"]
-    assert data["attributes"]["creation_date"]
-
-
-def test_retrieve_comment_file_id_v3(vtotal_v3):
-    """
-    Test for retrieving a comment for a given file ID.
-    """
-    resp = vtotal_v3.request(f"files/{FILE_ID}/comments", params={"limit": 2})
-    assert resp.status_code == 200
-    assert resp.links
-    assert resp.meta
-    assert resp.cursor
-    json = resp.data
-    # Retrieve first comment text
-    assert json[0]["attributes"]["text"]
-    # Retrieve second comment tags
-    assert json[1]["attributes"]["votes"] and isinstance(
-        json[1]["attributes"]["votes"], dict
-    )
-
-
-def test_retrieve_comment_file_id_v2(vtotal_v2):
-    """
-    Test for retrieving a comment for a given file ID using the VirusTotal v2 API.
-    """
-    resp = vtotal_v2.request("comments/get", params={"resource": FILE_ID})
-    assert resp.status_code == 200
-    assert resp.response_code == 1
-    json = resp.json()
-    for commentdata in json["comments"]:
-        assert commentdata["date"]
-        assert commentdata["comment"]
-
-
-def test_retrieve_comment_latest_v3(vtotal_v3):
-    """
-    Test for retrieveing the latest 10 comments made on VirusTotal.
-    """
-    resp = vtotal_v3.request("comments", params={"limit": 10})
-    assert resp.status_code == 200
-    assert resp.links
-    assert resp.meta
-    assert resp.cursor
-    json = resp.data
-    assert json[0]["attributes"]["date"]
-    assert len(json) == 10
-
-
-def test_retrieve_ip_info_v2(vtotal_v2):
-    """
-    Test for retrieving information about an IP address using the VirusTotal v2 API.
-    """
-    resp = vtotal_v2.request("ip-address/report", params={"ip": IP})
-    assert resp.status_code == 200
-    assert resp.response_code == 1
-    json = resp.json()
-    assert json["as_owner"] == "GOOGLE"
-    assert json["country"] == "US"
-    assert json["verbose_msg"] == "IP address in dataset"
-    for sample in json["detected_communicating_samples"]:
-        assert sample["date"]
-        assert sample["positives"]
-        assert sample["sha256"]
-        assert sample["total"]
-
-
-def test_retrieve_ip_info_v3(vtotal_v3):
-    """
-    Test for retrieving information about an IP address.
-    """
-    resp = vtotal_v3.request(f"ip_addresses/{IP}")
-    assert resp.status_code == 200
-    data = resp.data
-    assert data["attributes"]["as_owner"] == "GOOGLE"
-    assert data["attributes"]["country"] == "US"
-    assert data["attributes"]["last_analysis_stats"]
-    assert data["attributes"]["reputation"]
-    assert resp.object_type == "ip_address"
-
-
-def test_retrieve_graph_v3(vtotal_v3):
-    """
-    Test for retrieving the latest 3 graphs made on VirusTotal.
-    """
-    resp = vtotal_v3.request("graphs", params={"limit": 3})
-    assert resp.status_code == 200
-    assert len(resp.data) == 3
-    data = resp.data
-    assert data[0]["attributes"]["graph_data"]
-    assert resp.links
-    assert resp.meta
-    assert resp.cursor
-
-
-def test_search_v3(vtotal_v3):
-    """
-    Test searching the VirusTotal API.
-    """
-    resp = vtotal_v3.request("search", params={"query": URL_DOMAIN})
-    assert resp.status_code == 200
-    assert resp.links
-    assert resp.object_type
-    data = resp.data
-    assert data[0]["attributes"]["creation_date"]
-    assert data[0]["attributes"]["last_analysis_results"]
-    assert data[0]["attributes"]["last_analysis_stats"]
-    assert data[0]["attributes"]["last_dns_records"]
-    assert data[0]["attributes"]["last_https_certificate"]
-    assert data[0]["attributes"]["total_votes"]
-
-
-def test_metadata_v3(vtotal_v3):
-    """
-    Test retrieving metadata from the VirusTotal API.
-    """
-    resp = vtotal_v3.request("metadata")
-    assert resp.status_code == 200
-    engines_dict = resp.data["engines"]
-    assert engines_dict.keys()
-    assert "Microsoft" in engines_dict.keys()
-
-
-def test_contextmanager_v2():
-    """
-    Test basic Context Manager support.
-    """
-    with virustotal_python.Virustotal() as vtotal:
-        # Retrieve information about an IP address
-        resp = vtotal.request("ip-address/report", params={"ip": IP})
-        assert resp.status_code == 200
-        data = resp.json()
-        assert data["as_owner"] == "GOOGLE"
-        assert data["country"] == "US"
-
-
-def test_contextmanager_v3():
-    """
-    Test basic Context Manager support.
-    """
-    with virustotal_python.Virustotal(API_VERSION="v3") as vtotal:
-        # Retrieve information about an IP address
-        resp = vtotal.request(f"ip_addresses/{IP}")
-        assert resp.status_code == 200
-        data = resp.data
-        assert data["id"] == IP
-        assert data["attributes"]["as_owner"] == "GOOGLE"
-        assert data["attributes"]["country"] == "US"
-
-
-def test_large_file_v2(vtotal_v2, large_file_fixture):
-    """Test sending a large file to the VirusTotal v2 API for analysis.
-
-    https://developers.virustotal.com/v2.0/reference/file-scan-upload-url
-
-    NOTE: Currently this test does not work and returns a HTTP 500 internal server error.
-
-    Please see: https://github.com/dbrennand/virustotal-python/pull/33#issuecomment-1008307393
-    """
-    # Get URL to send large file
-    upload_url = vtotal_v2.request("file/scan/upload_url").json()["upload_url"]
-    # Expect VirustotalError due to HTTP 500 internal server error
-    with pytest.raises(virustotal_python.VirustotalError):
-        # Submit large file to VirusTotal v2 API for analysis
-        resp = vtotal_v2.request(
-            upload_url, files=large_file_fixture, method="POST", large_file=True
-        )
-        assert resp.status_code == 200
-        data = resp.json()
-        assert data["scan_id"]
-
-
-def test_large_file_v3(vtotal_v3, large_file_fixture):
-    """Test sending a large file to the VirusTotal v3 API for analysis.
-
-    https://developers.virustotal.com/reference/files-upload-url
-    """
-    # Get URL to send large file
-    upload_url = vtotal_v3.request("files/upload_url").data
-    # Submit large file to VirusTotal v3 API for analysis
-    resp = vtotal_v3.request(
-        upload_url, files=large_file_fixture, method="POST", large_file=True
-    )
-    assert resp.status_code == 200
-    data = resp.data
-    assert data["id"]
-    assert data["type"] == "analysis"