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

Issue:
Work on support for VirusTotal v3 API
VirusTotal now has a 3rd version of their API.

This issue is open to track adding this functionality for this API version to virustotal-python.

Work on this can be found in the [redesign](https://github.com/dbrennand/virustotal-python/tree/redesign) branch.


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index 894a44c..9f5febc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -24,6 +24,7 @@ wheels/
 .installed.cfg
 *.egg
 MANIFEST
+run.py
 
 # PyInstaller
 #  Usually these files are written by a python script from a template
diff --git a/LICENSE b/LICENSE
index 81f990d..77f0a0e 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 MIT License
 
-Copyright (c) 2020
+Copyright (c) 2020 dbrennand
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
diff --git a/Pipfile.lock b/Pipfile.lock
index 2e2aba9..e133728 100644
--- a/Pipfile.lock
+++ b/Pipfile.lock
@@ -18,10 +18,10 @@
     "default": {
         "certifi": {
             "hashes": [
-                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
-                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
+                "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
+                "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
             ],
-            "version": "==2019.11.28"
+            "version": "==2020.6.20"
         },
         "chardet": {
             "hashes": [
@@ -32,10 +32,11 @@
         },
         "idna": {
             "hashes": [
-                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
-                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
             ],
-            "version": "==2.8"
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.10"
         },
         "pysocks": {
             "hashes": [
@@ -50,64 +51,66 @@
                 "socks"
             ],
             "hashes": [
-                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
-                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
+                "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
+                "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
             ],
             "index": "pypi",
-            "version": "==2.22.0"
+            "version": "==2.24.0"
         },
         "urllib3": {
             "hashes": [
-                "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
-                "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
+                "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
+                "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
             ],
-            "version": "==1.25.8"
+            "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.25.11"
         }
     },
     "develop": {
         "appdirs": {
             "hashes": [
-                "sha256:9e5896d1372858f8dd3344faf4e5014d21849c756c8d5701f78f8a103b372d92",
-                "sha256:d8b24664561d0d34ddfaec54636d502d7cea6e29c3eaf68f3df6180863e2166e"
+                "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41",
+                "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"
             ],
-            "version": "==1.4.3"
+            "version": "==1.4.4"
         },
         "atomicwrites": {
             "hashes": [
-                "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4",
-                "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6"
+                "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197",
+                "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"
             ],
-            "version": "==1.3.0"
+            "markers": "sys_platform == 'win32'",
+            "version": "==1.4.0"
         },
         "attrs": {
             "hashes": [
-                "sha256:08a96c641c3a74e44eb59afb61a24f2cb9f4d7188748e76ba4bb5edfa3cb7d1c",
-                "sha256:f7b7ce16570fe9965acd6d30101a28f62fb4a7f9e926b3bbc9b61f8b04247e72"
+                "sha256:31b2eced602aa8423c2aea9c76a724617ed67cf9513173fd3a4f03e3a929c7e6",
+                "sha256:832aa3cde19744e49938b91fea06d69ecb9e649c93ba974535d08ad92164f700"
             ],
-            "version": "==19.3.0"
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==20.3.0"
         },
         "black": {
             "hashes": [
-                "sha256:09a9dcb7c46ed496a9850b76e4e825d6049ecd38b611f1224857a79bd985a8cf",
-                "sha256:68950ffd4d9169716bcb8719a56c07a2f4485354fec061cdd5910aa07369731c"
+                "sha256:1c02557aa099101b9d21496f8a914e9ed2222ef70336404eeeac8edba836fbea"
             ],
             "index": "pypi",
-            "version": "==19.3b0"
+            "version": "==20.8b1"
         },
         "bleach": {
             "hashes": [
-                "sha256:cc8da25076a1fe56c3ac63671e2194458e0c4d9c7becfd52ca251650d517903c",
-                "sha256:e78e426105ac07026ba098f04de8abe9b6e3e98b5befbf89b51a5ef0a4292b03"
+                "sha256:52b5919b81842b1854196eaae5ca29679a2f2e378905c346d3ca8227c2c66080",
+                "sha256:9f8ccbeb6183c6e6cddea37592dfb0167485c1e3b13b3363bc325aa8bda3adbd"
             ],
-            "index": "pypi",
-            "version": "==3.1.4"
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==3.2.1"
         },
         "certifi": {
             "hashes": [
-                "sha256:017c25db2a153ce562900032d5bc68e9f191e44e9a0f762f373977de9df1fbb3",
-                "sha256:25b64c7da4cd7479594d035c08c2d809eb4aab3a26e5a990ea98cc450c320f1f"
+                "sha256:5930595817496dd21bb8dc35dad090f1c2cd0adfaf21204bf6732ca5d8ee34d3",
+                "sha256:8fc0819f1f30ba15bdb34cceffb9ef04d99f420f68eb75d901e9560b8749fc41"
             ],
-            "version": "==2019.11.28"
+            "version": "==2020.6.20"
         },
         "chardet": {
             "hashes": [
@@ -118,108 +121,201 @@
         },
         "click": {
             "hashes": [
-                "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc",
-                "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a"
+                "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a",
+                "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
+            "version": "==7.1.2"
+        },
+        "colorama": {
+            "hashes": [
+                "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b",
+                "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"
             ],
-            "version": "==7.1.1"
+            "markers": "sys_platform == 'win32'",
+            "version": "==0.4.4"
         },
         "docutils": {
             "hashes": [
                 "sha256:0c5b78adfbf7762415433f5515cd5c9e762339e23369dbe8000d84a4bf4ab3af",
                 "sha256:c2de3a60e9e7d07be26b7f2b00ca0309c207e06c100f9cc2a94931fc75a478fc"
             ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'",
             "version": "==0.16"
         },
         "idna": {
             "hashes": [
-                "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407",
-                "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c"
+                "sha256:b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6",
+                "sha256:b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0"
             ],
-            "version": "==2.8"
+            "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:2a688cbaa90e0cc587f1df48bdc97a6eadccdcd9c35fb3f976a09e3b5016d90f",
-                "sha256:34513a8a0c4962bc66d35b359558fd8a5e10cd472d37aec5f66858addef32c1e"
+                "sha256:77a540690e24b0305878c37ffd421785a6f7e53c8b5720d211b211de8d0e95da",
+                "sha256:cefa1a2f919b866c5beb7c9f7b0ebb4061f30a8a9bf16d609b000e2dfaceb9c3"
             ],
             "markers": "python_version < '3.8'",
-            "version": "==1.6.0"
+            "version": "==2.0.0"
+        },
+        "iniconfig": {
+            "hashes": [
+                "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3",
+                "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"
+            ],
+            "version": "==1.1.1"
+        },
+        "keyring": {
+            "hashes": [
+                "sha256:4e34ea2fdec90c1c43d6610b5a5fafa1b9097db1802948e90caf5763974b8f8d",
+                "sha256:9aeadd006a852b78f4b4ef7c7556c2774d2432bbef8ee538a3e9089ac8b11466"
+            ],
+            "markers": "python_version >= '3.6'",
+            "version": "==21.4.0"
         },
-        "more-itertools": {
+        "mypy-extensions": {
             "hashes": [
-                "sha256:5dd8bcf33e5f9513ffa06d5ad33d78f31e1931ac9a18f33d37e77a180d393a7c",
-                "sha256:b1ddb932186d8a6ac451e1d95844b382f55e12686d51ca0c68b6f61f2ab7a507"
+                "sha256:090fedd75945a69ae91ce1303b5824f428daf5a028d2f6ab8a299250a846f15d",
+                "sha256:2d82818f5bb3e369420cb3c4060a7970edba416647068eb4c5343488a6c604a8"
             ],
-            "markers": "python_version > '2.7'",
-            "version": "==8.2.0"
+            "version": "==0.4.3"
         },
         "packaging": {
             "hashes": [
-                "sha256:3c292b474fda1671ec57d46d739d072bfd495a4f51ad01a055121d81e952b7a3",
-                "sha256:82f77b9bee21c1bafbf35a84905d604d5d1223801d639cf3ed140bd651c08752"
+                "sha256:4357f74f47b9c12db93624a82154e9b120fa8293699949152b22065d556079f8",
+                "sha256:998416ba6962ae7fbd6596850b80e17859a5753ba17c32284f67bfff33784181"
             ],
-            "version": "==20.3"
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==20.4"
+        },
+        "pathspec": {
+            "hashes": [
+                "sha256:7d91249d21749788d07a2d0f94147accd8f845507400749ea19c1ec9054a12b0",
+                "sha256:da45173eb3a6f2a5a487efba21f050af2b41948be6ab52b6a1e3ff22bb8b7061"
+            ],
+            "version": "==0.8.0"
         },
         "pkginfo": {
             "hashes": [
-                "sha256:7424f2c8511c186cd5424bbf31045b77435b37a8d604990b79d4e70d741148bb",
-                "sha256:a6d9e40ca61ad3ebd0b72fbadd4fba16e4c0e4df0428c041e01e06eb6ee71f32"
+                "sha256:a6a4ac943b496745cec21f14f021bbd869d5e9b4f6ec06918cffea5a2f4b9193",
+                "sha256:ce14d7296c673dc4c61c759a0b6c14bae34e34eb819c0017bb6ca5b7292c56e9"
             ],
-            "version": "==1.5.0.1"
+            "version": "==1.6.1"
         },
         "pluggy": {
             "hashes": [
                 "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0",
                 "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"
             ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
             "version": "==0.13.1"
         },
         "py": {
             "hashes": [
-                "sha256:5e27081401262157467ad6e7f851b7aa402c5852dbcb3dae06768434de5752aa",
-                "sha256:c20fdd83a5dbc0af9efd622bee9a5564e278f6380fffcacc43ba6f43db2813b0"
+                "sha256:366389d1db726cd2fcfc79732e75410e5fe4d31db13692115529d34069a043c2",
+                "sha256:9ca6883ce56b4e8da7e79ac18787889fa5206c79dcc67fb065376cd2fe03f342"
             ],
-            "version": "==1.8.1"
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.9.0"
         },
         "pygments": {
             "hashes": [
-                "sha256:647344a061c249a3b74e230c739f434d7ea4d8b1d5f3721bc0f3558049b38f44",
-                "sha256:ff7a40b4860b727ab48fad6360eb351cc1b33cbf9b15a0f689ca5353e9463324"
+                "sha256:381985fcc551eb9d37c52088a32914e00517e57f4a21609f48141ba08e193fa0",
+                "sha256:88a0bbcd659fcb9573703957c6b9cff9fab7295e6e76db54c9d00ae42df32773"
             ],
-            "version": "==2.6.1"
+            "markers": "python_version >= '3.5'",
+            "version": "==2.7.2"
         },
         "pyparsing": {
             "hashes": [
-                "sha256:4c830582a84fb022400b85429791bc551f1f4871c33f23e44f353119e92f969f",
-                "sha256:c342dccb5250c08d45fd6f8b4a559613ca603b57498511740e65cd11a2e7dcec"
+                "sha256:c203ec8783bf771a155b207279b9bccb8dea02d8f0c9e5f8ead507bc3246ecc1",
+                "sha256:ef9d7589ef3c200abe66653d3f1ab1033c3c419ae9b9bdb1240a85b024efc88b"
             ],
-            "version": "==2.4.6"
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==2.4.7"
         },
         "pytest": {
             "hashes": [
-                "sha256:4a784f1d4f2ef198fe9b7aef793e9fa1a3b2f84e822d9b3a64a181293a572d45",
-                "sha256:926855726d8ae8371803f7b2e6ec0a69953d9c6311fa7c3b6c1b929ff92d27da"
+                "sha256:4288fed0d9153d9646bfcdf0c0428197dba1ecb27a33bb6e031d002fa88653fe",
+                "sha256:c0a7e94a8cdbc5422a51ccdad8e6f1024795939cc89159a0ae7f0b316ad3823e"
             ],
             "index": "pypi",
-            "version": "==4.6.3"
+            "version": "==6.1.2"
         },
-        "readme-renderer": {
+        "pywin32-ctypes": {
             "hashes": [
-                "sha256:1b6d8dd1673a0b293766b4106af766b6eff3654605f9c4f239e65de6076bc222",
-                "sha256:e67d64242f0174a63c3b727801a2fff4c1f38ebe5d71d95ff7ece081945a6cd4"
+                "sha256:24ffc3b341d457d48e8922352130cf2644024a4ff09762a2261fd34c36ee5942",
+                "sha256:9dc2d991b3479cc2df15930958b674a48a227d5361d413827a4cfd0b5876fc98"
             ],
-            "version": "==25.0"
+            "markers": "sys_platform == 'win32'",
+            "version": "==0.2.0"
+        },
+        "readme-renderer": {
+            "hashes": [
+                "sha256:267854ac3b1530633c2394ead828afcd060fc273217c42ac36b6be9c42cd9a9d",
+                "sha256:6b7e5aa59210a40de72eb79931491eaf46fefca2952b9181268bd7c7c65c260a"
+            ],
+            "version": "==28.0"
+        },
+        "regex": {
+            "hashes": [
+                "sha256:03855ee22980c3e4863dc84c42d6d2901133362db5daf4c36b710dd895d78f0a",
+                "sha256:06b52815d4ad38d6524666e0d50fe9173533c9cc145a5779b89733284e6f688f",
+                "sha256:11116d424734fe356d8777f89d625f0df783251ada95d6261b4c36ad27a394bb",
+                "sha256:119e0355dbdd4cf593b17f2fc5dbd4aec2b8899d0057e4957ba92f941f704bf5",
+                "sha256:127a9e0c0d91af572fbb9e56d00a504dbd4c65e574ddda3d45b55722462210de",
+                "sha256:1ec66700a10e3c75f1f92cbde36cca0d3aaee4c73dfa26699495a3a30b09093c",
+                "sha256:227a8d2e5282c2b8346e7f68aa759e0331a0b4a890b55a5cfbb28bd0261b84c0",
+                "sha256:2564def9ce0710d510b1fc7e5178ce2d20f75571f788b5197b3c8134c366f50c",
+                "sha256:297116e79074ec2a2f885d22db00ce6e88b15f75162c5e8b38f66ea734e73c64",
+                "sha256:2dc522e25e57e88b4980d2bdd334825dbf6fa55f28a922fc3bfa60cc09e5ef53",
+                "sha256:3a5f08039eee9ea195a89e180c5762bfb55258bfb9abb61a20d3abee3b37fd12",
+                "sha256:3dfca201fa6b326239e1bccb00b915e058707028809b8ecc0cf6819ad233a740",
+                "sha256:49461446b783945597c4076aea3f49aee4b4ce922bd241e4fcf62a3e7c61794c",
+                "sha256:4afa350f162551cf402bfa3cd8302165c8e03e689c897d185f16a167328cc6dd",
+                "sha256:4b5a9bcb56cc146c3932c648603b24514447eafa6ce9295234767bf92f69b504",
+                "sha256:52e83a5f28acd621ba8e71c2b816f6541af7144b69cc5859d17da76c436a5427",
+                "sha256:625116aca6c4b57c56ea3d70369cacc4d62fead4930f8329d242e4fe7a58ce4b",
+                "sha256:654c1635f2313d0843028487db2191530bca45af61ca85d0b16555c399625b0e",
+                "sha256:8092a5a06ad9a7a247f2a76ace121183dc4e1a84c259cf9c2ce3bbb69fac3582",
+                "sha256:832339223b9ce56b7b15168e691ae654d345ac1635eeb367ade9ecfe0e66bee0",
+                "sha256:8ca9dca965bd86ea3631b975d63b0693566d3cc347e55786d5514988b6f5b84c",
+                "sha256:96f99219dddb33e235a37283306834700b63170d7bb2a1ee17e41c6d589c8eb9",
+                "sha256:9b6305295b6591e45f069d3553c54d50cc47629eb5c218aac99e0f7fafbf90a1",
+                "sha256:a62162be05edf64f819925ea88d09d18b09bebf20971b363ce0c24e8b4aa14c0",
+                "sha256:aacc8623ffe7999a97935eeabbd24b1ae701d08ea8f874a6ff050e93c3e658cf",
+                "sha256:b45bab9f224de276b7bc916f6306b86283f6aa8afe7ed4133423efb42015a898",
+                "sha256:b88fa3b8a3469f22b4f13d045d9bd3eda797aa4e406fde0a2644bc92bbdd4bdd",
+                "sha256:b8a686a6c98872007aa41fdbb2e86dc03b287d951ff4a7f1da77fb7f14113e4d",
+                "sha256:bd904c0dec29bbd0769887a816657491721d5f545c29e30fd9d7a1a275dc80ab",
+                "sha256:bf4f896c42c63d1f22039ad57de2644c72587756c0cfb3cc3b7530cfe228277f",
+                "sha256:c13d311a4c4a8d671f5860317eb5f09591fbe8259676b86a85769423b544451e",
+                "sha256:c2c6c56ee97485a127555c9595c069201b5161de9d05495fbe2132b5ac104786",
+                "sha256:c32c91a0f1ac779cbd73e62430de3d3502bbc45ffe5bb6c376015acfa848144b",
+                "sha256:c3466a84fce42c2016113101018a9981804097bacbab029c2d5b4fcb224b89de",
+                "sha256:c454ad88e56e80e44f824ef8366bb7e4c3def12999151fd5c0ea76a18fe9aa3e",
+                "sha256:c8a2b7ccff330ae4c460aff36626f911f918555660cc28163417cb84ffb25789",
+                "sha256:cb905f3d2e290a8b8f1579d3984f2cfa7c3a29cc7cba608540ceeed18513f520",
+                "sha256:cfcf28ed4ce9ced47b9b9670a4f0d3d3c0e4d4779ad4dadb1ad468b097f808aa",
+                "sha256:dd3e6547ecf842a29cf25123fbf8d2461c53c8d37aa20d87ecee130c89b7079b",
+                "sha256:de7fd57765398d141949946c84f3590a68cf5887dac3fc52388df0639b01eda4",
+                "sha256:ea37320877d56a7f0a1e6a625d892cf963aa7f570013499f5b8d5ab8402b5625",
+                "sha256:f1fce1e4929157b2afeb4bb7069204d4370bab9f4fc03ca1fbec8bd601f8c87d",
+                "sha256:f43109822df2d3faac7aad79613f5f02e4eab0fc8ad7932d2e70e2a83bd49c26"
+            ],
+            "version": "==2020.10.28"
         },
         "requests": {
             "extras": [
                 "socks"
             ],
             "hashes": [
-                "sha256:11e007a8a2aa0323f5a921e9e6a2d7e4e67d9877e85773fba9ba6419025cbeb4",
-                "sha256:9cf5292fcd0f598c671cfc1e0d7d1a7f13bb8085e9a590f48c010551dc6c4b31"
+                "sha256:b3559a131db72c33ee969480840fff4bb6dd111de7dd27c8ee1f820f4f00231b",
+                "sha256:fe75cc94a9443b9246fc7049224f75604b113c36acb93f87b80ed42c44cbb898"
             ],
             "index": "pypi",
-            "version": "==2.22.0"
+            "version": "==2.24.0"
         },
         "requests-toolbelt": {
             "hashes": [
@@ -228,48 +324,95 @@
             ],
             "version": "==0.9.1"
         },
+        "rfc3986": {
+            "hashes": [
+                "sha256:112398da31a3344dc25dbf477d8df6cb34f9278a94fee2625d89e4514be8bb9d",
+                "sha256:af9147e9aceda37c91a05f4deb128d4b4b49d6b199775fd2d2927768abdc8f50"
+            ],
+            "version": "==1.4.0"
+        },
         "six": {
             "hashes": [
-                "sha256:236bdbdce46e6e6a3d61a337c0f8b763ca1e8717c03b369e87a7ec7ce1319c0a",
-                "sha256:8f3cd2e254d8f793e7f3d6d9df77b92252b52637291d0f0da013c76ea2724b6c"
+                "sha256:30639c035cdb23534cd4aa2dd52c3bf48f06e5f4a941509c8bafd8ce11080259",
+                "sha256:8b74bedcbbbaca38ff6d7491d76f2b06b3592611af620f8426e82dddb04a5ced"
             ],
-            "version": "==1.14.0"
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==1.15.0"
         },
         "toml": {
             "hashes": [
-                "sha256:229f81c57791a41d65e399fc06bf0848bab550a9dfd5ed66df18ce5f05e73d5c",
-                "sha256:235682dd292d5899d361a811df37e04a8828a5b1da3115886b73cf81ebc9100e"
+                "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b",
+                "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"
             ],
-            "version": "==0.10.0"
+            "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:03d2366c64d44c7f61e74c700d9b202d57e9efe355ea5c28814c52bfe7a50b8c",
-                "sha256:be5ddeec77d78ba781ea41eacb2358a77f74cc2407f54b82222d7ee7dc8c8ccf"
+                "sha256:9ad44aaf0fc3697c06f6e05c7cf025dd66bc7bcb7613c66d85f4464c47ac8fad",
+                "sha256:ef54779f1c09f346b2b5a8e5c61f96fbcb639929e640e59f8cf810794f406432"
             ],
-            "version": "==4.44.1"
+            "markers": "python_version >= '2.6' and python_version not in '3.0, 3.1, 3.2, 3.3'",
+            "version": "==4.51.0"
         },
         "twine": {
             "hashes": [
-                "sha256:0fb0bfa3df4f62076cab5def36b1a71a2e4acb4d1fa5c97475b048117b1a6446",
-                "sha256:d6c29c933ecfc74e9b1d9fa13aa1f87c5d5770e119f5a4ce032092f0ff5b14dc"
+                "sha256:34352fd52ec3b9d29837e6072d5a2a7c6fe4290e97bba46bb8d478b5c598f7ab",
+                "sha256:ba9ff477b8d6de0c89dd450e70b2185da190514e91c42cc62f96850025c10472"
             ],
             "index": "pypi",
-            "version": "==1.13.0"
+            "version": "==3.2.0"
+        },
+        "typed-ast": {
+            "hashes": [
+                "sha256:0666aa36131496aed8f7be0410ff974562ab7eeac11ef351def9ea6fa28f6355",
+                "sha256:0c2c07682d61a629b68433afb159376e24e5b2fd4641d35424e462169c0a7919",
+                "sha256:0d8110d78a5736e16e26213114a38ca35cb15b6515d535413b090bd50951556d",
+                "sha256:249862707802d40f7f29f6e1aad8d84b5aa9e44552d2cc17384b209f091276aa",
+                "sha256:24995c843eb0ad11a4527b026b4dde3da70e1f2d8806c99b7b4a7cf491612652",
+                "sha256:269151951236b0f9a6f04015a9004084a5ab0d5f19b57de779f908621e7d8b75",
+                "sha256:3742b32cf1c6ef124d57f95be609c473d7ec4c14d0090e5a5e05a15269fb4d0c",
+                "sha256:4083861b0aa07990b619bd7ddc365eb7fa4b817e99cf5f8d9cf21a42780f6e01",
+                "sha256:498b0f36cc7054c1fead3d7fc59d2150f4d5c6c56ba7fb150c013fbc683a8d2d",
+                "sha256:4e3e5da80ccbebfff202a67bf900d081906c358ccc3d5e3c8aea42fdfdfd51c1",
+                "sha256:6daac9731f172c2a22ade6ed0c00197ee7cc1221aa84cfdf9c31defeb059a907",
+                "sha256:715ff2f2df46121071622063fc7543d9b1fd19ebfc4f5c8895af64a77a8c852c",
+                "sha256:73d785a950fc82dd2a25897d525d003f6378d1cb23ab305578394694202a58c3",
+                "sha256:7e4c9d7658aaa1fc80018593abdf8598bf91325af6af5cce4ce7c73bc45ea53d",
+                "sha256:8c8aaad94455178e3187ab22c8b01a3837f8ee50e09cf31f1ba129eb293ec30b",
+                "sha256:8ce678dbaf790dbdb3eba24056d5364fb45944f33553dd5869b7580cdbb83614",
+                "sha256:92c325624e304ebf0e025d1224b77dd4e6393f18aab8d829b5b7e04afe9b7a2c",
+                "sha256:aaee9905aee35ba5905cfb3c62f3e83b3bec7b39413f0a7f19be4e547ea01ebb",
+                "sha256:b52ccf7cfe4ce2a1064b18594381bccf4179c2ecf7f513134ec2f993dd4ab395",
+                "sha256:bcd3b13b56ea479b3650b82cabd6b5343a625b0ced5429e4ccad28a8973f301b",
+                "sha256:c9e348e02e4d2b4a8b2eedb48210430658df6951fa484e59de33ff773fbd4b41",
+                "sha256:d205b1b46085271b4e15f670058ce182bd1199e56b317bf2ec004b6a44f911f6",
+                "sha256:d43943ef777f9a1c42bf4e552ba23ac77a6351de620aa9acf64ad54933ad4d34",
+                "sha256:d5d33e9e7af3b34a40dc05f498939f0ebf187f07c385fd58d591c533ad8562fe",
+                "sha256:d648b8e3bf2fe648745c8ffcee3db3ff903d0817a01a12dd6a6ea7a8f4889072",
+                "sha256:f208eb7aff048f6bea9586e61af041ddf7f9ade7caed625742af423f6bae3298",
+                "sha256:fac11badff8313e23717f3dada86a15389d0708275bddf766cca67a84ead3e91",
+                "sha256:fc0fea399acb12edbf8a628ba8d2312f583bdbdb3335635db062fa98cf71fca4",
+                "sha256:fcf135e17cc74dbfbc05894ebca928ffeb23d9790b3167a674921db19082401f",
+                "sha256:fe460b922ec15dd205595c9b5b99e2f056fd98ae8f9f56b888e7a17dc2b757e7"
+            ],
+            "version": "==1.4.1"
+        },
+        "typing-extensions": {
+            "hashes": [
+                "sha256:7cb407020f00f7bfc3cb3e7881628838e69d8f3fcab2f64742a5e76b2f841918",
+                "sha256:99d4073b617d30288f569d3f13d2bd7548c3a7e4c8de87db09a9d29bb3a4a60c",
+                "sha256:dafc7639cde7f1b6e1acc0f457842a83e722ccca8eef5270af2d74792619a89f"
+            ],
+            "version": "==3.7.4.3"
         },
         "urllib3": {
             "hashes": [
-                "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc",
-                "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc"
-            ],
-            "version": "==1.25.8"
-        },
-        "wcwidth": {
-            "hashes": [
-                "sha256:cafe2186b3c009a04067022ce1dcd79cb38d8d65ee4f4791b8888d6599d1bbe1",
-                "sha256:ee73862862a156bf77ff92b09034fc4825dd3af9cf81bc5b360668d425f3c5f1"
+                "sha256:8d7eaa5a82a1cac232164990f04874c594c9453ec55eef02eab885aa02fc17a2",
+                "sha256:f5321fbe4bf3fefa0efd0bfe7fb14e90909eb62a48ccda331726b4319897dd5e"
             ],
-            "version": "==0.1.9"
+            "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.25.11"
         },
         "webencodings": {
             "hashes": [
@@ -280,10 +423,11 @@
         },
         "zipp": {
             "hashes": [
-                "sha256:aa36550ff0c0b7ef7fa639055d797116ee891440eac1a56f378e2d3179e0320b",
-                "sha256:c599e4d75c98f6798c509911d08a22e6c021d074469042177c8c86fb92eefd96"
+                "sha256:102c24ef8f171fd729d46599845e95c7ab894a4cf45f5de11a44cc7444fb1108",
+                "sha256:ed5eee1974372595f9e416cc7bbeeb12335201d8081ca8a0743c954d4446e5cb"
             ],
-            "version": "==3.1.0"
+            "markers": "python_version >= '3.6'",
+            "version": "==3.4.0"
         }
     }
 }
diff --git a/README.md b/README.md
index 9d82dac..e84391b 100644
--- a/README.md
+++ b/README.md
@@ -1,10 +1,17 @@
 # virustotal-python 🐍
 ![PyPI](https://img.shields.io/pypi/v/virustotal-python.svg?style=flat-square)
 
-A light wrapper around the public VirusTotal API.
+A Python library to interact with the public VirusTotal v2 and v3 APIs.
 
-# Dependencies
-* Written in Python 3.7.
+> [!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.
+
+# Dependencies and installation
+
+> [!NOTE]
+>
+> This library should work with Python versions >= 3.7.
 
 ```
 [dev-packages]
@@ -19,105 +26,193 @@ requests = {extras = ["socks"],version = "*"}
 Install `virustotal-python` using either:
 * `pip3 install virustotal-python`, `pipenv install`, `pip3 install -r requirements.txt`, `python setup.py install`.
 
-## Example Usage
+## Usage examples
+
+> [!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.
+
+Authenticate using your VirusTotal API key:
+
+> ![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)
+
 ```python
 from virustotal_python import Virustotal
-from pprint import pprint
 
-# Normal Initialisation.
-vtotal = Virustotal("Insert API Key Here.")
+# v2 example
+vtotal = Virustotal(API_KEY="Insert API key here.")
 
-# NEW as of version 0.0.5: Proxy support.
-# Example Usage: Using HTTP(S)
-vtotal = Virustotal(
-    "Insert API Key Here.",
-    {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"})
-# Or using SOCKS
+# v3 example
+vtotal = Virustotal(API_KEY="Insert API key here.", API_VERSION="v3")
+
+# 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)
+
+# You can also set proxies and timeouts for requests made by the library
 vtotal = Virustotal(
-    "Insert API Key Here.",
-    {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"})
-
-# NOTE: Check virustotal.py for docstrings containing full parameter descriptions.
-
-# Send a file to Virustotal for analysis.
-resp = vtotal.file_scan("./tests.py")  # PATH to file for querying.
-
-# Retrieve scan report(s) for given file(s) from Virustotal.
-# A list containing the resource (SHA256) HASH of a known malicious file.
-resp = vtotal.file_report(
-    ["9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"]
-)
-# A list of resource(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate).
-# The first is a scan_id, the second is a SHA256 HASH.
-resp = vtotal.file_report(
-    [
-        "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539",
-        "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115",
-    ]
-)
-
-# Query url(s) to VirusTotal.
-# A list containing a url to be scanned by VirusTotal.
-resp = vtotal.url_scan(["ihaveaproblem.info"])  # Query a single url.
-# A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate).
-resp = vtotal.url_scan(
-    ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"]
-)
-
-# 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.
-# 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(
-    [
-        "fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1555352750",
-        "google.com",
-        "wikipedia.com",
-        "github.com",
-    ],
-    scan=1,
-)
-
-# Query an IP to Virustotal.
-resp = vtotal.ipaddress_report("90.156.201.27")
-
-# Retrieve a domain report.
-resp = vtotal.domain_report("027.ru")
-
-# Put a comment onto a specific resource.
-resp = vtotal.put_comment(
-    "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115",
-    comment="#watchout, this looks very malicious!",
-)
-
-pprint(resp)
+    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)
+```
+
+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."
+```
+
+Now, initialise the `Virustotal` class:
+
+```python
+from virustotal_python import Virustotal
+
+# v2 example
+vtotal = Virustotal()
+
+# v3 example
+vtotal = Virustotal(API_VERSION="v3")
+```
+
+Send a file for analysis:
+
+```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"])
+```
+
+Retrieve information about a file:
+
+```python
+from pprint import pprint
+
+# The ID (either SHA-256, SHA-1 or MD5) 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)
 ```
 
+Send a URL for analysis, retrieve the analysis report and catch any potential exceptions that may occur (Non 200 HTTP status codes):
+
 ```python
-# Example resp for url_scan().
-# Assuming you have already initiated Virustotal() and imported pprint.
-resp = vtotal.url_scan(["ihaveaproblem.info"]) # Query a single url.
-pprint(resp)
-{'json_resp': {'permalink': 'https://www.virustotal.com/url/fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e/analysis/1549973453/',
-               'resource': 'http://ihaveaproblem.info/',
-               'response_code': 1,
-               'scan_date': '2019-02-12 12:10:53',
-               'scan_id': 'fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1549973453',
-               'url': 'http://ihaveaproblem.info/',
-               'verbose_msg': 'Scan request successfully queued, come back '
-                              'later for the report'},
- 'status_code': 200}
+from virustotal_python import VirustotalError
+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.")
 ```
 
-## Running Tests
+Retrieve information about a domain:
+
+```python
+from pprint import pprint
+
+domain = "virustotal.com"
 
-* `Navigate to ./virustotal_python/`
+# v2 example
+resp = vtotal.request("domain/report", params={"domain": domain})
 
-* `Run the command: pytest -s tests.py`
+print(resp.response_code)
+pprint(resp.json())
+
+# v3 example
+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.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`
@@ -134,11 +229,11 @@ pprint(resp)
 
 * 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 - Inital release of virustotal-python. Covered all endpoints of the Virustotal public API. 
+* 0.0.1 - Initial release of virustotal-python. Covered all endpoints of the Virustotal public API.
 
 ## Authors -- Contributors
 
-* **dbrennand** - *Author* - [dbrennand](https://github.com/dbrennand)
+* [**dbrennand**](https://github.com/dbrennand) - *Author*
 
 ## License
-This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details.
\ No newline at end of file
+This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details.
diff --git a/examples/comments.py b/examples/comments.py
new file mode 100644
index 0000000..fef6fc6
--- /dev/null
+++ b/examples/comments.py
@@ -0,0 +1,118 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Retrieve comments and interact with them using the VirusTotal API.
+
+Documentation:
+
+    * v2 documentation - https://developers.virustotal.com/reference#comments-get
+
+        * https://developers.virustotal.com/reference#comments-put
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#comments-1
+"""
+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
+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)
+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"
+
+# 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())
+
+# v3 examples
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+
+## Retriving comments for resources
+### Retrieve 10 comments for a file
+resp = vtotal.request(f"files/{FILE_ID}/comments", params={"limit": 10})
+### Retrieve 2 comments for a URL
+resp = vtotal.request(f"urls/{URL_ID}/comments", params={"limit": 2})
+### Retrieve 2 comments for a domain
+resp = vtotal.request(f"domains/{DOMAIN}/comments", params={"limit": 2})
+### Retrieve 5 comments for an IP address
+resp = vtotal.request(f"ip_addresses/{IP}/comments", params={"limit": 5})
+### Retrieve 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!"
+        }
+    }
+}
+
+## 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
+resp = vtotal.request(f"urls/{URL_ID}/comments", json=comment, method="POST")
+### Submit a comment on a domain
+resp = vtotal.request(f"domains/{DOMAIN}/comments", json=comment, method="POST")
+### 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
+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
+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 🤔
+resp = vtotal.request("comments", params={"limit": 10, "filter": "rat"})
+### Retrieve a specific 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!"
+        }
+    }
+}
+resp = vtotal.request(f"comments/{COMMENT_ID}", json=edited_comment, method="PATCH")
+### 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")
diff --git a/examples/cursor.py b/examples/cursor.py
new file mode 100644
index 0000000..51e39b0
--- /dev/null
+++ b/examples/cursor.py
@@ -0,0 +1,35 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Use a cursor from the VirusTotal API JSON response to retrieve more results.
+
+Documentation:
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#collections
+"""
+from virustotal_python import Virustotal
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+# Example IP address (Google DNS)
+IP = "8.8.8.8"
+
+# v3 example
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+
+# Retrieve communicating_files related to the IP address with a limit of 5
+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})
+    # 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
new file mode 100644
index 0000000..9be647f
--- /dev/null
+++ b/examples/domain_info.py
@@ -0,0 +1,32 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Retrieve information about a domain from the VirusTotal API.
+
+Documentation:
+
+    * v2 documentation - https://developers.virustotal.com/reference#domain-report
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#domain-info
+"""
+from virustotal_python import Virustotal
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+domain = "virustotal.com"
+
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY)
+
+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
new file mode 100644
index 0000000..67554e2
--- /dev/null
+++ b/examples/file_info.py
@@ -0,0 +1,33 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Retrieve information about a file from the VirusTotal API.
+
+Documentation:
+
+    * v2 documentation - https://developers.virustotal.com/reference#file-report
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#file-info
+"""
+from virustotal_python import Virustotal
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+# The ID (either SHA-256, SHA-1 or MD5) identifying the file
+FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
+
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY)
+
+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
new file mode 100644
index 0000000..1aff2f1
--- /dev/null
+++ b/examples/graphs.py
@@ -0,0 +1,29 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Retrieve graphs and interact with them using the VirusTotal v3 API.
+
+Documentation:
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#graphs-1
+"""
+from virustotal_python import Virustotal
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+# Example ID of a graph
+GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d"
+
+# v3 examples
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+
+## Retrieve 3 graphs from the VirusTotal v3 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
+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
new file mode 100644
index 0000000..00e657a
--- /dev/null
+++ b/examples/ip.py
@@ -0,0 +1,48 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Retrieve IP addresses using the VirusTotal API.
+
+Documentation:
+
+    * v2 documentation - https://developers.virustotal.com/reference#ip-address-report
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#ip-addresses
+"""
+from virustotal_python import Virustotal
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+# Example IP address (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")
+
+# Retrieve 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
+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})
+
+# Retrieve 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
+vote = {"data": {"type": "vote", "attributes": {"verdict": "harmless"}}}
+resp = vtotal.request(f"ip_addresses/{IP}/votes", json=vote, method="POST")
diff --git a/examples/scan_file.py b/examples/scan_file.py
new file mode 100644
index 0000000..469564c
--- /dev/null
+++ b/examples/scan_file.py
@@ -0,0 +1,37 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Send a file to the VirusTotal API for analysis.
+
+Documentation:
+
+    * v2 documentation - https://developers.virustotal.com/reference#file-scan
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#files-scan
+"""
+from virustotal_python import Virustotal
+import os.path
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+# 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")
+
+resp = vtotal.request("files", files=files, method="POST")
+
+pprint(resp.data)
diff --git a/examples/scan_urls.py b/examples/scan_urls.py
new file mode 100644
index 0000000..9ffee16
--- /dev/null
+++ b/examples/scan_urls.py
@@ -0,0 +1,52 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Send URLs to the VirusTotal API for analysis and retrieve the analysis results.
+
+Documentation:
+
+    * v2 documentation - https://developers.virustotal.com/reference#url-scan
+
+        * https://developers.virustotal.com/reference#url-report
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#urls
+
+        * https://developers.virustotal.com/v3.0/reference#url-info
+"""
+from virustotal_python import Virustotal
+import os.path
+from pprint import pprint
+from base64 import urlsafe_b64encode
+
+API_KEY = "Insert API key here."
+
+URLS = ["google.com", "wikipedia.com", "github.com", "ihaveaproblem.info"]
+
+# v2 example
+vtotal = Virustotal(API_KEY=API_KEY)
+
+# Send the URLs to VirusTotal for analysis
+# 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")
+for url_resp in 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())
+
+# 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
new file mode 100644
index 0000000..c1c38ab
--- /dev/null
+++ b/examples/searchmetadata.py
@@ -0,0 +1,39 @@
+"""
+The examples in this file are for virustotal-python version >=0.1.0
+
+Search the VirusTotal v3 API for a domain, IP address and comment tag.
+
+Also, retrieve VirusTotal metadata.
+
+Documentation:
+
+    * v3 documentation - https://developers.virustotal.com/v3.0/reference#search-1
+
+        * https://developers.virustotal.com/v3.0/reference#metadata
+"""
+from virustotal_python import Virustotal
+from pprint import pprint
+
+API_KEY = "Insert API key here."
+
+# The ID (either SHA-256, SHA-1 or MD5) identifying the file
+FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"
+
+# v3 examples
+vtotal = Virustotal(API_KEY=API_KEY, API_VERSION="v3")
+
+## 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)
+resp = vtotal.request("search", params={"query": "8.8.8.8"})
+## 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'
+resp = vtotal.request("search", params={"query": "#malicious"})
+
+## Retrieve VirusTotal metadata
+resp = vtotal.request("metadata")
+## 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/images/APIKey.png b/images/APIKey.png
new file mode 100644
index 0000000..d9ae185
Binary files /dev/null and b/images/APIKey.png differ
diff --git a/requirements.txt b/requirements.txt
index b6ce5c8..3d3f94f 100644
Binary files a/requirements.txt and b/requirements.txt differ
diff --git a/setup.py b/setup.py
index e8fe684..022c29b 100644
--- a/setup.py
+++ b/setup.py
@@ -5,9 +5,9 @@
 
 setup(
     name="virustotal-python",
-    version="0.0.9",
+    version="0.1.0",
     author="dbrennand",
-    description="A light wrapper around the public VirusTotal API.",
+    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",
@@ -17,5 +17,5 @@
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.7",
     ],
-    keywords="Light VirusTotal Wrapper Public API Library",
+    keywords="VirusTotal Wrapper Public API Library v2 v3",
 )
diff --git a/virustotal_python/examples.py b/virustotal_python/oldexamples.py
similarity index 66%
rename from virustotal_python/examples.py
rename to virustotal_python/oldexamples.py
index f5e1b67..5b6f32e 100644
--- a/virustotal_python/examples.py
+++ b/virustotal_python/oldexamples.py
@@ -1,46 +1,54 @@
+"""
+The examples below are for virustotal-python versions <0.1.0.
+
+Examples for versions >=0.1.0 head to /examples directory.
+"""
 from virustotal import Virustotal
 from pprint import pprint
+import os.path
 
-# Normal Initialisation.
-vtotal = Virustotal("Insert API Key Here.")
+# Normal Initialisation
+vtotal = Virustotal("Insert API key here.")
 
-# NEW as of version 0.0.5: Proxy support.
+# NEW as of version 0.0.5: Proxy support
 # Example Usage: Using HTTP(S)
 vtotal = Virustotal(
-    "Insert API Key Here.",
-    {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"})
+    "Insert API key here.",
+    {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"},
+)
 # Or using SOCKS
 vtotal = Virustotal(
-    "Insert API Key Here.",
-    {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"})
+    "Insert API key here.",
+    {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"},
+)
 
-# NOTE: Check virustotal.py for docstrings containing full parameter descriptions.
+# NOTE: Check virustotal.py for docstrings containing full parameter descriptions
 
-# Send a file to Virustotal for analysis.
-resp = vtotal.file_scan("./tests.py")  # PATH to file for querying.
+# Send a file to Virustotal for analysis
+resp = vtotal.file_scan("./tests.py")  # PATH to file for querying
 
-# NOTE: This endpoint has been removed from the Public Virustotal API.
-# Resend a file to Virustotal for analysis.
-# A list containing the resource (SHA256) HASH of the file above.
-#resp = vtotal.file_rescan(
+# NOTE: This endpoint has been removed from the Public Virustotal API
+# Resend a file to Virustotal for analysis
+# A list containing the resource (SHA256) HASH of the file above
+# resp = vtotal.file_rescan(
 #    ["75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53"]
-#)
-## A list containing md5/sha1/sha256 hashes. Can be a combination of any of the three allowed hashes (MAX 25 items).
-## NOTE: The second hash here is flagged as malicious by multiple engines.
-#resp = vtotal.file_rescan(
+# )
+## A list containing md5/sha1/sha256 hashes. Can be a combination of any of the three allowed hashes (MAX 25 items)
+## NOTE: The second hash here is flagged as malicious by multiple engines
+# resp = vtotal.file_rescan(
 #    [
 #        "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53",
 #        "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115",
 #    ]
-#)
+# )
 
-# Retrieve scan report(s) for given file(s) from Virustotal.
-# A list containing the resource (SHA256) HASH of a known malicious file.
+# Retrieve scan report(s) for given file(s) from Virustotal
+# A list containing the resource (SHA256) HASH of a known malicious file
 resp = vtotal.file_report(
     ["9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"]
 )
-# A list of resource(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate).
-# The first is a scan_id, the second is a SHA256 HASH.
+# A list of resource(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate)
+# The first is a scan_id, the second is a SHA256 HASH
 resp = vtotal.file_report(
     [
         "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539",
@@ -48,19 +56,19 @@
     ]
 )
 
-# Query url(s) to VirusTotal.
-# A list containing a url to be scanned by VirusTotal.
-resp = vtotal.url_scan(["ihaveaproblem.info"])  # Query a single url.
-# A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate).
+# Query url(s) to VirusTotal
+# A list containing a url to be scanned by VirusTotal
+resp = vtotal.url_scan(["ihaveaproblem.info"])  # Query a single url
+# A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate)
 resp = vtotal.url_scan(
     ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"]
 )
 
 # 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.
-# 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.
+# A list containing the url of the report to be retrieved
+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(
     [
         "fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1555352750",
@@ -71,13 +79,13 @@
     scan=1,
 )
 
-# Query an IP to Virustotal.
+# Query an IP to Virustotal
 resp = vtotal.ipaddress_report("90.156.201.27")
 
-# Retrieve a domain report.
+# Retrieve a domain report
 resp = vtotal.domain_report("027.ru")
 
-# Put a comment onto a specific resource.
+# Put a comment onto a specific resource
 resp = vtotal.put_comment(
     "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115",
     comment="#watchout, this looks very malicious!",
diff --git a/virustotal_python/virustotal.py b/virustotal_python/virustotal.py
index a50ef51..83ae236 100644
--- a/virustotal_python/virustotal.py
+++ b/virustotal_python/virustotal.py
@@ -1,7 +1,7 @@
 """
 MIT License
 
-Copyright (c) 2020 dbrennand (dbrennand)
+Copyright (c) 2020 dbrennand
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
@@ -19,167 +19,366 @@
 AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-SOFTWARE."""
-try:
-    from requests import get, post
-    from os.path import abspath, basename
-except ImportError as err:
-    print(f"Failed to import required modules: {err}")
+SOFTWARE.
+"""
+import requests
+import os
+from typing import Tuple
+from json.decoder import JSONDecodeError
+
+
+class VirustotalError(Exception):
+    """
+    Class for VirusTotal API errors.
+    """
+
+    def __init__(self, response: requests.Response):
+        """
+        Initalisation for VirustotalError class.
+
+        :param response: A requests.Response object 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 error(self) -> dict:
+        """
+        Retrieve the error that occurred from a VirusTotal API request.
+
+        [v3 documentation](https://developers.virustotal.com/v3.0/reference#errors)
+
+        [v2 documentation](https://developers.virustotal.com/reference#api-responses)
+
+        :returns: A dictionary 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
+        try:
+            return self.response.json().get("error", dict())
+        except ValueError:
+            # Catch exception if there is no JSON to be deserialized
+            # Most likely using the v2 VirusTotal API
+            # Check there is response text, if not, return an empty dict
+            if self.response.text:
+                return dict(message=self.response.text)
+            else:
+                return dict()
+
+
+class VirustotalResponse(object):
+    """
+    Response class for VirusTotal API requests.
+    """
+
+    def __init__(self, response: requests.Response):
+        """
+        Initalisation for VirustotalResponse class.
+
+        :param response: A requests.Response object from a successfull API request to the VirusTotal API.
+        """
+        self.response = response
+
+    @property
+    def headers(self) -> dict:
+        """
+        Retrieve the HTTP headers of a VirusTotal API request.
+
+        :returns: 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.
+
+        :returns: 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.
+
+        :returns: 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.
+        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.
+        """
+        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.
+
+        NOTE: Links are not retrieved for objects inside 'data'.
+
+        [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+
+        :returns: A dictionary containing 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.
+
+        [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+
+        :returns: A dictionary containing 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.
+
+        [v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+
+        :returns: A string representing the cursor used to retrieve additional 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
+        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.
+
+        [v3 documentation](https://developers.virustotal.com/v3.0/reference#objects)
+
+        :returns: A dictionary or list depending on the number of objects returned from the VirusTotal API (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.
+
+        [v3 documentation](https://developers.virustotal.com/v3.0/reference#objects)
+
+        [More v3 documentation](https://developers.virustotal.com/v3.0/reference#collections)
+
+        :returns: A list or string depending on the number of objects returned from the VirusTotal API (if any) otherwise, returns None.
+        """
+        data = self.data
+        # Check if data is 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
+        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.
+
+        [v2 documentation](https://developers.virustotal.com/reference#api-responses)
+
+        :returns: An int of 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.
+
+        :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.
+        """
+        try:
+            return self.response.json(**kwargs)
+        except JSONDecodeError:
+            return dict()
 
 
 class Virustotal(object):
     """
-    Base class for interacting with the Virustotal Public API. (https://www.virustotal.com/en/documentation/public-api/)
+    Interact with the public VirusTotal v2 and v3 APIs.
+
+    [v2 documentation](https://www.virustotal.com/en/documentation/public-api/)
+
+    [v3 documentation](https://developers.virustotal.com/v3.0/reference)
     """
 
-    def __init__(self, API_KEY: str = None, PROXIES: dict = None):
+    def __init__(
+        self,
+        API_KEY: str = os.environ.get("VIRUSTOTAL_API_KEY", None),
+        API_VERSION: str = "v2",
+        COMPATIBILITY_ENABLED: bool = False,
+        PROXIES: dict = None,
+        TIMEOUT: float = None,
+    ):
+        """
+        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.1.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'."
+            )
         self.API_KEY = API_KEY
+        self.COMPATIBILITY_ENABLED = COMPATIBILITY_ENABLED
         self.PROXIES = PROXIES
-        self.BASEURL = "https://www.virustotal.com/vtapi/v2/"
-        self.VERSION = "0.0.9"
-        self.headers = {
-            "Accept-Encoding": "gzip, deflate",
-            "User-Agent": f"gzip,  virustotal-python {self.VERSION}",
-        }
-        if API_KEY is None:
+        self.TIMEOUT = TIMEOUT
+        # Declare appropriate variables depending on the API_VERSION provided
+        if API_VERSION == "v2":
+            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}",
+            }
+        elif API_VERSION == "v3":
+            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}",
+                "x-apikey": f"{self.API_KEY}",
+            }
+        else:
             raise ValueError(
-                "An API_KEY is required to interact with the VirusTotal API."
+                f"The API version '{API_VERSION}' is not a valid VirusTotal API version.\nValid API versions are 'v2' or 'v3'."
             )
 
-    def file_scan(self, file):
-        """
-        Send a file to Virustotal for analysis. (https://developers.virustotal.com/v2.0/reference#file-scan)
-           :param file: The path to the file to be sent to Virustotal for analysis.
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY}
-        files = {"file": (basename(file), open(abspath(file), "rb"))}
-        resp = self.make_request(
-            f"{self.BASEURL}file/scan", params=params, files=files, proxies=self.PROXIES
-        )
-        return resp
-
-    def file_rescan(self, *resource: list):
-        """
-        Resend a file to Virustotal for analysis. (https://developers.virustotal.com/v2.0/reference#file-rescan)
-           :param *resource: A list of resource(s) of a specified file(s). Can be `md5/sha1/sha256 hashes`. Can be a combination of any of the three allowed hashes (MAX 25 items).
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        raise DeprecationWarning("VirusTotal removed this API endpoint from the public API.")
-
-    def file_report(self, *resource: list):
-        """
-        Retrieve scan report(s) for a given file from Virustotal. (https://developers.virustotal.com/v2.0/reference#file-report)
-           :param *resource: A list of resource(s) of a specified file(s). Can be `md5/sha1/sha256 hashes` and/or combination of hashes and scan_ids (MAX 4 per standard request rate).
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY, "resource": ",".join(*resource)}
-        resp = self.make_request(
-            f"{self.BASEURL}file/report",
-            params=params,
-            method="GET",
-            proxies=self.PROXIES,
-        )
-        return resp
-
-    def url_scan(self, *url: list):
-        """
-        Send url(s) to Virustotal. (https://developers.virustotal.com/v2.0/reference#url-scan)
-           :param *url: A list of url(s) to be scanned. (MAX 4 per standard request rate).
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY, "url": "\n".join(*url)}
-        resp = self.make_request(
-            f"{self.BASEURL}url/scan", params=params, proxies=self.PROXIES
-        )
-        return resp
-
-    def url_report(self, *resource: list, scan: int = None):
-        """
-        Retrieve scan report(s) for a given url(s) (https://developers.virustotal.com/v2.0/reference#url-report)
-           :param *resource: A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate).
-           :param scan: An optional parameter. When set to 1 it will automatically submit the URL for analysis if no report is found for it in VirusTotal's database.
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY, "resource": "\n".join(*resource)}
-        if scan is not None:
-            params["scan"] = scan
-        resp = self.make_request(
-            f"{self.BASEURL}url/report", params=params, proxies=self.PROXIES
-        )
-        return resp
-
-    def ipaddress_report(self, ip: str):
-        """
-        Retrieve a scan report for a specific ip address. (https://developers.virustotal.com/v2.0/reference#ip-address-report)
-           :param ip: A valid IPV4 address in dotted quad notation.
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY, "ip": ip}
-        resp = self.make_request(
-            f"{self.BASEURL}ip-address/report",
-            params=params,
-            method="GET",
-            proxies=self.PROXIES,
-        )
-        return resp
-
-    def domain_report(self, domain: str):
-        """
-        Retrieve a scan report for a specific domain name. (https://developers.virustotal.com/v2.0/reference#domain-report)
-           :param domain: A domain name.
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY, "domain": domain}
-        resp = self.make_request(
-            f"{self.BASEURL}domain/report",
-            params=params,
-            method="GET",
-            proxies=self.PROXIES,
-        )
-        return resp
-
-    def put_comment(self, resource: str, comment: str):
-        """
-        Make comments on files and URLs. (https://developers.virustotal.com/v2.0/reference#comments-put)
-           :param resource: The `md5/sha1/sha256 hash` of the file you want to review or the URL itself that you want to comment on.
-           :param comment: The str comment to be submitted.
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        params = {"apikey": self.API_KEY, "resource": resource, "comment": comment}
-        resp = self.make_request(
-            f"{self.BASEURL}comments/put", params=params, proxies=self.PROXIES
-        )
-        return resp
-
-    def make_request(self, endpoint: str, params: dict, method="POST", **kwargs):
-        """
-        Helper function to make the request to the specified endpoint.
-           :param endpoint: The specific Virustotal API endpoint.
-           :param method: The request method to use.
-           :param params: The parameters to go along with the request.
-           :rtype: A dictionary containing the resp_code and JSON response.
-        """
-        if method == "POST":
-            resp = post(endpoint, params=params, headers=self.headers, **kwargs)
-        elif method == "GET":
-            resp = get(endpoint, params=params, headers=self.headers, **kwargs)
+    def request(
+        self,
+        resource: str,
+        params: dict = {},
+        data: dict = None,
+        json: dict = None,
+        files: dict = None,
+        method: str = "GET",
+    ) -> 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.
+        :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.
+        """
+        # Create API endpoint
+        endpoint = f"{self.BASEURL}{resource}"
+        # If API version being used is v2, add the API key to params
+        if self.API_VERSION == "v2":
+            params["apikey"] = self.API_KEY
+        if method == "GET":
+            response = requests.get(
+                endpoint,
+                params=params,
+                data=data,
+                json=json,
+                files=files,
+                headers=self.HEADERS,
+                proxies=self.PROXIES,
+                timeout=self.TIMEOUT,
+            )
+        elif method == "POST":
+            response = requests.post(
+                endpoint,
+                params=params,
+                data=data,
+                json=json,
+                files=files,
+                headers=self.HEADERS,
+                proxies=self.PROXIES,
+                timeout=self.TIMEOUT,
+            )
+        elif method == "PATCH":
+            response = requests.patch(
+                endpoint,
+                params=params,
+                data=data,
+                json=json,
+                files=files,
+                headers=self.HEADERS,
+                proxies=self.PROXIES,
+                timeout=self.TIMEOUT,
+            )
+        elif method == "DELETE":
+            response = requests.delete(
+                endpoint,
+                params=params,
+                data=data,
+                json=json,
+                files=files,
+                headers=self.HEADERS,
+                proxies=self.PROXIES,
+                timeout=self.TIMEOUT,
+            )
         else:
-            raise ValueError("Invalid request method.")
-        return self.validate_response(resp)
+            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):
+    def validate_response(
+        self, response: requests.Response
+    ) -> Tuple[dict, VirustotalResponse]:
         """
-        Helper function to validate the response request produced from make_request().
-           :param response: The requests response object.
-           :rtype: A dictionary containing the resp_code and JSON response.
+        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 response.status_code == 200:
-            json_resp = response.json()
-            return dict(status_code=response.status_code, json_resp=json_resp)
+        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,
+                )
         else:
-            return dict(
-                status_code=response.status_code,
-                error=response.text,
-                resp=response.content,
-            )
+            if response.status_code != 200:
+                raise VirustotalError(response)
+            else:
+                return VirustotalResponse(response)


Test patch:
diff --git a/virustotal_python/tests.py b/virustotal_python/tests.py
index 80b8638..b90a92f 100644
--- a/virustotal_python/tests.py
+++ b/virustotal_python/tests.py
@@ -1,70 +1,297 @@
-from virustotal_python import Virustotal
-from time import sleep
+import virustotal_python
 import pytest
+import os.path
+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 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 virustotal_object(request):
-    API_KEY = "Insert API Key Here."
-    yield Virustotal(API_KEY)
+@pytest.fixture()
+def vtotal_v3(request):
+    yield virustotal_python.Virustotal(API_VERSION="v3")
 
     def fin():
         """
-        Sleep for 30 seconds after each test; to avoid Virustotal 403 rate quota limit.
+        Helper function which sleeps for 15 seconds between each test. This is to avoid VirusTotal 403 rate quota limits.
         """
-        print("Sleeping for 30 seconds...")
-        sleep(30)
+        print("Sleeping for 15 seconds to avoid VirusTotal 403 rate quota limits...")
+        sleep(15)
 
     request.addfinalizer(fin)
 
 
-def assert_content(resp):
+def test_file_scan_v2(vtotal_v2):
     """
-    Check json_resp data which is nested.
-        :param content: The nested json_resp object.
+    Test for sending a file to the VirusTotal v2 API for analysis.
     """
-    for content in enumerate(resp["json_resp"]):
-        assert content[1]["response_code"] == 1
+    # 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"),
+        )
+    }
+    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(virustotal_object):
-    resp = virustotal_object.file_scan("./examples.py")
-    assert resp["status_code"] == 200
-    assert resp["json_resp"]["response_code"] == 1
+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_report(virustotal_object):
-    resp = virustotal_object.file_report(
-        [
-            "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539",
-            "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115",
-        ]
-    )
+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_content(resp)
+    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_url_scan(virustotal_object):
-    resp = virustotal_object.url_scan(
-        ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"]
+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
     )
-    assert resp["status_code"] == 200
-    assert_content(resp)
 
 
-def test_url_report(virustotal_object):
-    resp = virustotal_object.url_report(["ihaveaproblem.info"], scan=1)
-    assert resp["status_code"] == 200
-    assert resp["json_resp"]["response_code"] == 1
+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_ipaddress_report(virustotal_object):
-    resp = virustotal_object.ipaddress_report("90.156.201.27")
-    assert resp["status_code"] == 200
-    assert resp["json_resp"]["response_code"] == 1
+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_domain_report(virustotal_object):
-    resp = virustotal_object.domain_report("027.ru")
-    assert resp["status_code"] == 200
-    assert resp["json_resp"]["response_code"] == 1
+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 LLC"
+    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 LLC"
+    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()