├── .github └── workflows │ ├── ci.yml │ └── publish.yml ├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── examples ├── comments.py ├── cursor.py ├── domain_info.py ├── file_info.py ├── graphs.py ├── ip.py ├── old_examples.py ├── scan_file.py ├── scan_urls.py └── searchmetadata.py ├── images └── APIKey.png ├── poetry.lock ├── pyproject.toml ├── tests ├── __init__.py ├── example.json └── test_virustotal_python.py └── virustotal_python ├── __init__.py └── virustotal.py /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | - pull_request 4 | - push 5 | 6 | jobs: 7 | ci: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Check out repository 11 | uses: actions/checkout@v4 12 | 13 | - name: Set up Python 14 | id: setup-python 15 | uses: actions/setup-python@v5 16 | with: 17 | python-version: 3.12 18 | 19 | - name: Load cached Poetry installation 20 | id: cached-poetry 21 | uses: actions/cache@v3 22 | with: 23 | path: ~/.local 24 | key: poetry-0 25 | 26 | - name: Install Poetry 27 | if: steps.cached-poetry.outputs.cache-hit != 'true' 28 | uses: snok/install-poetry@v1 29 | with: 30 | virtualenvs-create: true 31 | virtualenvs-in-project: true 32 | installer-parallel: true 33 | 34 | - name: Load cached venv 35 | id: cached-poetry-dependencies 36 | uses: actions/cache@v3 37 | with: 38 | path: .venv 39 | key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('**/poetry.lock') }} 40 | 41 | - name: Install dependencies if cache does not exist 42 | if: steps.cached-poetry-dependencies.outputs.cache-hit != 'true' 43 | run: poetry install --no-interaction --no-root 44 | 45 | - name: Install library 46 | run: poetry install --no-interaction 47 | 48 | - name: Run tests 49 | run: | 50 | source .venv/bin/activate 51 | black . --check 52 | pytest tests/ --cov=virustotal_python 53 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | on: 3 | push: 4 | tags: 5 | - "*.*.*" 6 | 7 | jobs: 8 | Publish: 9 | if: github.repository == 'dbrennand/virustotal-python' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags') 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Check out repository 13 | uses: actions/checkout@v4 14 | 15 | - name: Set up Python 16 | uses: actions/setup-python@v5 17 | with: 18 | python-version: 3.12 19 | 20 | - name: Load cached Poetry installation 21 | id: cached-poetry 22 | uses: actions/cache@v3 23 | with: 24 | path: ~/.local 25 | key: poetry-0 26 | 27 | - name: Install Poetry 28 | if: steps.cached-poetry.outputs.cache-hit != 'true' 29 | uses: snok/install-poetry@v1 30 | 31 | - name: Configure Poetry 32 | run: poetry config pypi-token.pypi "${{ secrets.PYPI_API_KEY }}" 33 | 34 | - name: Publish virustotal-python 35 | run: poetry publish --build 36 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | MANIFEST 27 | run.py 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .coverage 43 | .coverage.* 44 | .cache 45 | nosetests.xml 46 | coverage.xml 47 | *.cover 48 | .hypothesis/ 49 | .pytest_cache/ 50 | 51 | # Translations 52 | *.mo 53 | *.pot 54 | 55 | # Django stuff: 56 | *.log 57 | local_settings.py 58 | db.sqlite3 59 | 60 | # Flask stuff: 61 | instance/ 62 | .webassets-cache 63 | 64 | # Scrapy stuff: 65 | .scrapy 66 | 67 | # Sphinx documentation 68 | docs/_build/ 69 | 70 | # PyBuilder 71 | target/ 72 | 73 | # Jupyter Notebook 74 | .ipynb_checkpoints 75 | 76 | # pyenv 77 | .python-version 78 | 79 | # celery beat schedule file 80 | celerybeat-schedule 81 | 82 | # SageMath parsed files 83 | *.sage.py 84 | 85 | # Environments 86 | .env 87 | .venv 88 | env/ 89 | venv/ 90 | ENV/ 91 | env.bak/ 92 | venv.bak/ 93 | 94 | # Spyder project settings 95 | .spyderproject 96 | .spyproject 97 | 98 | # Rope project settings 99 | .ropeproject 100 | 101 | # mkdocs documentation 102 | /site 103 | 104 | # mypy 105 | .mypy_cache/ 106 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.1.0 4 | 5 | * chore(deps): Update all dependencies. 6 | * chore: update license year. 7 | 8 | ## 1.0.2 9 | 10 | * chore(deps): Update all dependencies. 11 | 12 | ## 1.0.1 13 | 14 | * chore(deps): Update all dependencies. 15 | 16 | ## 1.0.0 17 | 18 | ### Breaking Changes 19 | 20 | * The `Virustotal` class now defaults to version 3 of the VirusTotal API. 21 | 22 | * Dropped support for `COMPATIBILITY_ENABLED` parameter on the `Virustotal` class. 23 | 24 | ### Docs 25 | 26 | * Updated README content including instructions for running tests and improve general readability. 27 | 28 | * Moved to using Google docstring format. 29 | 30 | * Improved type hints. 31 | 32 | * Refactored examples to favour version 3 of the VirusTotal API. 33 | 34 | * Added a proper CHANGELOG. 35 | 36 | ### Tests 37 | 38 | * Added new unit tests with 95% coverage. 39 | 40 | ### Misc Changes 41 | 42 | * `API_VERSION` can now accept an `int` to specify VirusTotal API version to use. 43 | 44 | * Add GitHub actions workflows for automated testing and publishing. 45 | 46 | ## 0.2.0 47 | 48 | Added `large_file` parameter to `request` so a file larger than 32MB can be submitted for analysis. See [#33](https://github.com/dbrennand/virustotal-python/pull/33). Thank you @smk762. 49 | 50 | ## 0.1.3 51 | 52 | Update urllib3 to 1.26.5 to address [CVE-2021-33503](https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2021-33503). 53 | 54 | ## 0.1.2 55 | 56 | Update dependencies for security vulnerability. Fixed an issue with some tests failing. 57 | 58 | ## 0.1.1 59 | 60 | Added Context Manager support and tests. Updated dependencies and license year. 61 | 62 | ## 0.1.0 63 | 64 | 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). 65 | 66 | ## 0.0.9 67 | 68 | Update dependencies for security vulnerability. 69 | 70 | ## 0.0.8 71 | 72 | Updated dependencies, removed method `file_rescan` 73 | 74 | ## 0.0.7 75 | 76 | Added tests. Updated dependencies, Updated examples and README, `url_report` param `scan` now accepts `type(int)`, no longer `type(str)` 77 | 78 | ## 0.0.6 79 | 80 | Fixed usage example and dependencies in README.md, Setup github.io website, updated requirements.txt. 81 | 82 | ## 0.0.5 83 | 84 | Added proxy support. Via HTTP(S) or using SOCKS: See [#8](https://github.com/dbrennand/virustotal-python/pull/8). 85 | 86 | ## 0.0.4 87 | 88 | README.md updated; dependencies updated. 89 | 90 | ## 0.0.3 91 | 92 | Updated dependencies for `urllib3` security vulnerability. 93 | 94 | ## 0.0.2 95 | 96 | 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. 97 | 98 | ## 0.0.1 99 | 100 | Initial release of virustotal-python. Covered all endpoints of the Virustotal public API. 101 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Daniel Brennand 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # virustotal-python 🐍 2 | ![PyPI](https://img.shields.io/pypi/v/virustotal-python.svg?style=flat-square) 3 | ![PyPI Stats](https://img.shields.io/pypi/dm/virustotal-python?color=blue&style=flat-square) 4 | [![CI](https://github.com/dbrennand/virustotal-python/actions/workflows/ci.yml/badge.svg)](https://github.com/dbrennand/virustotal-python/actions/workflows/ci.yml) 5 | [![Publish](https://github.com/dbrennand/virustotal-python/actions/workflows/publish.yml/badge.svg)](https://github.com/dbrennand/virustotal-python/actions/workflows/publish.yml) 6 | 7 | A Python library to interact with the public VirusTotal v3 and v2 APIs. 8 | 9 | > 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. 10 | > 11 | > It is highly recommended that you use the VirusTotal v3 API as it is the "default and encouraged way to programmatically interact with VirusTotal". 12 | 13 | ## Installation 🛠 14 | 15 | ```bash 16 | # PyPi 17 | pip install virustotal-python 18 | # Manually 19 | pip install . 20 | # Poetry 21 | poetry install --no-dev 22 | ``` 23 | 24 | ## Get a VirusTotal API Key 🔑 25 | 26 | [Sign up](https://www.virustotal.com/gui/join-us) for a VirusTotal account. Then, view your VirusTotal API key. 27 | 28 | ![VirusTotal view API key](images/APIKey.png) 29 | 30 | ## Getting Started 31 | 32 | ```python 33 | import virustotal_python 34 | 35 | with virustotal_python.Virustotal("") as vtotal: 36 | # Your code here... 37 | 38 | # Use the (old) VirusTotal version 2 API 39 | with virustotal_python.Virustotal( 40 | API_KEY="", API_VERSION=2 41 | ) as vtotal: 42 | # Your code here... 43 | 44 | # You can also set proxies and timeouts for requests made by the library 45 | # NOTE: To use proxies, you must have the PySocks extra installed 46 | with virustotal_python.Virustotal( 47 | API_KEY="", 48 | PROXIES={"http": "http://10.10.1.10:3128", "https": "https://10.10.1.10:1080"}, 49 | TIMEOUT=5.0, 50 | ) as vtotal: 51 | # Your code here... 52 | 53 | # You can also omit the API_KEY parameter and provide your 54 | # API key via the environment variable VIRUSTOTAL_API_KEY 55 | # Bash: export VIRUSTOTAL_API_KEY="" 56 | # PowerShell: $Env:VIRUSTOTAL_API_KEY = "" 57 | # Then... 58 | with virustotal_python.Virustotal() as vtotal: 59 | # Your code here... 60 | ``` 61 | 62 | ## Code Snippets 63 | 64 | > Further usage examples can be found in [examples](examples). 65 | 66 | ### Send a file for analysis 🔎 67 | 68 | ```python 69 | import virustotal_python 70 | import os.path 71 | from pprint import pprint 72 | 73 | FILE_PATH = "/path/to/file/to/scan.txt" 74 | 75 | # Create dictionary containing the file to send for multipart encoding upload 76 | files = {"file": (os.path.basename(FILE_PATH), open(os.path.abspath(FILE_PATH), "rb"))} 77 | 78 | with virustotal_python.Virustotal("") as vtotal: 79 | resp = vtotal.request("files", files=files, method="POST") 80 | pprint(resp.json()) 81 | ``` 82 | 83 | ### Get information about a file 📁 84 | 85 | ```python 86 | import virustotal_python 87 | from pprint import pprint 88 | 89 | # The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file 90 | FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" 91 | 92 | with virustotal_python.Virustotal("") as vtotal: 93 | resp = vtotal.request(f"files/{FILE_ID}") 94 | pprint(resp.data) 95 | ``` 96 | 97 | ### Send a URL 🔗 for analysis and get the report 📄 98 | 99 | ```python 100 | import virustotal_python 101 | from pprint import pprint 102 | from base64 import urlsafe_b64encode 103 | 104 | url = "ihaveaproblem.info" 105 | 106 | with virustotal_python.Virustotal("") as vtotal: 107 | try: 108 | resp = vtotal.request("urls", data={"url": url}, method="POST") 109 | # Safe encode URL in base64 format 110 | # https://developers.virustotal.com/reference/url 111 | url_id = urlsafe_b64encode(url.encode()).decode().strip("=") 112 | report = vtotal.request(f"urls/{url_id}") 113 | pprint(report.object_type) 114 | pprint(report.data) 115 | except virustotal_python.VirustotalError as err: 116 | print(f"Failed to send URL: {url} for analysis and get the report: {err}") 117 | ``` 118 | 119 | ### Get information about a domain: 120 | 121 | ```python 122 | import virustotal_python 123 | from pprint import pprint 124 | 125 | domain = "virustotal.com" 126 | 127 | with virustotal_python.Virustotal("") as vtotal: 128 | resp = vtotal.request(f"domains/{domain}") 129 | pprint(resp.data) 130 | ``` 131 | 132 | ## Development 133 | 134 | [Black](https://github.com/psf/black) is used for code formatting. 135 | 136 | ### Unit Tests 137 | 138 | Install the development dependencies using Poetry: 139 | 140 | ```bash 141 | poetry install && poetry shell 142 | ``` 143 | 144 | To run the unit tests, run `pytest` from the root of the project: 145 | 146 | ```bash 147 | pytest --cov=virustotal_python 148 | ``` 149 | 150 | ### Publishing a new release 151 | 152 | ```bash 153 | # Run from the master branch 154 | export VERSION=x.x.x 155 | git commit --allow-empty -m "Publish $VERSION" 156 | git tag -a $VERSION -m "Version $VERSION" 157 | git push --tags 158 | ``` 159 | 160 | ## Authors & Contributors 161 | 162 | * [**dbrennand**](https://github.com/dbrennand) - *Author* 163 | 164 | * [**smk762**](https://github.com/smk762) - *Contributor* 165 | 166 | ## Changelog 167 | 168 | See the [CHANGELOG](CHANGELOG.md) for details. 169 | 170 | ## License 171 | This project is licensed under the MIT License - see the [LICENSE](LICENSE) for details. 172 | -------------------------------------------------------------------------------- /examples/comments.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Retrieve comments and interact with them using the VirusTotal API. 5 | 6 | Documentation: 7 | 8 | * v3 documentation 9 | 10 | https://developers.virustotal.com/reference/comments 11 | https://developers.virustotal.com/reference/get-comments 12 | https://developers.virustotal.com/reference/get-comment 13 | https://developers.virustotal.com/reference/comment-id-patch 14 | https://developers.virustotal.com/reference/comment-id-delete 15 | https://developers.virustotal.com/reference/vote-comment 16 | 17 | * v2 documentation 18 | 19 | https://developers.virustotal.com/v2.0/reference/comments-get 20 | https://developers.virustotal.com/v2.0/reference/comments-put 21 | """ 22 | 23 | from virustotal_python import Virustotal 24 | from base64 import urlsafe_b64encode 25 | 26 | API_KEY = "" 27 | # The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file 28 | FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" 29 | DOMAIN = "google.com" 30 | # Get the domain ID 31 | URL_ID = urlsafe_b64encode("https://github.com/home".encode()).decode().strip("=") 32 | # (Google DNS) 33 | IP = "8.8.8.8" 34 | # There are no comments on this graph so an empty list is returned 35 | GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d" 36 | COMMENT_ID = ( 37 | "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-07457619" 38 | ) 39 | 40 | # v3 examples 41 | vtotal = Virustotal(API_KEY=API_KEY) 42 | # Get comments for resources 43 | # Get 10 comments for a file 44 | resp = vtotal.request(f"files/{FILE_ID}/comments", params={"limit": 10}) 45 | # Get 2 comments for a URL 46 | resp = vtotal.request(f"urls/{URL_ID}/comments", params={"limit": 2}) 47 | # Get 2 comments for a domain 48 | resp = vtotal.request(f"domains/{DOMAIN}/comments", params={"limit": 2}) 49 | # Get 5 comments for an IP address 50 | resp = vtotal.request(f"ip_addresses/{IP}/comments", params={"limit": 5}) 51 | # Get 3 comments for a graph 52 | resp = vtotal.request(f"graphs/{GRAPH_ID}/comments", params={"limit": 3}) 53 | 54 | comment = { 55 | "data": { 56 | "type": "comment", 57 | "attributes": {"text": "Watchout! This looks dangerous!"}, 58 | } 59 | } 60 | 61 | # Submit comments on a resource 62 | # Submit a comment on a file 63 | resp = vtotal.request(f"files/{FILE_ID}/comments", json=comment, method="POST") 64 | # Submit a comment on a URL 65 | resp = vtotal.request(f"urls/{URL_ID}/comments", json=comment, method="POST") 66 | # Submit a comment on a domain 67 | resp = vtotal.request(f"domains/{DOMAIN}/comments", json=comment, method="POST") 68 | # Submit a comment on a IP address 69 | resp = vtotal.request(f"ip_addresses/{IP}/comments", json=comment, method="POST") 70 | # Submit a comment on a graph 71 | resp = vtotal.request(f"graphs/{GRAPH_ID}/comments", json=comment, method="POST") 72 | 73 | # Get the 10 latest comments added to VirusTotal 74 | resp = vtotal.request("comments", params={"limit": 10}) 75 | # Get the 10 latest comments added to VirusTotal, filtering for Remote Access Trojan (RAT) 76 | resp = vtotal.request("comments", params={"limit": 10, "filter": "rat"}) 77 | # Get a comment based on the ID 78 | resp = vtotal.request(f"comments/{COMMENT_ID}") 79 | 80 | edited_comment = { 81 | "data": { 82 | "type": "comment", 83 | "attributes": {"text": "#watchout, this looks quite malicious!"}, 84 | } 85 | } 86 | # Edit a comment based on the ID 87 | resp = vtotal.request(f"comments/{COMMENT_ID}", json=edited_comment, method="PATCH") 88 | # Delete a comment based on the ID 89 | resp = vtotal.request(f"comments/{COMMENT_ID}", method="DELETE") 90 | # Submit a vote for a comment 91 | # Vote options can be either positive, negative or abuse 92 | resp = vtotal.request( 93 | f"comments/{COMMENT_ID}/vote", json={"data": "positive"}, method="POST" 94 | ) 95 | 96 | # v2 examples 97 | vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2) 98 | # Get comments for a given file ID 99 | resp = vtotal.request("comments/get", params={"resource": FILE_ID}) 100 | # Create a comment for a given file ID 101 | resp = vtotal.request( 102 | "comments/put", 103 | params={"resource": FILE_ID, "comment": "Wow, this looks like a #malicious file!"}, 104 | method="POST", 105 | ) 106 | -------------------------------------------------------------------------------- /examples/cursor.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Use a cursor from the VirusTotal API JSON response to retrieve more results. 5 | 6 | Documentation: 7 | 8 | * v3 documentation - https://developers.virustotal.com/reference/collections 9 | """ 10 | 11 | from virustotal_python import Virustotal 12 | 13 | API_KEY = "" 14 | # (Google DNS) 15 | IP = "8.8.8.8" 16 | 17 | # v3 example 18 | vtotal = Virustotal(API_KEY=API_KEY) 19 | 20 | # Get communicating_files related to the IP address with a limit of 2 21 | resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 2}) 22 | 23 | count = 0 24 | # While a cursor is present, keep collecting results! 25 | while resp.cursor: 26 | print(f"Current count: {count} - Cursor: {resp.cursor}") 27 | # Get more results using the cursor 28 | resp = vtotal.request( 29 | f"ip_addresses/{IP}/communicating_files", 30 | params={"limit": 2, "cursor": resp.cursor}, 31 | ) 32 | # Do something with the resp here 33 | # Add to the count to show how many times we have retrieved another cursor 34 | count += 1 35 | -------------------------------------------------------------------------------- /examples/domain_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Retrieve information about a domain from the VirusTotal API. 5 | 6 | Documentation: 7 | 8 | * v3 documentation - https://developers.virustotal.com/reference/domain-info 9 | 10 | * v2 documentation - https://developers.virustotal.com/v2.0/reference/domain-report 11 | """ 12 | 13 | from virustotal_python import Virustotal 14 | from pprint import pprint 15 | 16 | API_KEY = "" 17 | 18 | domain = "virustotal.com" 19 | 20 | # v3 example 21 | vtotal = Virustotal(API_KEY=API_KEY) 22 | resp = vtotal.request(f"domains/{domain}") 23 | pprint(resp.data) 24 | 25 | # v2 example 26 | vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2) 27 | resp = vtotal.request("domain/report", params={"domain": domain}) 28 | print(resp.response_code) 29 | pprint(resp.json()) 30 | -------------------------------------------------------------------------------- /examples/file_info.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Retrieve information about a file from the VirusTotal API. 5 | 6 | Documentation: 7 | 8 | * v3 documentation - https://developers.virustotal.com/reference/file-info 9 | 10 | * v2 documentation - https://developers.virustotal.com/v2.0/reference/file-report 11 | """ 12 | 13 | from virustotal_python import Virustotal 14 | from pprint import pprint 15 | 16 | API_KEY = "" 17 | 18 | # The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file 19 | FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" 20 | 21 | # v3 example 22 | vtotal = Virustotal(API_KEY=API_KEY) 23 | resp = vtotal.request(f"files/{FILE_ID}") 24 | pprint(resp.data) 25 | 26 | # v2 example 27 | vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2) 28 | resp = vtotal.request("file/report", {"resource": FILE_ID}) 29 | print(resp.response_code) 30 | pprint(resp.json()) 31 | -------------------------------------------------------------------------------- /examples/graphs.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Retrieve graphs and interact with them using the VirusTotal v3 API. 5 | 6 | Documentation: 7 | 8 | * v3 documentation - https://developers.virustotal.com/reference/graphs 9 | """ 10 | 11 | from virustotal_python import Virustotal 12 | 13 | API_KEY = "" 14 | 15 | # Example ID of a graph 16 | GRAPH_ID = "g70fae134aefc4e2f90f069aba47d15a92e0073564310443aa0b6ca3384f5240d" 17 | 18 | # v3 examples 19 | vtotal = Virustotal(API_KEY=API_KEY) 20 | 21 | # Get 3 graphs from the VirusTotal API 22 | resp = vtotal.request("graphs", params={"limit": 3}) 23 | # Get 3 graphs from the VirusTotal API filtering by owner, order and attributes 24 | resp = vtotal.request( 25 | "graphs", 26 | params={ 27 | "limit": 2, 28 | "filter": "owner:hugoklugman", 29 | "order": "views_count", 30 | "attributes": "graph_data", 31 | }, 32 | ) 33 | # Get a graph using the graph's ID 34 | resp = vtotal.request(f"graphs/{GRAPH_ID}") 35 | 36 | # To create a graph, head to https://www.virustotal.com/graph/ 37 | -------------------------------------------------------------------------------- /examples/ip.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Retrieve IP addresses using the VirusTotal API. 5 | 6 | Documentation: 7 | 8 | * v3 documentation 9 | 10 | https://developers.virustotal.com/reference/ip-info 11 | https://developers.virustotal.com/reference/ip-object#relationships 12 | https://developers.virustotal.com/reference/ip-votes-post 13 | 14 | * v2 documentation - https://developers.virustotal.com/v2.0/reference/ip-address-report 15 | """ 16 | 17 | from virustotal_python import Virustotal 18 | from pprint import pprint 19 | 20 | API_KEY = "" 21 | 22 | # (Google DNS) 23 | IP = "8.8.8.8" 24 | 25 | # v3 examples 26 | vtotal = Virustotal(API_KEY=API_KEY) 27 | 28 | # Get information about an IP address 29 | resp = vtotal.request(f"ip_addresses/{IP}") 30 | # Get objects (relationships) related to an IP address 31 | # Get historical_whois relationship to the IP address 32 | resp = vtotal.request(f"ip_addresses/{IP}/historical_whois") 33 | # Get communicating_files related to the IP address with a limit of 5 34 | resp = vtotal.request(f"ip_addresses/{IP}/communicating_files", params={"limit": 5}) 35 | 36 | # Get votes for an IP address 37 | resp = vtotal.request(f"ip_addresses/{IP}/votes") 38 | # Add a vote for an IP address 39 | # Verdict can be either harmless or malicious 40 | vote = {"data": {"type": "vote", "attributes": {"verdict": "harmless"}}} 41 | resp = vtotal.request(f"ip_addresses/{IP}/votes", json=vote, method="POST") 42 | 43 | # v2 examples 44 | vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2) 45 | # Get information about an IP address 46 | resp = vtotal.request("ip-address/report", params={"ip": IP}) 47 | pprint(resp.json()) 48 | -------------------------------------------------------------------------------- /examples/old_examples.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples below are for virustotal-python versions <0.1.0. 3 | 4 | Examples for versions >=0.1.0 head to /examples directory. 5 | """ 6 | 7 | from virustotal_python import Virustotal 8 | from pprint import pprint 9 | import os.path 10 | 11 | # Normal Initialisation 12 | vtotal = Virustotal("Insert API key here.") 13 | 14 | # NEW as of version 0.0.5: Proxy support 15 | # Example Usage: Using HTTP(S) 16 | vtotal = Virustotal( 17 | "Insert API key here.", 18 | {"http": "http://10.10.1.10:3128", "https": "http://10.10.1.10:1080"}, 19 | ) 20 | # Or using SOCKS 21 | vtotal = Virustotal( 22 | "Insert API key here.", 23 | {"http": "socks5://user:pass@host:port", "https": "socks5://user:pass@host:port"}, 24 | ) 25 | 26 | # NOTE: Check virustotal.py for docstrings containing full parameter descriptions 27 | 28 | # Send a file to Virustotal for analysis 29 | resp = vtotal.file_scan("./tests.py") # PATH to file for querying 30 | 31 | # NOTE: This endpoint has been removed from the Public Virustotal API 32 | # Resend a file to Virustotal for analysis 33 | # A list containing the resource (SHA256) HASH of the file above 34 | # resp = vtotal.file_rescan( 35 | # ["75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53"] 36 | # ) 37 | ## A list containing md5/sha1/sha256 hashes. Can be a combination of any of the three allowed hashes (MAX 25 items) 38 | ## NOTE: The second hash here is flagged as malicious by multiple engines 39 | # resp = vtotal.file_rescan( 40 | # [ 41 | # "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53", 42 | # "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", 43 | # ] 44 | # ) 45 | 46 | # Retrieve scan report(s) for given file(s) from Virustotal 47 | # A list containing the resource (SHA256) HASH of a known malicious file 48 | resp = vtotal.file_report( 49 | ["9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115"] 50 | ) 51 | # 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) 52 | # The first is a scan_id, the second is a SHA256 HASH 53 | resp = vtotal.file_report( 54 | [ 55 | "75efd85cf6f8a962fe016787a7f57206ea9263086ee496fc62e3fc56734d4b53-1555351539", 56 | "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", 57 | ] 58 | ) 59 | 60 | # Query url(s) to VirusTotal 61 | # A list containing a url to be scanned by VirusTotal 62 | resp = vtotal.url_scan(["ihaveaproblem.info"]) # Query a single url 63 | # A list of url(s) to be scanned by VirusTotal (MAX 4 per standard request rate) 64 | resp = vtotal.url_scan( 65 | ["ihaveaproblem.info", "google.com", "wikipedia.com", "github.com"] 66 | ) 67 | 68 | # Retrieve url report(s) 69 | # A list containing the url of the report to be retrieved 70 | resp = vtotal.url_report(["ihaveaproblem.info"]) # Query a single url 71 | # A list of the url(s) and/or scan_id(s) report(s) to be retrieved (MAX 4 per standard request rate) 72 | # The first object in the list is a scan_id 73 | resp = vtotal.url_report( 74 | [ 75 | "fd21590d9df715452c8c000e1b5aa909c7c5ea434c2ddcad3f4ccfe9b0ee224e-1555352750", 76 | "google.com", 77 | "wikipedia.com", 78 | "github.com", 79 | ], 80 | scan=1, 81 | ) 82 | 83 | # Query an IP to Virustotal 84 | resp = vtotal.ipaddress_report("90.156.201.27") 85 | 86 | # Retrieve a domain report 87 | resp = vtotal.domain_report("027.ru") 88 | 89 | # Put a comment onto a specific resource 90 | resp = vtotal.put_comment( 91 | "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115", 92 | comment="#watchout, this looks very malicious!", 93 | ) 94 | 95 | pprint(resp) 96 | -------------------------------------------------------------------------------- /examples/scan_file.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Send a file to the VirusTotal API for analysis. 5 | 6 | Documentation: 7 | 8 | * v3 documentation 9 | 10 | https://developers.virustotal.com/reference/files-scan 11 | https://developers.virustotal.com/reference/files-upload-url 12 | 13 | * v2 documentation - https://developers.virustotal.com/v2.0/reference/file-scan 14 | """ 15 | 16 | from virustotal_python import Virustotal 17 | import os.path 18 | from pprint import pprint 19 | 20 | API_KEY = "" 21 | 22 | FILE_PATH = "/path/to/file/to/scan.txt" 23 | 24 | # Create dictionary containing the file to send for multipart encoding upload 25 | files = {"file": (os.path.basename(FILE_PATH), open(os.path.abspath(FILE_PATH), "rb"))} 26 | 27 | # v3 example 28 | vtotal = Virustotal(API_KEY=API_KEY) 29 | resp = vtotal.request("files", files=files, method="POST") 30 | pprint(resp.data) 31 | 32 | # v3 example for uploading a file larger than 32MB in size 33 | vtotal = Virustotal(API_KEY=API_KEY) 34 | # Create dictionary containing the large file to send for multipart encoding upload 35 | large_file = { 36 | "file": ( 37 | os.path.basename("/path/to/file/larger/than/32MB"), 38 | open(os.path.abspath("/path/to/file/larger/than/32MB"), "rb"), 39 | ) 40 | } 41 | # Get URL to send a large file 42 | upload_url = vtotal.request("files/upload_url").data 43 | # Submit large file to VirusTotal for analysis 44 | resp = vtotal.request(upload_url, files=large_file, method="POST", large_file=True) 45 | pprint(resp.data) 46 | 47 | # v2 example 48 | vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2) 49 | resp = vtotal.request("file/scan", files=files, method="POST") 50 | print(resp.response_code) 51 | pprint(resp.json()) 52 | -------------------------------------------------------------------------------- /examples/scan_urls.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Send URLs to the VirusTotal API for analysis and retrieve the analysis results. 5 | 6 | Documentation: 7 | 8 | * v3 documentation 9 | 10 | https://developers.virustotal.com/reference/scan-url 11 | https://developers.virustotal.com/reference/url-info 12 | 13 | * v2 documentation 14 | 15 | https://developers.virustotal.com/v2.0/reference/url-scan 16 | https://developers.virustotal.com/v2.0/reference/url-report 17 | """ 18 | 19 | from virustotal_python import Virustotal 20 | from pprint import pprint 21 | from base64 import urlsafe_b64encode 22 | 23 | API_KEY = "" 24 | 25 | URLS = ["google.com", "wikipedia.com", "github.com"] 26 | 27 | # v3 example 28 | vtotal = Virustotal(API_KEY=API_KEY) 29 | 30 | for url in URLS: 31 | resp = vtotal.request("urls", data={"url": url}, method="POST") 32 | # Safe encode URL in base64 format 33 | # https://developers.virustotal.com/reference/url 34 | url_id = urlsafe_b64encode(url.encode()).decode().strip("=") 35 | print(f"URL: {url} ID: {url_id}") 36 | report = vtotal.request(f"urls/{url_id}") 37 | print(report.object_type) 38 | pprint(report.data) 39 | 40 | # v2 example 41 | vtotal = Virustotal(API_KEY=API_KEY, API_VERSION=2) 42 | 43 | # A maximum of 4 URLs can be sent at once for a v2 API request 44 | resp = vtotal.request("url/scan", params={"url": "\n".join(URLS)}, method="POST") 45 | for url_resp in resp.json(): 46 | scan_id = url_resp["scan_id"] 47 | print(scan_id) 48 | # Request report for URL analysis 49 | analysis_resp = vtotal.request("url/report", params={"resource": scan_id}) 50 | print(analysis_resp.response_code) 51 | pprint(analysis_resp.json()) 52 | -------------------------------------------------------------------------------- /examples/searchmetadata.py: -------------------------------------------------------------------------------- 1 | """ 2 | The examples in this file are for virustotal-python version >=0.1.0 3 | 4 | Search the VirusTotal v3 API for a domain, IP address and comment tag. 5 | 6 | Also, retrieve VirusTotal metadata. 7 | 8 | Documentation: 9 | 10 | * v3 documentation 11 | 12 | https://developers.virustotal.com/reference/search-1 13 | https://developers.virustotal.com/reference/metadata 14 | """ 15 | 16 | from virustotal_python import Virustotal 17 | 18 | API_KEY = "" 19 | 20 | # The ID (either SHA-256, SHA-1 or MD5 hash) identifying the file 21 | FILE_ID = "9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115" 22 | 23 | # v3 examples 24 | vtotal = Virustotal(API_KEY=API_KEY) 25 | 26 | # Search the VirusTotal API for google.com 27 | resp = vtotal.request("search", params={"query": "google.com"}) 28 | # Search the VirusTotal API for information related to Google's DNS (8.8.8.8) 29 | resp = vtotal.request("search", params={"query": "8.8.8.8"}) 30 | # Search the VirusTotal API for a file ID 31 | resp = vtotal.request("search", params={"query": FILE_ID}) 32 | # Search the VirusTotal API for the tag comment '#malicious' 33 | resp = vtotal.request("search", params={"query": "#malicious"}) 34 | 35 | # Retrieve VirusTotal metadata 36 | resp = vtotal.request("metadata") 37 | # Print out a list of VirusTotal's supported engines 38 | resp = vtotal.request("metadata") 39 | engines_dict = resp.data["engines"] 40 | print(engines_dict.keys()) 41 | -------------------------------------------------------------------------------- /images/APIKey.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbrennand/virustotal-python/ed564c31fc6e82128145864e5f68c6d54601be1b/images/APIKey.png -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "black" 5 | version = "24.4.2" 6 | description = "The uncompromising code formatter." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, 11 | {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, 12 | {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, 13 | {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, 14 | {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, 15 | {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, 16 | {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, 17 | {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, 18 | {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, 19 | {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, 20 | {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, 21 | {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, 22 | {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, 23 | {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, 24 | {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, 25 | {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, 26 | {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, 27 | {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, 28 | {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, 29 | {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, 30 | {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, 31 | {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, 32 | ] 33 | 34 | [package.dependencies] 35 | click = ">=8.0.0" 36 | mypy-extensions = ">=0.4.3" 37 | packaging = ">=22.0" 38 | pathspec = ">=0.9.0" 39 | platformdirs = ">=2" 40 | 41 | [package.extras] 42 | colorama = ["colorama (>=0.4.3)"] 43 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 44 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 45 | uvloop = ["uvloop (>=0.15.2)"] 46 | 47 | [[package]] 48 | name = "certifi" 49 | version = "2024.2.2" 50 | description = "Python package for providing Mozilla's CA Bundle." 51 | optional = false 52 | python-versions = ">=3.6" 53 | files = [ 54 | {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, 55 | {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, 56 | ] 57 | 58 | [[package]] 59 | name = "cffi" 60 | version = "1.16.0" 61 | description = "Foreign Function Interface for Python calling C code." 62 | optional = false 63 | python-versions = ">=3.8" 64 | files = [ 65 | {file = "cffi-1.16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6b3d6606d369fc1da4fd8c357d026317fbb9c9b75d36dc16e90e84c26854b088"}, 66 | {file = "cffi-1.16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ac0f5edd2360eea2f1daa9e26a41db02dd4b0451b48f7c318e217ee092a213e9"}, 67 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7e61e3e4fa664a8588aa25c883eab612a188c725755afff6289454d6362b9673"}, 68 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a72e8961a86d19bdb45851d8f1f08b041ea37d2bd8d4fd19903bc3083d80c896"}, 69 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b50bf3f55561dac5438f8e70bfcdfd74543fd60df5fa5f62d94e5867deca684"}, 70 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7651c50c8c5ef7bdb41108b7b8c5a83013bfaa8a935590c5d74627c047a583c7"}, 71 | {file = "cffi-1.16.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4108df7fe9b707191e55f33efbcb2d81928e10cea45527879a4749cbe472614"}, 72 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:32c68ef735dbe5857c810328cb2481e24722a59a2003018885514d4c09af9743"}, 73 | {file = "cffi-1.16.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:673739cb539f8cdaa07d92d02efa93c9ccf87e345b9a0b556e3ecc666718468d"}, 74 | {file = "cffi-1.16.0-cp310-cp310-win32.whl", hash = "sha256:9f90389693731ff1f659e55c7d1640e2ec43ff725cc61b04b2f9c6d8d017df6a"}, 75 | {file = "cffi-1.16.0-cp310-cp310-win_amd64.whl", hash = "sha256:e6024675e67af929088fda399b2094574609396b1decb609c55fa58b028a32a1"}, 76 | {file = "cffi-1.16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b84834d0cf97e7d27dd5b7f3aca7b6e9263c56308ab9dc8aae9784abb774d404"}, 77 | {file = "cffi-1.16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b8ebc27c014c59692bb2664c7d13ce7a6e9a629be20e54e7271fa696ff2b417"}, 78 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee07e47c12890ef248766a6e55bd38ebfb2bb8edd4142d56db91b21ea68b7627"}, 79 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8a9d3ebe49f084ad71f9269834ceccbf398253c9fac910c4fd7053ff1386936"}, 80 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e70f54f1796669ef691ca07d046cd81a29cb4deb1e5f942003f401c0c4a2695d"}, 81 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5bf44d66cdf9e893637896c7faa22298baebcd18d1ddb6d2626a6e39793a1d56"}, 82 | {file = "cffi-1.16.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7b78010e7b97fef4bee1e896df8a4bbb6712b7f05b7ef630f9d1da00f6444d2e"}, 83 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c6a164aa47843fb1b01e941d385aab7215563bb8816d80ff3a363a9f8448a8dc"}, 84 | {file = "cffi-1.16.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e09f3ff613345df5e8c3667da1d918f9149bd623cd9070c983c013792a9a62eb"}, 85 | {file = "cffi-1.16.0-cp311-cp311-win32.whl", hash = "sha256:2c56b361916f390cd758a57f2e16233eb4f64bcbeee88a4881ea90fca14dc6ab"}, 86 | {file = "cffi-1.16.0-cp311-cp311-win_amd64.whl", hash = "sha256:db8e577c19c0fda0beb7e0d4e09e0ba74b1e4c092e0e40bfa12fe05b6f6d75ba"}, 87 | {file = "cffi-1.16.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:fa3a0128b152627161ce47201262d3140edb5a5c3da88d73a1b790a959126956"}, 88 | {file = "cffi-1.16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:68e7c44931cc171c54ccb702482e9fc723192e88d25a0e133edd7aff8fcd1f6e"}, 89 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abd808f9c129ba2beda4cfc53bde801e5bcf9d6e0f22f095e45327c038bfe68e"}, 90 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88e2b3c14bdb32e440be531ade29d3c50a1a59cd4e51b1dd8b0865c54ea5d2e2"}, 91 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcc8eb6d5902bb1cf6dc4f187ee3ea80a1eba0a89aba40a5cb20a5087d961357"}, 92 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b7be2d771cdba2942e13215c4e340bfd76398e9227ad10402a8767ab1865d2e6"}, 93 | {file = "cffi-1.16.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e715596e683d2ce000574bae5d07bd522c781a822866c20495e52520564f0969"}, 94 | {file = "cffi-1.16.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:2d92b25dbf6cae33f65005baf472d2c245c050b1ce709cc4588cdcdd5495b520"}, 95 | {file = "cffi-1.16.0-cp312-cp312-win32.whl", hash = "sha256:b2ca4e77f9f47c55c194982e10f058db063937845bb2b7a86c84a6cfe0aefa8b"}, 96 | {file = "cffi-1.16.0-cp312-cp312-win_amd64.whl", hash = "sha256:68678abf380b42ce21a5f2abde8efee05c114c2fdb2e9eef2efdb0257fba1235"}, 97 | {file = "cffi-1.16.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0c9ef6ff37e974b73c25eecc13952c55bceed9112be2d9d938ded8e856138bcc"}, 98 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a09582f178759ee8128d9270cd1344154fd473bb77d94ce0aeb2a93ebf0feaf0"}, 99 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e760191dd42581e023a68b758769e2da259b5d52e3103c6060ddc02c9edb8d7b"}, 100 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80876338e19c951fdfed6198e70bc88f1c9758b94578d5a7c4c91a87af3cf31c"}, 101 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a6a14b17d7e17fa0d207ac08642c8820f84f25ce17a442fd15e27ea18d67c59b"}, 102 | {file = "cffi-1.16.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6602bc8dc6f3a9e02b6c22c4fc1e47aa50f8f8e6d3f78a5e16ac33ef5fefa324"}, 103 | {file = "cffi-1.16.0-cp38-cp38-win32.whl", hash = "sha256:131fd094d1065b19540c3d72594260f118b231090295d8c34e19a7bbcf2e860a"}, 104 | {file = "cffi-1.16.0-cp38-cp38-win_amd64.whl", hash = "sha256:31d13b0f99e0836b7ff893d37af07366ebc90b678b6664c955b54561fc36ef36"}, 105 | {file = "cffi-1.16.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:582215a0e9adbe0e379761260553ba11c58943e4bbe9c36430c4ca6ac74b15ed"}, 106 | {file = "cffi-1.16.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b29ebffcf550f9da55bec9e02ad430c992a87e5f512cd63388abb76f1036d8d2"}, 107 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dc9b18bf40cc75f66f40a7379f6a9513244fe33c0e8aa72e2d56b0196a7ef872"}, 108 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9cb4a35b3642fc5c005a6755a5d17c6c8b6bcb6981baf81cea8bfbc8903e8ba8"}, 109 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b86851a328eedc692acf81fb05444bdf1891747c25af7529e39ddafaf68a4f3f"}, 110 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c0f31130ebc2d37cdd8e44605fb5fa7ad59049298b3f745c74fa74c62fbfcfc4"}, 111 | {file = "cffi-1.16.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f8e709127c6c77446a8c0a8c8bf3c8ee706a06cd44b1e827c3e6a2ee6b8c098"}, 112 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:748dcd1e3d3d7cd5443ef03ce8685043294ad6bd7c02a38d1bd367cfd968e000"}, 113 | {file = "cffi-1.16.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8895613bcc094d4a1b2dbe179d88d7fb4a15cee43c052e8885783fac397d91fe"}, 114 | {file = "cffi-1.16.0-cp39-cp39-win32.whl", hash = "sha256:ed86a35631f7bfbb28e108dd96773b9d5a6ce4811cf6ea468bb6a359b256b1e4"}, 115 | {file = "cffi-1.16.0-cp39-cp39-win_amd64.whl", hash = "sha256:3686dffb02459559c74dd3d81748269ffb0eb027c39a6fc99502de37d501faa8"}, 116 | {file = "cffi-1.16.0.tar.gz", hash = "sha256:bcb3ef43e58665bbda2fb198698fcae6776483e0c4a631aa5647806c25e02cc0"}, 117 | ] 118 | 119 | [package.dependencies] 120 | pycparser = "*" 121 | 122 | [[package]] 123 | name = "charset-normalizer" 124 | version = "3.3.2" 125 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 126 | optional = false 127 | python-versions = ">=3.7.0" 128 | files = [ 129 | {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, 130 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, 131 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, 132 | {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, 133 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, 134 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, 135 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, 136 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, 137 | {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, 138 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, 139 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, 140 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, 141 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, 142 | {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, 143 | {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, 144 | {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, 145 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, 146 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, 147 | {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, 148 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, 149 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, 150 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, 151 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, 152 | {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, 153 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, 154 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, 155 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, 156 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, 157 | {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, 158 | {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, 159 | {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, 160 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, 161 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, 162 | {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, 163 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, 164 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, 165 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, 166 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, 167 | {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, 168 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, 169 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, 170 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, 171 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, 172 | {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, 173 | {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, 174 | {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, 175 | {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, 176 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, 177 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, 178 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, 179 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, 180 | {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, 181 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, 182 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, 183 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, 184 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, 185 | {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, 186 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, 187 | {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, 188 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, 189 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, 190 | {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, 191 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, 192 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, 193 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, 194 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, 195 | {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, 196 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, 197 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, 198 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, 199 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, 200 | {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, 201 | {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, 202 | {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, 203 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, 204 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, 205 | {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, 206 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, 207 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, 208 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, 209 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, 210 | {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, 211 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, 212 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, 213 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, 214 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, 215 | {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, 216 | {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, 217 | {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, 218 | {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, 219 | ] 220 | 221 | [[package]] 222 | name = "click" 223 | version = "8.1.7" 224 | description = "Composable command line interface toolkit" 225 | optional = false 226 | python-versions = ">=3.7" 227 | files = [ 228 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 229 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 230 | ] 231 | 232 | [package.dependencies] 233 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 234 | 235 | [[package]] 236 | name = "colorama" 237 | version = "0.4.6" 238 | description = "Cross-platform colored terminal text." 239 | optional = false 240 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 241 | files = [ 242 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 243 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 244 | ] 245 | 246 | [[package]] 247 | name = "coverage" 248 | version = "7.5.1" 249 | description = "Code coverage measurement for Python" 250 | optional = false 251 | python-versions = ">=3.8" 252 | files = [ 253 | {file = "coverage-7.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c0884920835a033b78d1c73b6d3bbcda8161a900f38a488829a83982925f6c2e"}, 254 | {file = "coverage-7.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:39afcd3d4339329c5f58de48a52f6e4e50f6578dd6099961cf22228feb25f38f"}, 255 | {file = "coverage-7.5.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7b0ceee8147444347da6a66be737c9d78f3353b0681715b668b72e79203e4a"}, 256 | {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a9ca3f2fae0088c3c71d743d85404cec8df9be818a005ea065495bedc33da35"}, 257 | {file = "coverage-7.5.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5fd215c0c7d7aab005221608a3c2b46f58c0285a819565887ee0b718c052aa4e"}, 258 | {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:4bf0655ab60d754491004a5efd7f9cccefcc1081a74c9ef2da4735d6ee4a6223"}, 259 | {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:61c4bf1ba021817de12b813338c9be9f0ad5b1e781b9b340a6d29fc13e7c1b5e"}, 260 | {file = "coverage-7.5.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:db66fc317a046556a96b453a58eced5024af4582a8dbdc0c23ca4dbc0d5b3146"}, 261 | {file = "coverage-7.5.1-cp310-cp310-win32.whl", hash = "sha256:b016ea6b959d3b9556cb401c55a37547135a587db0115635a443b2ce8f1c7228"}, 262 | {file = "coverage-7.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:df4e745a81c110e7446b1cc8131bf986157770fa405fe90e15e850aaf7619bc8"}, 263 | {file = "coverage-7.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:796a79f63eca8814ca3317a1ea443645c9ff0d18b188de470ed7ccd45ae79428"}, 264 | {file = "coverage-7.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4fc84a37bfd98db31beae3c2748811a3fa72bf2007ff7902f68746d9757f3746"}, 265 | {file = "coverage-7.5.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6175d1a0559986c6ee3f7fccfc4a90ecd12ba0a383dcc2da30c2b9918d67d8a3"}, 266 | {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fc81d5878cd6274ce971e0a3a18a8803c3fe25457165314271cf78e3aae3aa2"}, 267 | {file = "coverage-7.5.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:556cf1a7cbc8028cb60e1ff0be806be2eded2daf8129b8811c63e2b9a6c43bca"}, 268 | {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:9981706d300c18d8b220995ad22627647be11a4276721c10911e0e9fa44c83e8"}, 269 | {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:d7fed867ee50edf1a0b4a11e8e5d0895150e572af1cd6d315d557758bfa9c057"}, 270 | {file = "coverage-7.5.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ef48e2707fb320c8f139424a596f5b69955a85b178f15af261bab871873bb987"}, 271 | {file = "coverage-7.5.1-cp311-cp311-win32.whl", hash = "sha256:9314d5678dcc665330df5b69c1e726a0e49b27df0461c08ca12674bcc19ef136"}, 272 | {file = "coverage-7.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:5fa567e99765fe98f4e7d7394ce623e794d7cabb170f2ca2ac5a4174437e90dd"}, 273 | {file = "coverage-7.5.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b6cf3764c030e5338e7f61f95bd21147963cf6aa16e09d2f74f1fa52013c1206"}, 274 | {file = "coverage-7.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2ec92012fefebee89a6b9c79bc39051a6cb3891d562b9270ab10ecfdadbc0c34"}, 275 | {file = "coverage-7.5.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:16db7f26000a07efcf6aea00316f6ac57e7d9a96501e990a36f40c965ec7a95d"}, 276 | {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:beccf7b8a10b09c4ae543582c1319c6df47d78fd732f854ac68d518ee1fb97fa"}, 277 | {file = "coverage-7.5.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8748731ad392d736cc9ccac03c9845b13bb07d020a33423fa5b3a36521ac6e4e"}, 278 | {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7352b9161b33fd0b643ccd1f21f3a3908daaddf414f1c6cb9d3a2fd618bf2572"}, 279 | {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:7a588d39e0925f6a2bff87154752481273cdb1736270642aeb3635cb9b4cad07"}, 280 | {file = "coverage-7.5.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:68f962d9b72ce69ea8621f57551b2fa9c70509af757ee3b8105d4f51b92b41a7"}, 281 | {file = "coverage-7.5.1-cp312-cp312-win32.whl", hash = "sha256:f152cbf5b88aaeb836127d920dd0f5e7edff5a66f10c079157306c4343d86c19"}, 282 | {file = "coverage-7.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:5a5740d1fb60ddf268a3811bcd353de34eb56dc24e8f52a7f05ee513b2d4f596"}, 283 | {file = "coverage-7.5.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e2213def81a50519d7cc56ed643c9e93e0247f5bbe0d1247d15fa520814a7cd7"}, 284 | {file = "coverage-7.5.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5037f8fcc2a95b1f0e80585bd9d1ec31068a9bcb157d9750a172836e98bc7a90"}, 285 | {file = "coverage-7.5.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c3721c2c9e4c4953a41a26c14f4cef64330392a6d2d675c8b1db3b645e31f0e"}, 286 | {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ca498687ca46a62ae590253fba634a1fe9836bc56f626852fb2720f334c9e4e5"}, 287 | {file = "coverage-7.5.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cdcbc320b14c3e5877ee79e649677cb7d89ef588852e9583e6b24c2e5072661"}, 288 | {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:57e0204b5b745594e5bc14b9b50006da722827f0b8c776949f1135677e88d0b8"}, 289 | {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:8fe7502616b67b234482c3ce276ff26f39ffe88adca2acf0261df4b8454668b4"}, 290 | {file = "coverage-7.5.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:9e78295f4144f9dacfed4f92935fbe1780021247c2fabf73a819b17f0ccfff8d"}, 291 | {file = "coverage-7.5.1-cp38-cp38-win32.whl", hash = "sha256:1434e088b41594baa71188a17533083eabf5609e8e72f16ce8c186001e6b8c41"}, 292 | {file = "coverage-7.5.1-cp38-cp38-win_amd64.whl", hash = "sha256:0646599e9b139988b63704d704af8e8df7fa4cbc4a1f33df69d97f36cb0a38de"}, 293 | {file = "coverage-7.5.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:4cc37def103a2725bc672f84bd939a6fe4522310503207aae4d56351644682f1"}, 294 | {file = "coverage-7.5.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fc0b4d8bfeabd25ea75e94632f5b6e047eef8adaed0c2161ada1e922e7f7cece"}, 295 | {file = "coverage-7.5.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d0a0f5e06881ecedfe6f3dd2f56dcb057b6dbeb3327fd32d4b12854df36bf26"}, 296 | {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9735317685ba6ec7e3754798c8871c2f49aa5e687cc794a0b1d284b2389d1bd5"}, 297 | {file = "coverage-7.5.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d21918e9ef11edf36764b93101e2ae8cc82aa5efdc7c5a4e9c6c35a48496d601"}, 298 | {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:c3e757949f268364b96ca894b4c342b41dc6f8f8b66c37878aacef5930db61be"}, 299 | {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:79afb6197e2f7f60c4824dd4b2d4c2ec5801ceb6ba9ce5d2c3080e5660d51a4f"}, 300 | {file = "coverage-7.5.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d1d0d98d95dd18fe29dc66808e1accf59f037d5716f86a501fc0256455219668"}, 301 | {file = "coverage-7.5.1-cp39-cp39-win32.whl", hash = "sha256:1cc0fe9b0b3a8364093c53b0b4c0c2dd4bb23acbec4c9240b5f284095ccf7981"}, 302 | {file = "coverage-7.5.1-cp39-cp39-win_amd64.whl", hash = "sha256:dde0070c40ea8bb3641e811c1cfbf18e265d024deff6de52c5950677a8fb1e0f"}, 303 | {file = "coverage-7.5.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:6537e7c10cc47c595828b8a8be04c72144725c383c4702703ff4e42e44577312"}, 304 | {file = "coverage-7.5.1.tar.gz", hash = "sha256:54de9ef3a9da981f7af93eafde4ede199e0846cd819eb27c88e2b712aae9708c"}, 305 | ] 306 | 307 | [package.extras] 308 | toml = ["tomli"] 309 | 310 | [[package]] 311 | name = "cryptography" 312 | version = "42.0.7" 313 | description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." 314 | optional = false 315 | python-versions = ">=3.7" 316 | files = [ 317 | {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:a987f840718078212fdf4504d0fd4c6effe34a7e4740378e59d47696e8dfb477"}, 318 | {file = "cryptography-42.0.7-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:bd13b5e9b543532453de08bcdc3cc7cebec6f9883e886fd20a92f26940fd3e7a"}, 319 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a79165431551042cc9d1d90e6145d5d0d3ab0f2d66326c201d9b0e7f5bf43604"}, 320 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a47787a5e3649008a1102d3df55424e86606c9bae6fb77ac59afe06d234605f8"}, 321 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:02c0eee2d7133bdbbc5e24441258d5d2244beb31da5ed19fbb80315f4bbbff55"}, 322 | {file = "cryptography-42.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:5e44507bf8d14b36b8389b226665d597bc0f18ea035d75b4e53c7b1ea84583cc"}, 323 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:7f8b25fa616d8b846aef64b15c606bb0828dbc35faf90566eb139aa9cff67af2"}, 324 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:93a3209f6bb2b33e725ed08ee0991b92976dfdcf4e8b38646540674fc7508e13"}, 325 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e6b8f1881dac458c34778d0a424ae5769de30544fc678eac51c1c8bb2183e9da"}, 326 | {file = "cryptography-42.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3de9a45d3b2b7d8088c3fbf1ed4395dfeff79d07842217b38df14ef09ce1d8d7"}, 327 | {file = "cryptography-42.0.7-cp37-abi3-win32.whl", hash = "sha256:789caea816c6704f63f6241a519bfa347f72fbd67ba28d04636b7c6b7da94b0b"}, 328 | {file = "cryptography-42.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:8cb8ce7c3347fcf9446f201dc30e2d5a3c898d009126010cbd1f443f28b52678"}, 329 | {file = "cryptography-42.0.7-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:a3a5ac8b56fe37f3125e5b72b61dcde43283e5370827f5233893d461b7360cd4"}, 330 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:779245e13b9a6638df14641d029add5dc17edbef6ec915688f3acb9e720a5858"}, 331 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d563795db98b4cd57742a78a288cdbdc9daedac29f2239793071fe114f13785"}, 332 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:31adb7d06fe4383226c3e963471f6837742889b3c4caa55aac20ad951bc8ffda"}, 333 | {file = "cryptography-42.0.7-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:efd0bf5205240182e0f13bcaea41be4fdf5c22c5129fc7ced4a0282ac86998c9"}, 334 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:a9bc127cdc4ecf87a5ea22a2556cab6c7eda2923f84e4f3cc588e8470ce4e42e"}, 335 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:3577d029bc3f4827dd5bf8bf7710cac13527b470bbf1820a3f394adb38ed7d5f"}, 336 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:2e47577f9b18723fa294b0ea9a17d5e53a227867a0a4904a1a076d1646d45ca1"}, 337 | {file = "cryptography-42.0.7-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1a58839984d9cb34c855197043eaae2c187d930ca6d644612843b4fe8513c886"}, 338 | {file = "cryptography-42.0.7-cp39-abi3-win32.whl", hash = "sha256:e6b79d0adb01aae87e8a44c2b64bc3f3fe59515280e00fb6d57a7267a2583cda"}, 339 | {file = "cryptography-42.0.7-cp39-abi3-win_amd64.whl", hash = "sha256:16268d46086bb8ad5bf0a2b5544d8a9ed87a0e33f5e77dd3c3301e63d941a83b"}, 340 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2954fccea107026512b15afb4aa664a5640cd0af630e2ee3962f2602693f0c82"}, 341 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:362e7197754c231797ec45ee081f3088a27a47c6c01eff2ac83f60f85a50fe60"}, 342 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:4f698edacf9c9e0371112792558d2f705b5645076cc0aaae02f816a0171770fd"}, 343 | {file = "cryptography-42.0.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:5482e789294854c28237bba77c4c83be698be740e31a3ae5e879ee5444166582"}, 344 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e9b2a6309f14c0497f348d08a065d52f3020656f675819fc405fb63bbcd26562"}, 345 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d8e3098721b84392ee45af2dd554c947c32cc52f862b6a3ae982dbb90f577f14"}, 346 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:c65f96dad14f8528a447414125e1fc8feb2ad5a272b8f68477abbcc1ea7d94b9"}, 347 | {file = "cryptography-42.0.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:36017400817987670037fbb0324d71489b6ead6231c9604f8fc1f7d008087c68"}, 348 | {file = "cryptography-42.0.7.tar.gz", hash = "sha256:ecbfbc00bf55888edda9868a4cf927205de8499e7fabe6c050322298382953f2"}, 349 | ] 350 | 351 | [package.dependencies] 352 | cffi = {version = ">=1.12", markers = "platform_python_implementation != \"PyPy\""} 353 | 354 | [package.extras] 355 | docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.1.1)"] 356 | docstest = ["pyenchant (>=1.6.11)", "readme-renderer", "sphinxcontrib-spelling (>=4.0.1)"] 357 | nox = ["nox"] 358 | pep8test = ["check-sdist", "click", "mypy", "ruff"] 359 | sdist = ["build"] 360 | ssh = ["bcrypt (>=3.1.5)"] 361 | test = ["certifi", "pretend", "pytest (>=6.2.0)", "pytest-benchmark", "pytest-cov", "pytest-xdist"] 362 | test-randomorder = ["pytest-randomly"] 363 | 364 | [[package]] 365 | name = "docutils" 366 | version = "0.21.2" 367 | description = "Docutils -- Python Documentation Utilities" 368 | optional = false 369 | python-versions = ">=3.9" 370 | files = [ 371 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 372 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 373 | ] 374 | 375 | [[package]] 376 | name = "idna" 377 | version = "3.7" 378 | description = "Internationalized Domain Names in Applications (IDNA)" 379 | optional = false 380 | python-versions = ">=3.5" 381 | files = [ 382 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 383 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 384 | ] 385 | 386 | [[package]] 387 | name = "importlib-metadata" 388 | version = "7.1.0" 389 | description = "Read metadata from Python packages" 390 | optional = false 391 | python-versions = ">=3.8" 392 | files = [ 393 | {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, 394 | {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, 395 | ] 396 | 397 | [package.dependencies] 398 | zipp = ">=0.5" 399 | 400 | [package.extras] 401 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 402 | perf = ["ipython"] 403 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] 404 | 405 | [[package]] 406 | name = "iniconfig" 407 | version = "2.0.0" 408 | description = "brain-dead simple config-ini parsing" 409 | optional = false 410 | python-versions = ">=3.7" 411 | files = [ 412 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 413 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 414 | ] 415 | 416 | [[package]] 417 | name = "jaraco-classes" 418 | version = "3.4.0" 419 | description = "Utility functions for Python class constructs" 420 | optional = false 421 | python-versions = ">=3.8" 422 | files = [ 423 | {file = "jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790"}, 424 | {file = "jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd"}, 425 | ] 426 | 427 | [package.dependencies] 428 | more-itertools = "*" 429 | 430 | [package.extras] 431 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 432 | testing = ["pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 433 | 434 | [[package]] 435 | name = "jaraco-context" 436 | version = "5.3.0" 437 | description = "Useful decorators and context managers" 438 | optional = false 439 | python-versions = ">=3.8" 440 | files = [ 441 | {file = "jaraco.context-5.3.0-py3-none-any.whl", hash = "sha256:3e16388f7da43d384a1a7cd3452e72e14732ac9fe459678773a3608a812bf266"}, 442 | {file = "jaraco.context-5.3.0.tar.gz", hash = "sha256:c2f67165ce1f9be20f32f650f25d8edfc1646a8aeee48ae06fb35f90763576d2"}, 443 | ] 444 | 445 | [package.extras] 446 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 447 | testing = ["portend", "pytest (>=6,!=8.1.1)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 448 | 449 | [[package]] 450 | name = "jaraco-functools" 451 | version = "4.0.1" 452 | description = "Functools like those found in stdlib" 453 | optional = false 454 | python-versions = ">=3.8" 455 | files = [ 456 | {file = "jaraco.functools-4.0.1-py3-none-any.whl", hash = "sha256:3b24ccb921d6b593bdceb56ce14799204f473976e2a9d4b15b04d0f2c2326664"}, 457 | {file = "jaraco_functools-4.0.1.tar.gz", hash = "sha256:d33fa765374c0611b52f8b3a795f8900869aa88c84769d4d1746cd68fb28c3e8"}, 458 | ] 459 | 460 | [package.dependencies] 461 | more-itertools = "*" 462 | 463 | [package.extras] 464 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] 465 | testing = ["jaraco.classes", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 466 | 467 | [[package]] 468 | name = "jeepney" 469 | version = "0.8.0" 470 | description = "Low-level, pure Python DBus protocol wrapper." 471 | optional = false 472 | python-versions = ">=3.7" 473 | files = [ 474 | {file = "jeepney-0.8.0-py3-none-any.whl", hash = "sha256:c0a454ad016ca575060802ee4d590dd912e35c122fa04e70306de3d076cce755"}, 475 | {file = "jeepney-0.8.0.tar.gz", hash = "sha256:5efe48d255973902f6badc3ce55e2aa6c5c3b3bc642059ef3a91247bcfcc5806"}, 476 | ] 477 | 478 | [package.extras] 479 | test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"] 480 | trio = ["async_generator", "trio"] 481 | 482 | [[package]] 483 | name = "keyring" 484 | version = "25.2.1" 485 | description = "Store and access your passwords safely." 486 | optional = false 487 | python-versions = ">=3.8" 488 | files = [ 489 | {file = "keyring-25.2.1-py3-none-any.whl", hash = "sha256:2458681cdefc0dbc0b7eb6cf75d0b98e59f9ad9b2d4edd319d18f68bdca95e50"}, 490 | {file = "keyring-25.2.1.tar.gz", hash = "sha256:daaffd42dbda25ddafb1ad5fec4024e5bbcfe424597ca1ca452b299861e49f1b"}, 491 | ] 492 | 493 | [package.dependencies] 494 | "jaraco.classes" = "*" 495 | "jaraco.context" = "*" 496 | "jaraco.functools" = "*" 497 | jeepney = {version = ">=0.4.2", markers = "sys_platform == \"linux\""} 498 | pywin32-ctypes = {version = ">=0.2.0", markers = "sys_platform == \"win32\""} 499 | SecretStorage = {version = ">=3.2", markers = "sys_platform == \"linux\""} 500 | 501 | [package.extras] 502 | completion = ["shtab (>=1.1.0)"] 503 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 504 | testing = ["pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 505 | 506 | [[package]] 507 | name = "markdown-it-py" 508 | version = "3.0.0" 509 | description = "Python port of markdown-it. Markdown parsing, done right!" 510 | optional = false 511 | python-versions = ">=3.8" 512 | files = [ 513 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 514 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 515 | ] 516 | 517 | [package.dependencies] 518 | mdurl = ">=0.1,<1.0" 519 | 520 | [package.extras] 521 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 522 | code-style = ["pre-commit (>=3.0,<4.0)"] 523 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 524 | linkify = ["linkify-it-py (>=1,<3)"] 525 | plugins = ["mdit-py-plugins"] 526 | profiling = ["gprof2dot"] 527 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 528 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 529 | 530 | [[package]] 531 | name = "mdurl" 532 | version = "0.1.2" 533 | description = "Markdown URL utilities" 534 | optional = false 535 | python-versions = ">=3.7" 536 | files = [ 537 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 538 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 539 | ] 540 | 541 | [[package]] 542 | name = "more-itertools" 543 | version = "10.2.0" 544 | description = "More routines for operating on iterables, beyond itertools" 545 | optional = false 546 | python-versions = ">=3.8" 547 | files = [ 548 | {file = "more-itertools-10.2.0.tar.gz", hash = "sha256:8fccb480c43d3e99a00087634c06dd02b0d50fbf088b380de5a41a015ec239e1"}, 549 | {file = "more_itertools-10.2.0-py3-none-any.whl", hash = "sha256:686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684"}, 550 | ] 551 | 552 | [[package]] 553 | name = "mypy-extensions" 554 | version = "1.0.0" 555 | description = "Type system extensions for programs checked with the mypy type checker." 556 | optional = false 557 | python-versions = ">=3.5" 558 | files = [ 559 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 560 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 561 | ] 562 | 563 | [[package]] 564 | name = "nh3" 565 | version = "0.2.17" 566 | description = "Python bindings to the ammonia HTML sanitization library." 567 | optional = false 568 | python-versions = "*" 569 | files = [ 570 | {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:551672fd71d06cd828e282abdb810d1be24e1abb7ae2543a8fa36a71c1006fe9"}, 571 | {file = "nh3-0.2.17-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c551eb2a3876e8ff2ac63dff1585236ed5dfec5ffd82216a7a174f7c5082a78a"}, 572 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:66f17d78826096291bd264f260213d2b3905e3c7fae6dfc5337d49429f1dc9f3"}, 573 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0316c25b76289cf23be6b66c77d3608a4fdf537b35426280032f432f14291b9a"}, 574 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:22c26e20acbb253a5bdd33d432a326d18508a910e4dcf9a3316179860d53345a"}, 575 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:85cdbcca8ef10733bd31f931956f7fbb85145a4d11ab9e6742bbf44d88b7e351"}, 576 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:40015514022af31975c0b3bca4014634fa13cb5dc4dbcbc00570acc781316dcc"}, 577 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ba73a2f8d3a1b966e9cdba7b211779ad8a2561d2dba9674b8a19ed817923f65f"}, 578 | {file = "nh3-0.2.17-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c21bac1a7245cbd88c0b0e4a420221b7bfa838a2814ee5bb924e9c2f10a1120b"}, 579 | {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d7a25fd8c86657f5d9d576268e3b3767c5cd4f42867c9383618be8517f0f022a"}, 580 | {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:c790769152308421283679a142dbdb3d1c46c79c823008ecea8e8141db1a2062"}, 581 | {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:b4427ef0d2dfdec10b641ed0bdaf17957eb625b2ec0ea9329b3d28806c153d71"}, 582 | {file = "nh3-0.2.17-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a3f55fabe29164ba6026b5ad5c3151c314d136fd67415a17660b4aaddacf1b10"}, 583 | {file = "nh3-0.2.17-cp37-abi3-win32.whl", hash = "sha256:1a814dd7bba1cb0aba5bcb9bebcc88fd801b63e21e2450ae6c52d3b3336bc911"}, 584 | {file = "nh3-0.2.17-cp37-abi3-win_amd64.whl", hash = "sha256:1aa52a7def528297f256de0844e8dd680ee279e79583c76d6fa73a978186ddfb"}, 585 | {file = "nh3-0.2.17.tar.gz", hash = "sha256:40d0741a19c3d645e54efba71cb0d8c475b59135c1e3c580f879ad5514cbf028"}, 586 | ] 587 | 588 | [[package]] 589 | name = "packaging" 590 | version = "24.0" 591 | description = "Core utilities for Python packages" 592 | optional = false 593 | python-versions = ">=3.7" 594 | files = [ 595 | {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, 596 | {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, 597 | ] 598 | 599 | [[package]] 600 | name = "pathspec" 601 | version = "0.12.1" 602 | description = "Utility library for gitignore style pattern matching of file paths." 603 | optional = false 604 | python-versions = ">=3.8" 605 | files = [ 606 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 607 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 608 | ] 609 | 610 | [[package]] 611 | name = "pkginfo" 612 | version = "1.10.0" 613 | description = "Query metadata from sdists / bdists / installed packages." 614 | optional = false 615 | python-versions = ">=3.6" 616 | files = [ 617 | {file = "pkginfo-1.10.0-py3-none-any.whl", hash = "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097"}, 618 | {file = "pkginfo-1.10.0.tar.gz", hash = "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297"}, 619 | ] 620 | 621 | [package.extras] 622 | testing = ["pytest", "pytest-cov", "wheel"] 623 | 624 | [[package]] 625 | name = "platformdirs" 626 | version = "4.2.2" 627 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 628 | optional = false 629 | python-versions = ">=3.8" 630 | files = [ 631 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 632 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 633 | ] 634 | 635 | [package.extras] 636 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 637 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 638 | type = ["mypy (>=1.8)"] 639 | 640 | [[package]] 641 | name = "pluggy" 642 | version = "1.5.0" 643 | description = "plugin and hook calling mechanisms for python" 644 | optional = false 645 | python-versions = ">=3.8" 646 | files = [ 647 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 648 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 649 | ] 650 | 651 | [package.extras] 652 | dev = ["pre-commit", "tox"] 653 | testing = ["pytest", "pytest-benchmark"] 654 | 655 | [[package]] 656 | name = "pycparser" 657 | version = "2.22" 658 | description = "C parser in Python" 659 | optional = false 660 | python-versions = ">=3.8" 661 | files = [ 662 | {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, 663 | {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, 664 | ] 665 | 666 | [[package]] 667 | name = "pygments" 668 | version = "2.18.0" 669 | description = "Pygments is a syntax highlighting package written in Python." 670 | optional = false 671 | python-versions = ">=3.8" 672 | files = [ 673 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 674 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 675 | ] 676 | 677 | [package.extras] 678 | windows-terminal = ["colorama (>=0.4.6)"] 679 | 680 | [[package]] 681 | name = "pysocks" 682 | version = "1.7.1" 683 | description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." 684 | optional = true 685 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 686 | files = [ 687 | {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, 688 | {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, 689 | {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, 690 | ] 691 | 692 | [[package]] 693 | name = "pytest" 694 | version = "8.2.0" 695 | description = "pytest: simple powerful testing with Python" 696 | optional = false 697 | python-versions = ">=3.8" 698 | files = [ 699 | {file = "pytest-8.2.0-py3-none-any.whl", hash = "sha256:1733f0620f6cda4095bbf0d9ff8022486e91892245bb9e7d5542c018f612f233"}, 700 | {file = "pytest-8.2.0.tar.gz", hash = "sha256:d507d4482197eac0ba2bae2e9babf0672eb333017bcedaa5fb1a3d42c1174b3f"}, 701 | ] 702 | 703 | [package.dependencies] 704 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 705 | iniconfig = "*" 706 | packaging = "*" 707 | pluggy = ">=1.5,<2.0" 708 | 709 | [package.extras] 710 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 711 | 712 | [[package]] 713 | name = "pytest-cov" 714 | version = "5.0.0" 715 | description = "Pytest plugin for measuring coverage." 716 | optional = false 717 | python-versions = ">=3.8" 718 | files = [ 719 | {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, 720 | {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, 721 | ] 722 | 723 | [package.dependencies] 724 | coverage = {version = ">=5.2.1", extras = ["toml"]} 725 | pytest = ">=4.6" 726 | 727 | [package.extras] 728 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] 729 | 730 | [[package]] 731 | name = "pytest-mock" 732 | version = "3.14.0" 733 | description = "Thin-wrapper around the mock package for easier use with pytest" 734 | optional = false 735 | python-versions = ">=3.8" 736 | files = [ 737 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 738 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 739 | ] 740 | 741 | [package.dependencies] 742 | pytest = ">=6.2.5" 743 | 744 | [package.extras] 745 | dev = ["pre-commit", "pytest-asyncio", "tox"] 746 | 747 | [[package]] 748 | name = "pywin32-ctypes" 749 | version = "0.2.2" 750 | description = "A (partial) reimplementation of pywin32 using ctypes/cffi" 751 | optional = false 752 | python-versions = ">=3.6" 753 | files = [ 754 | {file = "pywin32-ctypes-0.2.2.tar.gz", hash = "sha256:3426e063bdd5fd4df74a14fa3cf80a0b42845a87e1d1e81f6549f9daec593a60"}, 755 | {file = "pywin32_ctypes-0.2.2-py3-none-any.whl", hash = "sha256:bf490a1a709baf35d688fe0ecf980ed4de11d2b3e37b51e5442587a75d9957e7"}, 756 | ] 757 | 758 | [[package]] 759 | name = "readme-renderer" 760 | version = "43.0" 761 | description = "readme_renderer is a library for rendering readme descriptions for Warehouse" 762 | optional = false 763 | python-versions = ">=3.8" 764 | files = [ 765 | {file = "readme_renderer-43.0-py3-none-any.whl", hash = "sha256:19db308d86ecd60e5affa3b2a98f017af384678c63c88e5d4556a380e674f3f9"}, 766 | {file = "readme_renderer-43.0.tar.gz", hash = "sha256:1818dd28140813509eeed8d62687f7cd4f7bad90d4db586001c5dc09d4fde311"}, 767 | ] 768 | 769 | [package.dependencies] 770 | docutils = ">=0.13.1" 771 | nh3 = ">=0.2.14" 772 | Pygments = ">=2.5.1" 773 | 774 | [package.extras] 775 | md = ["cmarkgfm (>=0.8.0)"] 776 | 777 | [[package]] 778 | name = "requests" 779 | version = "2.31.0" 780 | description = "Python HTTP for Humans." 781 | optional = false 782 | python-versions = ">=3.7" 783 | files = [ 784 | {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, 785 | {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, 786 | ] 787 | 788 | [package.dependencies] 789 | certifi = ">=2017.4.17" 790 | charset-normalizer = ">=2,<4" 791 | idna = ">=2.5,<4" 792 | urllib3 = ">=1.21.1,<3" 793 | 794 | [package.extras] 795 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 796 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 797 | 798 | [[package]] 799 | name = "requests-mock" 800 | version = "1.12.1" 801 | description = "Mock out responses from the requests package" 802 | optional = false 803 | python-versions = ">=3.5" 804 | files = [ 805 | {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, 806 | {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, 807 | ] 808 | 809 | [package.dependencies] 810 | requests = ">=2.22,<3" 811 | 812 | [package.extras] 813 | fixture = ["fixtures"] 814 | 815 | [[package]] 816 | name = "requests-toolbelt" 817 | version = "1.0.0" 818 | description = "A utility belt for advanced users of python-requests" 819 | optional = false 820 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 821 | files = [ 822 | {file = "requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6"}, 823 | {file = "requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06"}, 824 | ] 825 | 826 | [package.dependencies] 827 | requests = ">=2.0.1,<3.0.0" 828 | 829 | [[package]] 830 | name = "rfc3986" 831 | version = "2.0.0" 832 | description = "Validating URI References per RFC 3986" 833 | optional = false 834 | python-versions = ">=3.7" 835 | files = [ 836 | {file = "rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd"}, 837 | {file = "rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c"}, 838 | ] 839 | 840 | [package.extras] 841 | idna2008 = ["idna"] 842 | 843 | [[package]] 844 | name = "rich" 845 | version = "13.7.1" 846 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 847 | optional = false 848 | python-versions = ">=3.7.0" 849 | files = [ 850 | {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, 851 | {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, 852 | ] 853 | 854 | [package.dependencies] 855 | markdown-it-py = ">=2.2.0" 856 | pygments = ">=2.13.0,<3.0.0" 857 | 858 | [package.extras] 859 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 860 | 861 | [[package]] 862 | name = "secretstorage" 863 | version = "3.3.3" 864 | description = "Python bindings to FreeDesktop.org Secret Service API" 865 | optional = false 866 | python-versions = ">=3.6" 867 | files = [ 868 | {file = "SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99"}, 869 | {file = "SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77"}, 870 | ] 871 | 872 | [package.dependencies] 873 | cryptography = ">=2.0" 874 | jeepney = ">=0.6" 875 | 876 | [[package]] 877 | name = "twine" 878 | version = "5.1.0" 879 | description = "Collection of utilities for publishing packages on PyPI" 880 | optional = false 881 | python-versions = ">=3.8" 882 | files = [ 883 | {file = "twine-5.1.0-py3-none-any.whl", hash = "sha256:fe1d814395bfe50cfbe27783cb74efe93abeac3f66deaeb6c8390e4e92bacb43"}, 884 | {file = "twine-5.1.0.tar.gz", hash = "sha256:4d74770c88c4fcaf8134d2a6a9d863e40f08255ff7d8e2acb3cbbd57d25f6e9d"}, 885 | ] 886 | 887 | [package.dependencies] 888 | importlib-metadata = ">=3.6" 889 | keyring = ">=15.1" 890 | pkginfo = ">=1.8.1" 891 | readme-renderer = ">=35.0" 892 | requests = ">=2.20" 893 | requests-toolbelt = ">=0.8.0,<0.9.0 || >0.9.0" 894 | rfc3986 = ">=1.4.0" 895 | rich = ">=12.0.0" 896 | urllib3 = ">=1.26.0" 897 | 898 | [[package]] 899 | name = "urllib3" 900 | version = "2.2.1" 901 | description = "HTTP library with thread-safe connection pooling, file post, and more." 902 | optional = false 903 | python-versions = ">=3.8" 904 | files = [ 905 | {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, 906 | {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, 907 | ] 908 | 909 | [package.extras] 910 | brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] 911 | h2 = ["h2 (>=4,<5)"] 912 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 913 | zstd = ["zstandard (>=0.18.0)"] 914 | 915 | [[package]] 916 | name = "zipp" 917 | version = "3.18.2" 918 | description = "Backport of pathlib-compatible object wrapper for zip files" 919 | optional = false 920 | python-versions = ">=3.8" 921 | files = [ 922 | {file = "zipp-3.18.2-py3-none-any.whl", hash = "sha256:dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e"}, 923 | {file = "zipp-3.18.2.tar.gz", hash = "sha256:6278d9ddbcfb1f1089a88fde84481528b07b0e10474e09dcfe53dad4069fa059"}, 924 | ] 925 | 926 | [package.extras] 927 | docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] 928 | testing = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] 929 | 930 | [extras] 931 | socks = ["PySocks"] 932 | 933 | [metadata] 934 | lock-version = "2.0" 935 | python-versions = "^3.12" 936 | content-hash = "6e8be5b406f7be578d2870425750609c6a841e675e5915f2592ed5d6edd9af43" 937 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "virustotal-python" 3 | version = "1.1.0" 4 | description = "A Python library to interact with the public VirusTotal v3 and v2 APIs." 5 | authors = ["dbrennand"] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/dbrennand/virustotal-python" 9 | repository = "https://github.com/dbrennand/virustotal-python" 10 | keywords = [ 11 | "VirusTotal", 12 | "Wrapper", 13 | "Public API", 14 | "Library", 15 | "v3", 16 | "v2" 17 | ] 18 | classifiers = [ 19 | "Programming Language :: Python :: 3", 20 | "Programming Language :: Python :: 3.7", 21 | "Programming Language :: Python :: 3.8", 22 | "Programming Language :: Python :: 3.9", 23 | "Programming Language :: Python :: 3.10", 24 | "Programming Language :: Python :: 3.11", 25 | "Programming Language :: Python :: 3.12", 26 | ] 27 | exclude = [".gitignore"] 28 | 29 | [tool.poetry.dependencies] 30 | python = "^3.12" 31 | requests = { version = "^2.31.0" } 32 | PySocks = { version = "^1.7.1", optional = true } 33 | 34 | [tool.poetry.extras] 35 | socks = ["PySocks"] 36 | 37 | [tool.poetry.dev-dependencies] 38 | black = "^24.4.2" 39 | twine = "^5.1.0" 40 | pytest = "^8.2.0" 41 | pytest-mock = "^3.14.0" 42 | requests-mock = "^1.12.1" 43 | pytest-cov = "^5.0.0" 44 | 45 | [build-system] 46 | requires = ["poetry-core>=1.0.0"] 47 | build-backend = "poetry.core.masonry.api" 48 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/dbrennand/virustotal-python/ed564c31fc6e82128145864e5f68c6d54601be1b/tests/__init__.py -------------------------------------------------------------------------------- /tests/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "response_code": 1, 3 | "meta": { 4 | "count": 37, 5 | "cursor": "CtIBChEKBGRhdGUSCQiv86vhnf_sAhK4AWoRc352aXJ1c3RvdGFsY2xvdWRyogELEgZTYW1wbGUiQDlmMTAxNDgzNjYyZmMwNzFiN2MxMGY4MWM2NGJiMzQ0OTFjYTRhODc3MTkxZDQ2NGZmNDZmZDk0YzcyNDcxMTUMCxIHQ29tbWVudCJJOWYxMDE0ODM2NjJmYzA3MWI3YzEwZjgxYzY0YmIzNDQ5MWNhNGE4NzcxOTFkNDY0ZmY0NmZkOTRjNzI0NzExNS01NTM3YjRjOAwYACAB" 6 | }, 7 | "data": [ 8 | { 9 | "attributes": { 10 | "date": 1632291032, 11 | "text": "Watchout! This looks dangerous!", 12 | "votes": { 13 | "positive": 0, 14 | "abuse": 0, 15 | "negative": 0 16 | }, 17 | "html": "Watchout! This looks dangerous!", 18 | "tags": [] 19 | }, 20 | "type": "comment", 21 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-b32ca808", 22 | "links": { 23 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-b32ca808" 24 | } 25 | }, 26 | { 27 | "attributes": { 28 | "date": 1632291027, 29 | "text": "Wow, this looks like a #malicious file!", 30 | "votes": { 31 | "positive": 0, 32 | "abuse": 0, 33 | "negative": 0 34 | }, 35 | "html": "Wow, this looks like a #malicious file!", 36 | "tags": [ 37 | "malicious" 38 | ] 39 | }, 40 | "type": "comment", 41 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-55f30362", 42 | "links": { 43 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-55f30362" 44 | } 45 | }, 46 | { 47 | "attributes": { 48 | "date": 1631698602, 49 | "text": "Watchout! This looks dangerous!", 50 | "votes": { 51 | "positive": 0, 52 | "abuse": 0, 53 | "negative": 0 54 | }, 55 | "html": "Watchout! This looks dangerous!", 56 | "tags": [] 57 | }, 58 | "type": "comment", 59 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c8f29681", 60 | "links": { 61 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c8f29681" 62 | } 63 | }, 64 | { 65 | "attributes": { 66 | "date": 1628487995, 67 | "text": "Watchout! This looks dangerous!", 68 | "votes": { 69 | "positive": 0, 70 | "abuse": 0, 71 | "negative": 0 72 | }, 73 | "html": "Watchout! This looks dangerous!", 74 | "tags": [] 75 | }, 76 | "type": "comment", 77 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-f868ec62", 78 | "links": { 79 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-f868ec62" 80 | } 81 | }, 82 | { 83 | "attributes": { 84 | "date": 1628487990, 85 | "text": "Wow, this looks like a #malicious file!", 86 | "votes": { 87 | "positive": 0, 88 | "abuse": 0, 89 | "negative": 0 90 | }, 91 | "html": "Wow, this looks like a #malicious file!", 92 | "tags": [ 93 | "malicious" 94 | ] 95 | }, 96 | "type": "comment", 97 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-75d8015c", 98 | "links": { 99 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-75d8015c" 100 | } 101 | }, 102 | { 103 | "attributes": { 104 | "date": 1624463998, 105 | "text": "Watchout! This looks dangerous!", 106 | "votes": { 107 | "positive": 0, 108 | "abuse": 0, 109 | "negative": 0 110 | }, 111 | "html": "Watchout! This looks dangerous!", 112 | "tags": [] 113 | }, 114 | "type": "comment", 115 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c92f1477", 116 | "links": { 117 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-c92f1477" 118 | } 119 | }, 120 | { 121 | "attributes": { 122 | "date": 1624463994, 123 | "text": "Wow, this looks like a #malicious file!", 124 | "votes": { 125 | "positive": 0, 126 | "abuse": 0, 127 | "negative": 0 128 | }, 129 | "html": "Wow, this looks like a #malicious file!", 130 | "tags": [ 131 | "malicious" 132 | ] 133 | }, 134 | "type": "comment", 135 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-1499e7d7", 136 | "links": { 137 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-1499e7d7" 138 | } 139 | }, 140 | { 141 | "attributes": { 142 | "date": 1620216813, 143 | "text": "YARA Signature Match - THOR APT Scanner\n\nRULE: SUSP_Encoded_EnvUserProfile\nRULE_SET: Livehunt - Suspicious1 Indicators\nRULE_TYPE: Valhalla Rule Feed Only\nDESCRIPTION: Detects encoded keyword - $env:userprofile\nRULE_AUTHOR: Florian Roth\n\nDetection Timestamp: 2021-05-05 12:17\nAV Detection Ratio: 38 / 60\n\nUse these tags to search for similar matches: #livehunt-suspicious1indicators #susp_encoded_envuserprofile\nMore information: https://www.nextron-systems.com/notes-on-virustotal-matches/\n", 144 | "votes": { 145 | "positive": 0, 146 | "abuse": 0, 147 | "negative": 0 148 | }, 149 | "html": "YARA Signature Match - THOR APT Scanner

RULE: SUSP_Encoded_EnvUserProfile
RULE_SET: Livehunt - Suspicious1 Indicators
RULE_TYPE: Valhalla Rule Feed Only
DESCRIPTION: Detects encoded keyword - $env:userprofile
RULE_AUTHOR: Florian Roth

Detection Timestamp: 2021-05-05 12:17
AV Detection Ratio: 38 / 60

Use these tags to search for similar matches: #livehunt-suspicious1indicators #susp_encoded_envuserprofile
More information: https://www.nextron-systems.com/notes-on-virustotal-matches/
", 150 | "tags": [ 151 | "livehunt-suspicious1indicators", 152 | "susp_encoded_envuserprofile" 153 | ] 154 | }, 155 | "type": "comment", 156 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-3b6e7a36", 157 | "links": { 158 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-3b6e7a36" 159 | } 160 | }, 161 | { 162 | "attributes": { 163 | "date": 1606741727, 164 | "text": "This file looks much more malicious - c56d9d52e9318dbd341e72cc90002e48ade97352b013b5d2f28f9325a56564bd", 165 | "votes": { 166 | "positive": 0, 167 | "abuse": 0, 168 | "negative": 0 169 | }, 170 | "html": "This file looks much more malicious - c56d9d52e9318dbd341e72cc90002e48ade97352b013b5d2f28f9325a56564bd", 171 | "tags": [] 172 | }, 173 | "type": "comment", 174 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-ad12bfc6", 175 | "links": { 176 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-ad12bfc6" 177 | } 178 | }, 179 | { 180 | "attributes": { 181 | "date": 1605260605, 182 | "text": "#watchout, this looks very malicious!", 183 | "votes": { 184 | "positive": 0, 185 | "abuse": 0, 186 | "negative": 0 187 | }, 188 | "html": "#watchout, this looks very malicious!", 189 | "tags": [ 190 | "watchout" 191 | ] 192 | }, 193 | "type": "comment", 194 | "id": "f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-5537b4c8", 195 | "links": { 196 | "self": "https://www.virustotal.com/api/v3/comments/f-9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115-5537b4c8" 197 | } 198 | } 199 | ], 200 | "links": { 201 | "self": "https://www.virustotal.com/api/v3/files/9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115/comments?limit=10", 202 | "next": "https://www.virustotal.com/api/v3/files/9f101483662fc071b7c10f81c64bb34491ca4a877191d464ff46fd94c7247115/comments?cursor=CtIBChEKBGRhdGUSCQiv86vhnf_sAhK4AWoRc352aXJ1c3RvdGFsY2xvdWRyogELEgZTYW1wbGUiQDlmMTAxNDgzNjYyZmMwNzFiN2MxMGY4MWM2NGJiMzQ0OTFjYTRhODc3MTkxZDQ2NGZmNDZmZDk0YzcyNDcxMTUMCxIHQ29tbWVudCJJOWYxMDE0ODM2NjJmYzA3MWI3YzEwZjgxYzY0YmIzNDQ5MWNhNGE4NzcxOTFkNDY0ZmY0NmZkOTRjNzI0NzExNS01NTM3YjRjOAwYACAB&limit=10" 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /tests/test_virustotal_python.py: -------------------------------------------------------------------------------- 1 | """Tests for virustotal-python. 2 | """ 3 | 4 | import virustotal_python 5 | import json 6 | import requests 7 | import pytest 8 | import pytest_mock 9 | import requests_mock as req_mock 10 | 11 | with open("tests/example.json") as json_file: 12 | example_json = json.dumps(json.load(json_file)) 13 | 14 | 15 | @pytest.fixture 16 | def mock_http_request(requests_mock: req_mock.Mocker) -> None: 17 | """Fixture to mock HTTP requests made by the `Virustotal.request()` method. 18 | 19 | Args: 20 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 21 | thin-wrapper around patching the `requests` library. 22 | """ 23 | requests_mock.register_uri( 24 | req_mock.ANY, 25 | req_mock.ANY, 26 | status_code=200, 27 | text=example_json, 28 | headers={"test": "test"}, 29 | ) 30 | 31 | 32 | def test_virustotal_apikey_env(mocker: pytest_mock.MockerFixture) -> None: 33 | """Test `Virustotal` environment variable `VIRUSTOTAL_API_KEY`. 34 | 35 | Args: 36 | mocker (pytest_mock.MockerFixture): A pytest_mock.MockerFixture providing a 37 | thin-wrapper around the patching API from the mock library. 38 | """ 39 | mocker.patch.dict("os.environ", dict(VIRUSTOTAL_API_KEY="API key")) 40 | vtotal = virustotal_python.Virustotal() 41 | assert vtotal.API_KEY == "API key" 42 | 43 | 44 | def test_virustotal(mocker: pytest_mock.MockerFixture) -> None: 45 | """Test `Virustotal` parameters. 46 | 47 | Args: 48 | mocker (pytest_mock.MockerFixture): A pytest_mock.MockerFixture providing a 49 | thin-wrapper around the patching API from the mock library. 50 | """ 51 | keyword_arguments = { 52 | "API_KEY": "test", 53 | "API_VERSION": 3, 54 | "PROXIES": {"http": "http://10.10.1.10:3128"}, 55 | "TIMEOUT": 5.0, 56 | } 57 | mock_vtotal = mocker.patch("virustotal_python.Virustotal") 58 | virustotal_python.Virustotal(**keyword_arguments) 59 | mock_vtotal.assert_called_with(**keyword_arguments) 60 | 61 | 62 | def test_virustotal_context_manager() -> None: 63 | """Test `Virustotal` context manager support.""" 64 | with virustotal_python.Virustotal(API_KEY="test") as vtotal: 65 | vtotal.__exit__("", "", "") 66 | 67 | 68 | def test_virustotal_api_key_value_error() -> None: 69 | """Test `Virustotal` raises `ValueError` when no `API_KEY` is provided.""" 70 | with pytest.raises(ValueError) as execinfo: 71 | virustotal_python.Virustotal() 72 | assert ( 73 | "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'." 74 | == str(execinfo.value) 75 | ) 76 | 77 | 78 | def test_virustotal_api_version_value_error() -> None: 79 | """Test `Virustotal` raises `ValueError` when an unsupported `API_VERSION` is provided.""" 80 | with pytest.raises(ValueError) as execinfo: 81 | virustotal_python.Virustotal(API_KEY="test", API_VERSION="test") 82 | assert ( 83 | "The API version 'test' is not a valid VirusTotal API version.\nValid API versions are 'v2', 2, 'v3' and 3." 84 | == str(execinfo.value) 85 | ) 86 | 87 | 88 | def test_request_notimplemented_error() -> None: 89 | """Test `Virustotal.request()` raises `NotImplementedError` when an unsupported `method` is provided.""" 90 | with pytest.raises(NotImplementedError) as execinfo: 91 | vtotal = virustotal_python.Virustotal(API_KEY="test") 92 | vtotal.request("test", method="test") 93 | assert "The request method 'test' is not implemented." == str(execinfo.value) 94 | 95 | 96 | def test_request_large_file(requests_mock: req_mock.Mocker) -> None: 97 | """Test `Virustotal.request()` `large_file` parameter. 98 | 99 | Args: 100 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 101 | thin-wrapper around patching the `requests` library. 102 | """ 103 | requests_mock.register_uri( 104 | "GET", 105 | "https://www.virustotal.com/api/v3/files/upload_url", 106 | status_code=200, 107 | json={ 108 | "data": "http://www.virustotal.com/_ah/upload/AMmfu6b-_DXUeFe36Sb3b0F4B8mH9Nb-CHbRoUNVOPwG/" 109 | }, 110 | ) 111 | requests_mock.register_uri( 112 | "POST", 113 | "http://www.virustotal.com/_ah/upload/AMmfu6b-_DXUeFe36Sb3b0F4B8mH9Nb-CHbRoUNVOPwG/", 114 | status_code=200, 115 | json={"data": {"type": "analysis", "id": "test=="}}, 116 | ) 117 | with virustotal_python.Virustotal(API_KEY="test", API_VERSION=3) as vtotal: 118 | resp = vtotal.request("files/upload_url") 119 | large_upload_url = resp.data 120 | large_upload_resp = vtotal.request( 121 | large_upload_url, method="POST", large_file=True 122 | ) 123 | assert large_upload_resp.data == {"type": "analysis", "id": "test=="} 124 | 125 | 126 | def test_virustotal_response_headers(mock_http_request) -> None: 127 | """Test `VirustotalResponse.headers` property. 128 | 129 | Args: 130 | mock_http_request (`mock_http_request()`): A pytest fixture to 131 | to mock HTTP requests made by the `Virustotal.request()` method. 132 | """ 133 | with virustotal_python.Virustotal("test") as vtotal: 134 | resp = vtotal.request("test") 135 | assert resp.headers == {"test": "test"} 136 | 137 | 138 | def test_virustotal_response_status_code(mock_http_request) -> None: 139 | """Test `VirustotalResponse.status_code` property. 140 | 141 | Args: 142 | mock_http_request (`mock_http_request()`): A pytest fixture to 143 | to mock HTTP requests made by the `Virustotal.request()` method. 144 | """ 145 | with virustotal_python.Virustotal("test") as vtotal: 146 | resp = vtotal.request("test") 147 | assert resp.status_code == 200 148 | 149 | 150 | def test_virustotal_response_text(mock_http_request) -> None: 151 | """Test `VirustotalResponse.text` property. 152 | 153 | Args: 154 | mock_http_request (`mock_http_request()`): A pytest fixture to 155 | to mock HTTP requests made by the `Virustotal.request()` method. 156 | """ 157 | with virustotal_python.Virustotal("test") as vtotal: 158 | resp = vtotal.request("test") 159 | assert resp.text == example_json 160 | 161 | 162 | def test_virustotal_response_requests_response(mock_http_request) -> None: 163 | """Test `VirustotalResponse.requests_response` property. 164 | 165 | Args: 166 | mock_http_request (`mock_http_request()`): A pytest fixture to 167 | to mock HTTP requests made by the `Virustotal.request()` method. 168 | """ 169 | with virustotal_python.Virustotal("test") as vtotal: 170 | resp = vtotal.request("test") 171 | assert type(resp.requests_response) == requests.Response 172 | 173 | 174 | def test_virustotal_response_links(mock_http_request) -> None: 175 | """Test `VirustotalResponse.links` property. 176 | 177 | Args: 178 | mock_http_request (`mock_http_request()`): A pytest fixture to 179 | to mock HTTP requests made by the `Virustotal.request()` method. 180 | """ 181 | with virustotal_python.Virustotal("test") as vtotal: 182 | resp = vtotal.request("test") 183 | assert resp.links == json.loads(example_json)["links"] 184 | 185 | 186 | def test_virustotal_response_meta(mock_http_request) -> None: 187 | """Test `VirustotalResponse.meta` property. 188 | 189 | Args: 190 | mock_http_request (`mock_http_request()`): A pytest fixture to 191 | to mock HTTP requests made by the `Virustotal.request()` method. 192 | """ 193 | with virustotal_python.Virustotal("test") as vtotal: 194 | resp = vtotal.request("test") 195 | assert resp.meta == json.loads(example_json)["meta"] 196 | 197 | 198 | def test_virustotal_response_cursor(mock_http_request) -> None: 199 | """Test `VirustotalResponse.cursor` property. 200 | 201 | Args: 202 | mock_http_request (`mock_http_request()`): A pytest fixture to 203 | to mock HTTP requests made by the `Virustotal.request()` method. 204 | """ 205 | with virustotal_python.Virustotal("test") as vtotal: 206 | resp = vtotal.request("test") 207 | assert resp.cursor == json.loads(example_json)["meta"]["cursor"] 208 | 209 | 210 | def test_virustotal_response_cursor_none(requests_mock: req_mock.Mocker) -> None: 211 | """Test `VirustotalResponse.cursor` property returns `None` if `meta` key 212 | is not present in the JSON response. 213 | 214 | Args: 215 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 216 | thin-wrapper around patching the `requests` library. 217 | """ 218 | requests_mock.register_uri( 219 | req_mock.ANY, 220 | req_mock.ANY, 221 | status_code=200, 222 | ) 223 | with virustotal_python.Virustotal("test") as vtotal: 224 | resp = vtotal.request("test") 225 | assert resp.cursor == None 226 | 227 | 228 | def test_virustotal_response_data(mock_http_request) -> None: 229 | """Test `VirustotalResponse.data` property. 230 | 231 | Args: 232 | mock_http_request (`mock_http_request()`): A pytest fixture to 233 | to mock HTTP requests made by the `Virustotal.request()` method. 234 | """ 235 | with virustotal_python.Virustotal("test") as vtotal: 236 | resp = vtotal.request("test") 237 | assert resp.data == json.loads(example_json)["data"] 238 | 239 | 240 | def test_virustotal_response_object_type_list(mock_http_request) -> None: 241 | """Test `VirustotalResponse.object_type` property returns a `list` of all the object types. 242 | 243 | Args: 244 | mock_http_request (`mock_http_request()`): A pytest fixture to 245 | to mock HTTP requests made by the `Virustotal.request()` method. 246 | """ 247 | with virustotal_python.Virustotal("test") as vtotal: 248 | resp = vtotal.request("test") 249 | obj_types = [] 250 | for comment in resp.data: 251 | obj_types.append(comment["type"]) 252 | assert resp.object_type == obj_types 253 | 254 | 255 | def test_virustotal_response_object_type_str(requests_mock: req_mock.Mocker) -> None: 256 | """Test `VirustotalResponse.object_type` property returns a `str` of a single 257 | object type. 258 | 259 | Args: 260 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 261 | thin-wrapper around patching the `requests` library. 262 | """ 263 | requests_mock.register_uri( 264 | req_mock.ANY, 265 | req_mock.ANY, 266 | status_code=200, 267 | json={"data": {"type": "test type"}}, 268 | ) 269 | with virustotal_python.Virustotal("test") as vtotal: 270 | resp = vtotal.request("test") 271 | assert resp.object_type == "test type" 272 | 273 | 274 | def test_virustotal_response_object_type_none(requests_mock: req_mock.Mocker) -> None: 275 | """Test `VirustotalResponse.object_type` property returns `None` when the data property 276 | is neither a `list` or `dict`. 277 | 278 | Example endpoint: https://developers.virustotal.com/reference/files-upload-url 279 | 280 | Args: 281 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 282 | thin-wrapper around patching the `requests` library. 283 | """ 284 | requests_mock.register_uri( 285 | req_mock.ANY, 286 | req_mock.ANY, 287 | status_code=200, 288 | json={ 289 | "data": "http://www.virustotal.com/_ah/upload/AMmfu6b-_DXUeFe36Sb3b0F4B8mH9Nb-CHbRoUNVOPwG/" 290 | }, 291 | ) 292 | with virustotal_python.Virustotal("test") as vtotal: 293 | resp = vtotal.request("test") 294 | assert resp.object_type == None 295 | 296 | 297 | def test_virustotal_response_response_code(mock_http_request) -> None: 298 | """Test `VirustotalResponse.response_code` property. 299 | 300 | Args: 301 | mock_http_request (`mock_http_request()`): A pytest fixture to 302 | to mock HTTP requests made by the `Virustotal.request()` method. 303 | """ 304 | with virustotal_python.Virustotal("test") as vtotal: 305 | resp = vtotal.request("test") 306 | assert resp.response_code == json.loads(example_json)["response_code"] 307 | 308 | 309 | def test_virustotal_response_json(mock_http_request) -> None: 310 | """Test `VirustotalResponse.json()` method. 311 | 312 | Args: 313 | mock_http_request (`mock_http_request()`): A pytest fixture to 314 | to mock HTTP requests made by the `Virustotal.request()` method. 315 | """ 316 | with virustotal_python.Virustotal("test") as vtotal: 317 | resp = vtotal.request("test") 318 | assert resp.json() == json.loads(example_json) 319 | 320 | 321 | def test_virustotal_error(requests_mock: req_mock.Mocker) -> None: 322 | """Test `VirustotalError.error()` method and string dunder. 323 | 324 | Args: 325 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 326 | thin-wrapper around patching the `requests` library. 327 | """ 328 | requests_mock.register_uri( 329 | req_mock.ANY, 330 | req_mock.ANY, 331 | status_code=404, 332 | json={ 333 | "error": { 334 | "code": "NotFoundError", 335 | "message": 'URL "thisurlidmakesnosenseatall" not found', 336 | } 337 | }, 338 | ) 339 | with pytest.raises(virustotal_python.VirustotalError) as execinfo: 340 | vtotal = virustotal_python.Virustotal("test") 341 | vtotal.request("test") 342 | assert ( 343 | 'Error NotFoundError (404): URL "thisurlidmakesnosenseatall" not found' 344 | == str(execinfo.value) 345 | ) 346 | 347 | 348 | def test_virustotal_error_no_code_message(requests_mock: req_mock.Mocker) -> None: 349 | """Test `VirustotalError.error()` method and string dunder with no code or message. 350 | 351 | Args: 352 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 353 | thin-wrapper around patching the `requests` library. 354 | """ 355 | requests_mock.register_uri(req_mock.ANY, req_mock.ANY, status_code=400) 356 | with pytest.raises(virustotal_python.VirustotalError) as execinfo: 357 | vtotal = virustotal_python.Virustotal("test") 358 | vtotal.request("test") 359 | assert "Error Unknown (400): No message" == str(execinfo.value) 360 | 361 | 362 | def test_virustotal_error_text_only(requests_mock: req_mock.Mocker) -> None: 363 | """Test `VirustotalError.error()` method and string dunder with `requests.Response.text` present. 364 | 365 | Args: 366 | requests_mock (req_mock.Mocker): A req_mock.Mocker providing a 367 | thin-wrapper around patching the `requests` library. 368 | """ 369 | requests_mock.register_uri( 370 | req_mock.ANY, req_mock.ANY, status_code=400, text="Request failed" 371 | ) 372 | with pytest.raises(virustotal_python.VirustotalError) as execinfo: 373 | vtotal = virustotal_python.Virustotal("test") 374 | vtotal.request("test") 375 | assert "Error Unknown (400): Request failed" == str(execinfo.value) 376 | -------------------------------------------------------------------------------- /virustotal_python/__init__.py: -------------------------------------------------------------------------------- 1 | """A Python library to interact with the public VirusTotal v3 and v2 APIs. 2 | """ 3 | 4 | from virustotal_python.virustotal import Virustotal 5 | from virustotal_python.virustotal import VirustotalResponse 6 | from virustotal_python.virustotal import VirustotalError 7 | 8 | name = "virustotal-python" 9 | 10 | __all__ = ["Virustotal", "VirustotalResponse", "VirustotalError"] 11 | __author__ = "dbrennand" 12 | __version__ = "1.1.0" 13 | -------------------------------------------------------------------------------- /virustotal_python/virustotal.py: -------------------------------------------------------------------------------- 1 | """ 2 | MIT License 3 | 4 | Copyright (c) 2024 Daniel Brennand 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | """ 24 | 25 | import requests 26 | import os 27 | from typing import Union 28 | from json.decoder import JSONDecodeError 29 | 30 | 31 | class VirustotalError(Exception): 32 | """Class for VirusTotal API errors.""" 33 | 34 | def __init__(self, response: requests.Response) -> None: 35 | """Initialisation for `VirustotalError`. 36 | 37 | Args: 38 | response (requests.Response): From a failed API request to the VirusTotal API. 39 | """ 40 | self.response = response 41 | 42 | def __str__(self) -> str: 43 | """String dunder method for `VirustotalError`. 44 | 45 | Returns: 46 | str: Containing the error code, HTTP status code and error message from a failed VirusTotal API request. 47 | """ 48 | error = self.error() 49 | return f"Error {error.get('code', 'Unknown')} ({self.response.status_code}): {error.get('message', 'No message')}" 50 | 51 | def error(self) -> dict: 52 | """Retrieve the error that occurred from a failed VirusTotal API request. 53 | 54 | https://developers.virustotal.com/v2.0/reference/api-responses 55 | 56 | https://developers.virustotal.com/reference/errors 57 | 58 | Returns: 59 | dict: Containing the error code and message returned from the VirusTotal API (if any) otherwise, returns an empty dictionary. 60 | """ 61 | # Attempt to decode JSON as the v3 VirusTotal API returns the error message as JSON 62 | # Fallback to an empty dict if error is somehow missing 63 | try: 64 | return self.response.json().get("error", dict()) 65 | except ValueError: 66 | # Catch ValueError if JSON fails to be deserialized or there is no JSON 67 | # Most likely using the v2 VirusTotal API 68 | # Check there is response text, if not, return an empty dict 69 | if self.response.text: 70 | return dict(message=self.response.text) 71 | else: 72 | return dict() 73 | 74 | 75 | class VirustotalResponse(object): 76 | """Response class for VirusTotal API requests.""" 77 | 78 | def __init__(self, response: requests.Response): 79 | """Initialisation for `VirustotalResponse`. 80 | 81 | Args: 82 | response (requests.Response): From a successful API request to the VirusTotal API. 83 | """ 84 | self.response = response 85 | 86 | @property 87 | def headers(self) -> dict: 88 | """Retrieve the HTTP headers of a VirusTotal API request. 89 | 90 | Returns: 91 | dict: The HTTP headers of the requests.Response object. 92 | """ 93 | return self.response.headers 94 | 95 | @property 96 | def status_code(self) -> int: 97 | """Retrieve the HTTP status code of a VirusTotal API request. 98 | 99 | Returns: 100 | int: The HTTP status code of the requests.Response object. 101 | """ 102 | return self.response.status_code 103 | 104 | @property 105 | def text(self) -> str: 106 | """Retrieve the HTTP text response of a VirusTotal API request. 107 | 108 | Returns: 109 | str: The HTTP text response of the requests.Response object. 110 | """ 111 | return self.response.text 112 | 113 | @property 114 | def requests_response(self) -> requests.Response: 115 | """Retrieve the `requests.Response` object of a VirusTotal API request. 116 | You may want to access this property if you wanted to read other aspects of the response such as cookies. 117 | 118 | Returns: 119 | requests.Response: From a successful API request to the VirusTotal API. 120 | """ 121 | return self.response 122 | 123 | @property 124 | def links(self) -> Union[dict, None]: 125 | """Retrieve the value of the key 'links' in the JSON response from a VirusTotal API request. 126 | 127 | https://developers.virustotal.com/reference/collections 128 | 129 | NOTE: Links are not retrieved for objects inside 'data'. 130 | 131 | Returns: 132 | Union[dict, None]: The links used to retrieve the next set of objects (if any), otherwise, returns `None`. 133 | """ 134 | return self.json().get("links", None) 135 | 136 | @property 137 | def meta(self) -> Union[dict, None]: 138 | """Retrieve the value of the key 'meta' in the JSON response from a VirusTotal API request. 139 | 140 | https://developers.virustotal.com/reference/collections 141 | 142 | Returns: 143 | Union[dict, None]: The metadata about the object(s) (if any), otherwise, returns `None`. 144 | """ 145 | return self.json().get("meta", None) 146 | 147 | @property 148 | def cursor(self) -> Union[str, None]: 149 | """Retrieve the value of the key 'cursor' in the JSON response value 'meta' from a VirusTotal API request. 150 | 151 | https://developers.virustotal.com/reference/collections 152 | 153 | Returns: 154 | Union[str, None]: The cursor used to retrieve related object(s), otherwise, returns `None`. 155 | """ 156 | try: 157 | return self.meta.get("cursor", None) 158 | # Catch AttributeError that occurs when attempting to call attribute 'get' on None 159 | # which is raised if the 'meta' key is not present in the JSON response 160 | except AttributeError: 161 | return None 162 | 163 | @property 164 | def data(self) -> Union[dict, list, str, None]: 165 | """Retrieve the value of the key 'data' in the JSON response from a VirusTotal API request. 166 | 167 | https://developers.virustotal.com/reference/objects 168 | 169 | Returns: 170 | Union[dict, list, str, None]: The data from a VirusTotal API request (if any) otherwise, returns `None`. 171 | """ 172 | return self.json().get("data", None) 173 | 174 | @property 175 | def object_type(self) -> Union[list, str, None]: 176 | """Retrieve the object type(s) in the JSON response from a VirusTotal API request. 177 | 178 | https://developers.virustotal.com/reference/objects 179 | 180 | https://developers.virustotal.com/reference/collections 181 | 182 | Returns: 183 | Union[list, str, None]: A list or string depending on the number of objects returned (if any) otherwise, returns `None`. 184 | """ 185 | data = self.data 186 | # Check if data contains more than one object 187 | if isinstance(data, list): 188 | object_list = [] 189 | for data_object in data: 190 | data_object_type = data_object.get("type", None) 191 | object_list.append(data_object_type) 192 | return object_list 193 | # Data contains only one object 194 | elif isinstance(data, dict): 195 | return data.get("type", None) 196 | else: 197 | return None 198 | 199 | @property 200 | def response_code(self) -> Union[int, None]: 201 | """Retrieve the value of the key 'response_code' in the JSON response from a VirusTotal v2 API request. 202 | 203 | https://developers.virustotal.com/v2.0/reference/api-responses 204 | 205 | Returns: 206 | Union[int, None]: The response_code from the VirusTotal API (if any), otherwise, returns `None`. 207 | """ 208 | return self.json().get("response_code", None) 209 | 210 | def json(self, **kwargs) -> dict: 211 | """Retrieve the JSON response from a VirusTotal API request. 212 | 213 | Args: 214 | **kwargs: Identical to `json.loads(**kwargs)`. 215 | 216 | Returns: 217 | dict: JSON response from a VirusTotal API request. 218 | """ 219 | try: 220 | return self.response.json(**kwargs) 221 | except JSONDecodeError: 222 | return dict() 223 | 224 | 225 | class Virustotal(object): 226 | """Interact with the public VirusTotal v3 and v2 APIs. 227 | 228 | https://developers.virustotal.com/v3.0/reference 229 | 230 | https://developers.virustotal.com/v2.0/reference 231 | """ 232 | 233 | def __init__( 234 | self, 235 | API_KEY: str = None, 236 | API_VERSION: Union[int, str] = 3, 237 | PROXIES: dict = None, 238 | TIMEOUT: float = None, 239 | ): 240 | """Interact with the public VirusTotal v3 and v2 APIs. 241 | 242 | Args: 243 | API_KEY (str, optional): The API key used to interact with the VirusTotal v3 and v2 APIs. 244 | Alternatively, the environment variable `VIRUSTOTAL_API_KEY` can be provided. 245 | API_VERSION (Union[int, str], optional): The version to use when interacting with the VirusTotal API. 246 | Defaults to 3. 247 | PROXIES (dict, optional): Proxies to use when making requests. 248 | E.g. `{"http": "http://10.10.1.10:3128", "https": "https://10.10.1.10:1080"}` 249 | Defaults to `None`. 250 | TIMEOUT (float, optional): The amount of time (in seconds) to wait for the HTTP request before timing out. 251 | Defaults to `None`. 252 | 253 | Raises: 254 | ValueError: When no `API_KEY` is provided or the `API_VERSION` is invalid. 255 | """ 256 | if API_KEY is None: 257 | API_KEY = os.environ.get("VIRUSTOTAL_API_KEY", None) 258 | if API_KEY is None: 259 | raise ValueError( 260 | "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'." 261 | ) 262 | self.API_KEY = API_KEY 263 | self.PROXIES = PROXIES 264 | self.TIMEOUT = TIMEOUT 265 | # Declare appropriate variables depending on the API_VERSION provided 266 | if (API_VERSION == "v2") or (API_VERSION == 2): 267 | self.API_VERSION = API_VERSION 268 | self.BASEURL = "https://www.virustotal.com/vtapi/v2/" 269 | self.HEADERS = { 270 | "Accept-Encoding": "gzip, deflate", 271 | "User-Agent": f"gzip, virustotal-python 1.1.0", 272 | } 273 | elif (API_VERSION == "v3") or (API_VERSION == 3): 274 | self.API_VERSION = API_VERSION 275 | self.BASEURL = "https://www.virustotal.com/api/v3/" 276 | self.HEADERS = { 277 | "Accept-Encoding": "gzip, deflate", 278 | "User-Agent": f"gzip, virustotal-python 1.1.0", 279 | "x-apikey": f"{self.API_KEY}", 280 | } 281 | else: 282 | raise ValueError( 283 | f"The API version '{API_VERSION}' is not a valid VirusTotal API version.\nValid API versions are 'v2', 2, 'v3' and 3." 284 | ) 285 | 286 | def __enter__(self): 287 | """Context Manager enter function.""" 288 | return self 289 | 290 | def __exit__(self, type, value, traceback): 291 | """Context Manager exit function.""" 292 | return 293 | 294 | def request( 295 | self, 296 | resource: str, 297 | params: dict = {}, 298 | data: dict = None, 299 | json: dict = None, 300 | files: dict = None, 301 | method: str = "GET", 302 | large_file: bool = False, 303 | ) -> VirustotalResponse: 304 | """Make a request to the VirusTotal API. 305 | 306 | Args: 307 | resource (str): A valid VirusTotal API endpoint. E.g. `f'files/{id}'`. 308 | params (dict, optional): API endpoint query parameters. Defaults to `{}`. 309 | data (dict, optional): Data to send in the body of the request. Defaults to `None`. 310 | json (dict, optional): JSON payload to send with the request. Defaults to `None`. 311 | files (dict, optional): File(s) for multipart encoding upload. Defaults to `None`. 312 | method (str, optional): The HTTP request method to use. Defaults to `"GET"`. 313 | large_file (bool, optional): If a file is larger than 32MB, a custom generated upload URL is required. 314 | If this param is set to `True`, this URL can be set via the resource param. Defaults to `False`. 315 | 316 | Raises: 317 | NotImplementedError: When a unsupported HTTP method is provided. 318 | VirustotalError: When a failed VirusTotal API response occurs. 319 | 320 | Returns: 321 | VirustotalResponse: A `VirustotalResponse` class object. 322 | """ 323 | endpoint = f"{self.BASEURL}{resource}" 324 | if large_file: 325 | endpoint = resource 326 | # VirusTotal API version 2 requires API_KEY to be in the request parameters 327 | if (self.API_VERSION == "v2") or (self.API_VERSION == 2): 328 | params["apikey"] = self.API_KEY 329 | if method == "GET": 330 | response = requests.get( 331 | endpoint, 332 | params=params, 333 | data=data, 334 | json=json, 335 | files=files, 336 | headers=self.HEADERS, 337 | proxies=self.PROXIES, 338 | timeout=self.TIMEOUT, 339 | ) 340 | elif method == "POST": 341 | response = requests.post( 342 | endpoint, 343 | params=params, 344 | data=data, 345 | json=json, 346 | files=files, 347 | headers=self.HEADERS, 348 | proxies=self.PROXIES, 349 | timeout=self.TIMEOUT, 350 | ) 351 | elif method == "PATCH": 352 | response = requests.patch( 353 | endpoint, 354 | params=params, 355 | data=data, 356 | json=json, 357 | files=files, 358 | headers=self.HEADERS, 359 | proxies=self.PROXIES, 360 | timeout=self.TIMEOUT, 361 | ) 362 | elif method == "DELETE": 363 | response = requests.delete( 364 | endpoint, 365 | params=params, 366 | data=data, 367 | json=json, 368 | files=files, 369 | headers=self.HEADERS, 370 | proxies=self.PROXIES, 371 | timeout=self.TIMEOUT, 372 | ) 373 | else: 374 | raise NotImplementedError( 375 | f"The request method '{method}' is not implemented." 376 | ) 377 | if response.status_code != 200: 378 | raise VirustotalError(response) 379 | else: 380 | return VirustotalResponse(response) 381 | --------------------------------------------------------------------------------