├── .github ├── dependabot.yml └── workflows │ ├── mypy.yml │ ├── python-package.yml │ └── release.yml ├── .gitignore ├── .pre-commit-config.yaml ├── .readthedocs.yml ├── LICENSE ├── README.md ├── docs ├── Makefile ├── __init__.py ├── make.bat └── source │ ├── __init__.py │ ├── api_reference.rst │ ├── conf.py │ └── index.rst ├── mypy.ini ├── poetry.lock ├── pypdns ├── __init__.py ├── api.py ├── errors.py └── py.typed ├── pyproject.toml └── tests └── test.py /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "pip" 9 | directory: "/" 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" 15 | schedule: 16 | # Check for updates to GitHub Actions every weekday 17 | interval: "daily" 18 | -------------------------------------------------------------------------------- /.github/workflows/mypy.yml: -------------------------------------------------------------------------------- 1 | name: Mypy test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | strategy: 14 | matrix: 15 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 16 | 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Set up Python ${{matrix.python-version}} 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{matrix.python-version}} 23 | 24 | - name: Install dependencies 25 | run: | 26 | python -m pip install --upgrade pip poetry 27 | poetry install -vvv 28 | 29 | - name: Run MyPy 30 | run: | 31 | poetry run mypy . 32 | -------------------------------------------------------------------------------- /.github/workflows/python-package.yml: -------------------------------------------------------------------------------- 1 | # This workflow will install Python dependencies, run tests and lint with a variety of Python versions 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions 3 | 4 | name: Python package 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | - name: Set up Python ${{ matrix.python-version }} 24 | uses: actions/setup-python@v5 25 | with: 26 | python-version: ${{ matrix.python-version }} 27 | 28 | - name: Install dependencies 29 | run: | 30 | python -m pip install --upgrade pip poetry 31 | poetry install -vvv 32 | 33 | - name: Test with pytest 34 | env: 35 | LOGIN: ${{ secrets.PDNS_LOGIN }} 36 | PASSWORD: ${{ secrets.PDNS_PASSWORD }} 37 | run: | 38 | poetry run pytest tests/test.py 39 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | on: 2 | release: 3 | types: 4 | - published 5 | 6 | name: release 7 | 8 | jobs: 9 | pypi-publish: 10 | name: Upload release to PyPI 11 | runs-on: ubuntu-latest 12 | environment: 13 | name: pypi 14 | url: https://pypi.org/p/pypdns 15 | permissions: 16 | id-token: write # IMPORTANT: this permission is mandatory for trusted publishing 17 | steps: 18 | - uses: actions/checkout@v4 19 | with: 20 | fetch-depth: 0 21 | - name: Install Poetry 22 | run: python -m pip install --upgrade pip poetry 23 | - name: Build artifacts 24 | run: poetry build 25 | - name: Publish package distributions to PyPI 26 | uses: pypa/gh-action-pypi-publish@release/v1 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | pypdns.egg-info/ 5 | virtenv/ 6 | 7 | pypdns/api_key.py 8 | 9 | .coverage 10 | .mypy_cache/ 11 | *.swp 12 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | exclude: "tests/data" 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | - id: end-of-file-fixer 10 | - id: check-yaml 11 | - id: check-added-large-files 12 | - repo: https://github.com/asottile/pyupgrade 13 | rev: v3.19.1 14 | hooks: 15 | - id: pyupgrade 16 | args: [--py38-plus] 17 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | build: 3 | os: "ubuntu-lts-latest" 4 | tools: 5 | python: "3" 6 | 7 | sphinx: 8 | configuration: docs/source/conf.py 9 | 10 | python: 11 | install: 12 | - method: pip 13 | path: . 14 | extra_requirements: 15 | - docs 16 | 17 | formats: all 18 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013, 2014 Raphaël Vinot 2 | Copyright (c) 2013, 2014 Alexandre Dulaunoy 3 | Copyright (c) 2013, 2014 CIRCL - Computer Incident Response Center Luxembourg 4 | (c/o smile, security made in Lëtzebuerg, Groupement 5 | d'Intérêt Economique) 6 | 7 | All rights reserved. 8 | 9 | Redistribution and use in source and binary forms, with or without modification, 10 | are permitted provided that the following conditions are met: 11 | 12 | 1. Redistributions of source code must retain the above copyright notice, 13 | this list of conditions and the following disclaimer. 14 | 2. Redistributions in binary form must reproduce the above copyright notice, 15 | this list of conditions and the following disclaimer in the documentation 16 | and/or other materials provided with the distribution. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 21 | IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 22 | INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, 23 | BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 24 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 25 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE 26 | OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED 27 | OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Documentation Status](https://readthedocs.org/projects/pypdns/badge/?version=latest)](https://pypdns.readthedocs.io/en/latest/?badge=latest) 2 | 3 | Client API for PDNS 4 | =================== 5 | 6 | Client API to query any Passive DNS implementation following the Passive DNS - Common Output Format. 7 | 8 | * https://datatracker.ietf.org/doc/draft-dulaunoy-dnsop-passive-dns-cof/ 9 | 10 | ## Installation 11 | 12 | ```bash 13 | pip install pypdns 14 | ``` 15 | 16 | ## Usage 17 | 18 | ### Command line 19 | 20 | You can use the `pdns` command to trigger a request. 21 | 22 | ```bash 23 | usage: pdns [-h] --username USERNAME --password PASSWORD --query QUERY [--rrtype RRTYPE] 24 | 25 | Triggers a request againse CIRCL Passive DNS. 26 | 27 | options: 28 | -h, --help show this help message and exit 29 | --username USERNAME The username of you account. 30 | --password PASSWORD The password of you account. 31 | --query QUERY The query, can be an IP. domain, hostname, TLD. 32 | --rrtype RRTYPE Filter the request based on the RR Type. 33 | ``` 34 | 35 | ### Library 36 | 37 | See [API Reference](https://pypdns.readthedocs.io/en/latest/api_reference.html) 38 | 39 | 40 | Example 41 | ======= 42 | 43 | ~~~~ 44 | import pypdns 45 | import json 46 | x = pypdns.PyPDNS(basic_auth=('username','yourpassword')) 47 | 48 | for record in x.iter_query(q='circl.lu', filter_rrtype='A'): 49 | print(json.dumps(record.record, indent=2)) 50 | ~~~~ 51 | 52 | Passive DNS Services 53 | ==================== 54 | 55 | * (default) [CIRCL Passive DNS](http://www.circl.lu/services/passive-dns/) 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = source 9 | BUILDDIR = build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /docs/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CIRCL/PyPDNS/ad8ea6f9a4c09d12b51a41f555ee27ac1f0b085e/docs/__init__.py -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=source 11 | set BUILDDIR=build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /docs/source/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CIRCL/PyPDNS/ad8ea6f9a4c09d12b51a41f555ee27ac1f0b085e/docs/source/__init__.py -------------------------------------------------------------------------------- /docs/source/api_reference.rst: -------------------------------------------------------------------------------- 1 | API reference 2 | ============= 3 | 4 | .. toctree:: 5 | :maxdepth: 2 6 | 7 | .. automodule:: pypdns 8 | :members: 9 | 10 | PyPDNS 11 | -------- 12 | 13 | .. autoclass:: PyPDNS 14 | :members: 15 | 16 | .. autoclass:: PDNSRecord 17 | :members: 18 | -------------------------------------------------------------------------------- /docs/source/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'PyPDNS' 21 | copyright = '2023, Passive DNS team' 22 | author = 'Passive DNS team' 23 | 24 | # The full version, including alpha/beta/rc tags 25 | release = 'v2.2.0' 26 | 27 | 28 | # -- General configuration --------------------------------------------------- 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be 31 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 32 | # ones. 33 | extensions = [ 34 | 'sphinx.ext.autodoc' 35 | ] 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = [] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'alabaster' 52 | 53 | html_theme_options = { 54 | 'page_width': '1200', 55 | 'body_min_width': '1200', 56 | } 57 | 58 | # Add any paths that contain custom static files (such as style sheets) here, 59 | # relative to this directory. They are copied after the builtin static files, 60 | # so a file named "default.css" will overwrite the builtin "default.css". 61 | html_static_path = ['_static'] 62 | -------------------------------------------------------------------------------- /docs/source/index.rst: -------------------------------------------------------------------------------- 1 | .. PyLookyloo documentation master file, created by 2 | sphinx-quickstart on Tue Mar 23 12:28:17 2021. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Welcome to PyPDNS's documentation! 7 | ================================== 8 | 9 | This is the client API for `CIRCL passive DNS `_ 10 | 11 | Installation 12 | ------------ 13 | 14 | The package is available on PyPi, so you can install it with:: 15 | 16 | pip install pypdns 17 | 18 | 19 | Usage 20 | ----- 21 | 22 | You can use `pdns` as a python script:: 23 | 24 | $ pdns -h 25 | usage: pdns [-h] --username USERNAME --password PASSWORD --query QUERY [--rrtype RRTYPE] 26 | 27 | Triggers a request againse CIRCL Passive DNS. 28 | 29 | options: 30 | -h, --help show this help message and exit 31 | --username USERNAME The username of you account. 32 | --password PASSWORD The password of you account. 33 | --query QUERY The query, can be an IP. domain, hostname, TLD. 34 | --rrtype RRTYPE Filter the request based on the RR Type. 35 | 36 | Or as a library: 37 | 38 | .. toctree:: 39 | :glob: 40 | 41 | api_reference 42 | 43 | 44 | Indices and tables 45 | ================== 46 | 47 | * :ref:`genindex` 48 | * :ref:`modindex` 49 | * :ref:`search` 50 | -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | strict = True 3 | warn_return_any = False 4 | show_error_context = True 5 | pretty = True 6 | 7 | [mypy-docs.source.*] 8 | ignore_errors = True 9 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "alabaster" 5 | version = "1.0.0" 6 | description = "A light, configurable Sphinx theme" 7 | optional = true 8 | python-versions = ">=3.10" 9 | groups = ["main"] 10 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 11 | files = [ 12 | {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, 13 | {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, 14 | ] 15 | 16 | [[package]] 17 | name = "attrs" 18 | version = "25.1.0" 19 | description = "Classes Without Boilerplate" 20 | optional = false 21 | python-versions = ">=3.8" 22 | groups = ["main"] 23 | files = [ 24 | {file = "attrs-25.1.0-py3-none-any.whl", hash = "sha256:c75a69e28a550a7e93789579c22aa26b0f5b83b75dc4e08fe092980051e1090a"}, 25 | {file = "attrs-25.1.0.tar.gz", hash = "sha256:1c97078a80c814273a76b2a298a932eb681c87415c11dee0a6921de7f1b02c3e"}, 26 | ] 27 | 28 | [package.extras] 29 | benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 30 | cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 31 | dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 32 | docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"] 33 | tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"] 34 | tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""] 35 | 36 | [[package]] 37 | name = "babel" 38 | version = "2.17.0" 39 | description = "Internationalization utilities" 40 | optional = true 41 | python-versions = ">=3.8" 42 | groups = ["main"] 43 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 44 | files = [ 45 | {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, 46 | {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, 47 | ] 48 | 49 | [package.extras] 50 | dev = ["backports.zoneinfo ; python_version < \"3.9\"", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata ; sys_platform == \"win32\""] 51 | 52 | [[package]] 53 | name = "cattrs" 54 | version = "24.1.2" 55 | description = "Composable complex class support for attrs and dataclasses." 56 | optional = false 57 | python-versions = ">=3.8" 58 | groups = ["main"] 59 | files = [ 60 | {file = "cattrs-24.1.2-py3-none-any.whl", hash = "sha256:67c7495b760168d931a10233f979b28dc04daf853b30752246f4f8471c6d68d0"}, 61 | {file = "cattrs-24.1.2.tar.gz", hash = "sha256:8028cfe1ff5382df59dd36474a86e02d817b06eaf8af84555441bac915d2ef85"}, 62 | ] 63 | 64 | [package.dependencies] 65 | attrs = ">=23.1.0" 66 | exceptiongroup = {version = ">=1.1.1", markers = "python_version < \"3.11\""} 67 | typing-extensions = {version = ">=4.1.0,<4.6.3 || >4.6.3", markers = "python_version < \"3.11\""} 68 | 69 | [package.extras] 70 | bson = ["pymongo (>=4.4.0)"] 71 | cbor2 = ["cbor2 (>=5.4.6)"] 72 | msgpack = ["msgpack (>=1.0.5)"] 73 | msgspec = ["msgspec (>=0.18.5) ; implementation_name == \"cpython\""] 74 | orjson = ["orjson (>=3.9.2) ; implementation_name == \"cpython\""] 75 | pyyaml = ["pyyaml (>=6.0)"] 76 | tomlkit = ["tomlkit (>=0.11.8)"] 77 | ujson = ["ujson (>=5.7.0)"] 78 | 79 | [[package]] 80 | name = "certifi" 81 | version = "2025.1.31" 82 | description = "Python package for providing Mozilla's CA Bundle." 83 | optional = false 84 | python-versions = ">=3.6" 85 | groups = ["main"] 86 | files = [ 87 | {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, 88 | {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, 89 | ] 90 | 91 | [[package]] 92 | name = "charset-normalizer" 93 | version = "3.4.1" 94 | description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." 95 | optional = false 96 | python-versions = ">=3.7" 97 | groups = ["main"] 98 | files = [ 99 | {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, 100 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, 101 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, 102 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, 103 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, 104 | {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, 105 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, 106 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, 107 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, 108 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, 109 | {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, 110 | {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, 111 | {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, 112 | {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, 113 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, 114 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, 115 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, 116 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, 117 | {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, 118 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, 119 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, 120 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, 121 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, 122 | {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, 123 | {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, 124 | {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, 125 | {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, 126 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, 127 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, 128 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, 129 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, 130 | {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, 131 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, 132 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, 133 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, 134 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, 135 | {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, 136 | {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, 137 | {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, 138 | {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, 139 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, 140 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, 141 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, 142 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, 143 | {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, 144 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, 145 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, 146 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, 147 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, 148 | {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, 149 | {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, 150 | {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, 151 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, 152 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, 153 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, 154 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, 155 | {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, 156 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, 157 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, 158 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, 159 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, 160 | {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, 161 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, 162 | {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, 163 | {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, 164 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, 165 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, 166 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, 167 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, 168 | {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, 169 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, 170 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, 171 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, 172 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, 173 | {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, 174 | {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, 175 | {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, 176 | {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, 177 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, 178 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, 179 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, 180 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, 181 | {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, 182 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, 183 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, 184 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, 185 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, 186 | {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, 187 | {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, 188 | {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, 189 | {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, 190 | {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, 191 | ] 192 | 193 | [[package]] 194 | name = "colorama" 195 | version = "0.4.6" 196 | description = "Cross-platform colored terminal text." 197 | optional = false 198 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 199 | groups = ["main", "dev"] 200 | files = [ 201 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 202 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 203 | ] 204 | markers = {main = "python_version >= \"3.11\" and extra == \"docs\" and sys_platform == \"win32\"", dev = "sys_platform == \"win32\""} 205 | 206 | [[package]] 207 | name = "dnspython" 208 | version = "2.7.0" 209 | description = "DNS toolkit" 210 | optional = false 211 | python-versions = ">=3.9" 212 | groups = ["main"] 213 | files = [ 214 | {file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"}, 215 | {file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"}, 216 | ] 217 | 218 | [package.extras] 219 | dev = ["black (>=23.1.0)", "coverage (>=7.0)", "flake8 (>=7)", "hypercorn (>=0.16.0)", "mypy (>=1.8)", "pylint (>=3)", "pytest (>=7.4)", "pytest-cov (>=4.1.0)", "quart-trio (>=0.11.0)", "sphinx (>=7.2.0)", "sphinx-rtd-theme (>=2.0.0)", "twine (>=4.0.0)", "wheel (>=0.42.0)"] 220 | dnssec = ["cryptography (>=43)"] 221 | doh = ["h2 (>=4.1.0)", "httpcore (>=1.0.0)", "httpx (>=0.26.0)"] 222 | doq = ["aioquic (>=1.0.0)"] 223 | idna = ["idna (>=3.7)"] 224 | trio = ["trio (>=0.23)"] 225 | wmi = ["wmi (>=1.5.1)"] 226 | 227 | [[package]] 228 | name = "docutils" 229 | version = "0.21.2" 230 | description = "Docutils -- Python Documentation Utilities" 231 | optional = true 232 | python-versions = ">=3.9" 233 | groups = ["main"] 234 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 235 | files = [ 236 | {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, 237 | {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, 238 | ] 239 | 240 | [[package]] 241 | name = "exceptiongroup" 242 | version = "1.2.2" 243 | description = "Backport of PEP 654 (exception groups)" 244 | optional = false 245 | python-versions = ">=3.7" 246 | groups = ["main", "dev"] 247 | markers = "python_version < \"3.11\"" 248 | files = [ 249 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 250 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 251 | ] 252 | 253 | [package.extras] 254 | test = ["pytest (>=6)"] 255 | 256 | [[package]] 257 | name = "idna" 258 | version = "3.10" 259 | description = "Internationalized Domain Names in Applications (IDNA)" 260 | optional = false 261 | python-versions = ">=3.6" 262 | groups = ["main"] 263 | files = [ 264 | {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, 265 | {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, 266 | ] 267 | 268 | [package.extras] 269 | all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] 270 | 271 | [[package]] 272 | name = "imagesize" 273 | version = "1.4.1" 274 | description = "Getting image size from png/jpeg/jpeg2000/gif file" 275 | optional = true 276 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 277 | groups = ["main"] 278 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 279 | files = [ 280 | {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, 281 | {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, 282 | ] 283 | 284 | [[package]] 285 | name = "iniconfig" 286 | version = "2.0.0" 287 | description = "brain-dead simple config-ini parsing" 288 | optional = false 289 | python-versions = ">=3.7" 290 | groups = ["dev"] 291 | files = [ 292 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 293 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 294 | ] 295 | 296 | [[package]] 297 | name = "jinja2" 298 | version = "3.1.6" 299 | description = "A very fast and expressive template engine." 300 | optional = true 301 | python-versions = ">=3.7" 302 | groups = ["main"] 303 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 304 | files = [ 305 | {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, 306 | {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, 307 | ] 308 | 309 | [package.dependencies] 310 | MarkupSafe = ">=2.0" 311 | 312 | [package.extras] 313 | i18n = ["Babel (>=2.7)"] 314 | 315 | [[package]] 316 | name = "markupsafe" 317 | version = "3.0.2" 318 | description = "Safely add untrusted strings to HTML/XML markup." 319 | optional = true 320 | python-versions = ">=3.9" 321 | groups = ["main"] 322 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 323 | files = [ 324 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, 325 | {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, 326 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, 327 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, 328 | {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, 329 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, 330 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, 331 | {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, 332 | {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, 333 | {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, 334 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, 335 | {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, 336 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, 337 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, 338 | {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, 339 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, 340 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, 341 | {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, 342 | {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, 343 | {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, 344 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, 345 | {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, 346 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, 347 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, 348 | {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, 349 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, 350 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, 351 | {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, 352 | {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, 353 | {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, 354 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, 355 | {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, 356 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, 357 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, 358 | {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, 359 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, 360 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, 361 | {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, 362 | {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, 363 | {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, 364 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, 365 | {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, 366 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, 367 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, 368 | {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, 369 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, 370 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, 371 | {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, 372 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, 373 | {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, 374 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, 375 | {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, 376 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, 377 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, 378 | {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, 379 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, 380 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, 381 | {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, 382 | {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, 383 | {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, 384 | {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, 385 | ] 386 | 387 | [[package]] 388 | name = "mypy" 389 | version = "1.15.0" 390 | description = "Optional static typing for Python" 391 | optional = false 392 | python-versions = ">=3.9" 393 | groups = ["dev"] 394 | files = [ 395 | {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, 396 | {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, 397 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, 398 | {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, 399 | {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, 400 | {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, 401 | {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, 402 | {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, 403 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, 404 | {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, 405 | {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, 406 | {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, 407 | {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, 408 | {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, 409 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, 410 | {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, 411 | {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, 412 | {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, 413 | {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, 414 | {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, 415 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, 416 | {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, 417 | {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, 418 | {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, 419 | {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, 420 | {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, 421 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, 422 | {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, 423 | {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, 424 | {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, 425 | {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, 426 | {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, 427 | ] 428 | 429 | [package.dependencies] 430 | mypy_extensions = ">=1.0.0" 431 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 432 | typing_extensions = ">=4.6.0" 433 | 434 | [package.extras] 435 | dmypy = ["psutil (>=4.0)"] 436 | faster-cache = ["orjson"] 437 | install-types = ["pip"] 438 | mypyc = ["setuptools (>=50)"] 439 | reports = ["lxml"] 440 | 441 | [[package]] 442 | name = "mypy-extensions" 443 | version = "1.0.0" 444 | description = "Type system extensions for programs checked with the mypy type checker." 445 | optional = false 446 | python-versions = ">=3.5" 447 | groups = ["dev"] 448 | files = [ 449 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 450 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 451 | ] 452 | 453 | [[package]] 454 | name = "packaging" 455 | version = "24.2" 456 | description = "Core utilities for Python packages" 457 | optional = false 458 | python-versions = ">=3.8" 459 | groups = ["main", "dev"] 460 | files = [ 461 | {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, 462 | {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, 463 | ] 464 | markers = {main = "python_version >= \"3.11\" and extra == \"docs\""} 465 | 466 | [[package]] 467 | name = "platformdirs" 468 | version = "4.3.6" 469 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 470 | optional = false 471 | python-versions = ">=3.8" 472 | groups = ["main"] 473 | files = [ 474 | {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, 475 | {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, 476 | ] 477 | 478 | [package.extras] 479 | docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] 480 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] 481 | type = ["mypy (>=1.11.2)"] 482 | 483 | [[package]] 484 | name = "pluggy" 485 | version = "1.5.0" 486 | description = "plugin and hook calling mechanisms for python" 487 | optional = false 488 | python-versions = ">=3.8" 489 | groups = ["dev"] 490 | files = [ 491 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 492 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 493 | ] 494 | 495 | [package.extras] 496 | dev = ["pre-commit", "tox"] 497 | testing = ["pytest", "pytest-benchmark"] 498 | 499 | [[package]] 500 | name = "pygments" 501 | version = "2.19.1" 502 | description = "Pygments is a syntax highlighting package written in Python." 503 | optional = true 504 | python-versions = ">=3.8" 505 | groups = ["main"] 506 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 507 | files = [ 508 | {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, 509 | {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, 510 | ] 511 | 512 | [package.extras] 513 | windows-terminal = ["colorama (>=0.4.6)"] 514 | 515 | [[package]] 516 | name = "pytest" 517 | version = "8.3.5" 518 | description = "pytest: simple powerful testing with Python" 519 | optional = false 520 | python-versions = ">=3.8" 521 | groups = ["dev"] 522 | files = [ 523 | {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, 524 | {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, 525 | ] 526 | 527 | [package.dependencies] 528 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 529 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 530 | iniconfig = "*" 531 | packaging = "*" 532 | pluggy = ">=1.5,<2" 533 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 534 | 535 | [package.extras] 536 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 537 | 538 | [[package]] 539 | name = "requests" 540 | version = "2.32.3" 541 | description = "Python HTTP for Humans." 542 | optional = false 543 | python-versions = ">=3.8" 544 | groups = ["main"] 545 | files = [ 546 | {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, 547 | {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, 548 | ] 549 | 550 | [package.dependencies] 551 | certifi = ">=2017.4.17" 552 | charset-normalizer = ">=2,<4" 553 | idna = ">=2.5,<4" 554 | urllib3 = ">=1.21.1,<3" 555 | 556 | [package.extras] 557 | socks = ["PySocks (>=1.5.6,!=1.5.7)"] 558 | use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] 559 | 560 | [[package]] 561 | name = "requests-cache" 562 | version = "1.2.1" 563 | description = "A persistent cache for python requests" 564 | optional = false 565 | python-versions = ">=3.8" 566 | groups = ["main"] 567 | files = [ 568 | {file = "requests_cache-1.2.1-py3-none-any.whl", hash = "sha256:1285151cddf5331067baa82598afe2d47c7495a1334bfe7a7d329b43e9fd3603"}, 569 | {file = "requests_cache-1.2.1.tar.gz", hash = "sha256:68abc986fdc5b8d0911318fbb5f7c80eebcd4d01bfacc6685ecf8876052511d1"}, 570 | ] 571 | 572 | [package.dependencies] 573 | attrs = ">=21.2" 574 | cattrs = ">=22.2" 575 | platformdirs = ">=2.5" 576 | requests = ">=2.22" 577 | url-normalize = ">=1.4" 578 | urllib3 = ">=1.25.5" 579 | 580 | [package.extras] 581 | all = ["boto3 (>=1.15)", "botocore (>=1.18)", "itsdangerous (>=2.0)", "pymongo (>=3)", "pyyaml (>=6.0.1)", "redis (>=3)", "ujson (>=5.4)"] 582 | bson = ["bson (>=0.5)"] 583 | docs = ["furo (>=2023.3,<2024.0)", "linkify-it-py (>=2.0,<3.0)", "myst-parser (>=1.0,<2.0)", "sphinx (>=5.0.2,<6.0.0)", "sphinx-autodoc-typehints (>=1.19)", "sphinx-automodapi (>=0.14)", "sphinx-copybutton (>=0.5)", "sphinx-design (>=0.2)", "sphinx-notfound-page (>=0.8)", "sphinxcontrib-apidoc (>=0.3)", "sphinxext-opengraph (>=0.9)"] 584 | dynamodb = ["boto3 (>=1.15)", "botocore (>=1.18)"] 585 | json = ["ujson (>=5.4)"] 586 | mongodb = ["pymongo (>=3)"] 587 | redis = ["redis (>=3)"] 588 | security = ["itsdangerous (>=2.0)"] 589 | yaml = ["pyyaml (>=6.0.1)"] 590 | 591 | [[package]] 592 | name = "roman-numerals-py" 593 | version = "3.1.0" 594 | description = "Manipulate well-formed Roman numerals" 595 | optional = true 596 | python-versions = ">=3.9" 597 | groups = ["main"] 598 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 599 | files = [ 600 | {file = "roman_numerals_py-3.1.0-py3-none-any.whl", hash = "sha256:9da2ad2fb670bcf24e81070ceb3be72f6c11c440d73bd579fbeca1e9f330954c"}, 601 | {file = "roman_numerals_py-3.1.0.tar.gz", hash = "sha256:be4bf804f083a4ce001b5eb7e3c0862479d10f94c936f6c4e5f250aa5ff5bd2d"}, 602 | ] 603 | 604 | [package.extras] 605 | lint = ["mypy (==1.15.0)", "pyright (==1.1.394)", "ruff (==0.9.7)"] 606 | test = ["pytest (>=8)"] 607 | 608 | [[package]] 609 | name = "six" 610 | version = "1.17.0" 611 | description = "Python 2 and 3 compatibility utilities" 612 | optional = false 613 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" 614 | groups = ["main"] 615 | files = [ 616 | {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, 617 | {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, 618 | ] 619 | 620 | [[package]] 621 | name = "snowballstemmer" 622 | version = "2.2.0" 623 | description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." 624 | optional = true 625 | python-versions = "*" 626 | groups = ["main"] 627 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 628 | files = [ 629 | {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, 630 | {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, 631 | ] 632 | 633 | [[package]] 634 | name = "sphinx" 635 | version = "8.2.3" 636 | description = "Python documentation generator" 637 | optional = true 638 | python-versions = ">=3.11" 639 | groups = ["main"] 640 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 641 | files = [ 642 | {file = "sphinx-8.2.3-py3-none-any.whl", hash = "sha256:4405915165f13521d875a8c29c8970800a0141c14cc5416a38feca4ea5d9b9c3"}, 643 | {file = "sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348"}, 644 | ] 645 | 646 | [package.dependencies] 647 | alabaster = ">=0.7.14" 648 | babel = ">=2.13" 649 | colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} 650 | docutils = ">=0.20,<0.22" 651 | imagesize = ">=1.3" 652 | Jinja2 = ">=3.1" 653 | packaging = ">=23.0" 654 | Pygments = ">=2.17" 655 | requests = ">=2.30.0" 656 | roman-numerals-py = ">=1.0.0" 657 | snowballstemmer = ">=2.2" 658 | sphinxcontrib-applehelp = ">=1.0.7" 659 | sphinxcontrib-devhelp = ">=1.0.6" 660 | sphinxcontrib-htmlhelp = ">=2.0.6" 661 | sphinxcontrib-jsmath = ">=1.0.1" 662 | sphinxcontrib-qthelp = ">=1.0.6" 663 | sphinxcontrib-serializinghtml = ">=1.1.9" 664 | 665 | [package.extras] 666 | docs = ["sphinxcontrib-websupport"] 667 | lint = ["betterproto (==2.0.0b6)", "mypy (==1.15.0)", "pypi-attestations (==0.0.21)", "pyright (==1.1.395)", "pytest (>=8.0)", "ruff (==0.9.9)", "sphinx-lint (>=0.9)", "types-Pillow (==10.2.0.20240822)", "types-Pygments (==2.19.0.20250219)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20241128)", "types-requests (==2.32.0.20241016)", "types-urllib3 (==1.26.25.14)"] 668 | test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "pytest-xdist[psutil] (>=3.4)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] 669 | 670 | [[package]] 671 | name = "sphinxcontrib-applehelp" 672 | version = "2.0.0" 673 | description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" 674 | optional = true 675 | python-versions = ">=3.9" 676 | groups = ["main"] 677 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 678 | files = [ 679 | {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, 680 | {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, 681 | ] 682 | 683 | [package.extras] 684 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 685 | standalone = ["Sphinx (>=5)"] 686 | test = ["pytest"] 687 | 688 | [[package]] 689 | name = "sphinxcontrib-devhelp" 690 | version = "2.0.0" 691 | description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" 692 | optional = true 693 | python-versions = ">=3.9" 694 | groups = ["main"] 695 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 696 | files = [ 697 | {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, 698 | {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, 699 | ] 700 | 701 | [package.extras] 702 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 703 | standalone = ["Sphinx (>=5)"] 704 | test = ["pytest"] 705 | 706 | [[package]] 707 | name = "sphinxcontrib-htmlhelp" 708 | version = "2.1.0" 709 | description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" 710 | optional = true 711 | python-versions = ">=3.9" 712 | groups = ["main"] 713 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 714 | files = [ 715 | {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, 716 | {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, 717 | ] 718 | 719 | [package.extras] 720 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 721 | standalone = ["Sphinx (>=5)"] 722 | test = ["html5lib", "pytest"] 723 | 724 | [[package]] 725 | name = "sphinxcontrib-jsmath" 726 | version = "1.0.1" 727 | description = "A sphinx extension which renders display math in HTML via JavaScript" 728 | optional = true 729 | python-versions = ">=3.5" 730 | groups = ["main"] 731 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 732 | files = [ 733 | {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, 734 | {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, 735 | ] 736 | 737 | [package.extras] 738 | test = ["flake8", "mypy", "pytest"] 739 | 740 | [[package]] 741 | name = "sphinxcontrib-qthelp" 742 | version = "2.0.0" 743 | description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" 744 | optional = true 745 | python-versions = ">=3.9" 746 | groups = ["main"] 747 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 748 | files = [ 749 | {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, 750 | {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, 751 | ] 752 | 753 | [package.extras] 754 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 755 | standalone = ["Sphinx (>=5)"] 756 | test = ["defusedxml (>=0.7.1)", "pytest"] 757 | 758 | [[package]] 759 | name = "sphinxcontrib-serializinghtml" 760 | version = "2.0.0" 761 | description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" 762 | optional = true 763 | python-versions = ">=3.9" 764 | groups = ["main"] 765 | markers = "python_version >= \"3.11\" and extra == \"docs\"" 766 | files = [ 767 | {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, 768 | {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, 769 | ] 770 | 771 | [package.extras] 772 | lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] 773 | standalone = ["Sphinx (>=5)"] 774 | test = ["pytest"] 775 | 776 | [[package]] 777 | name = "tomli" 778 | version = "2.2.1" 779 | description = "A lil' TOML parser" 780 | optional = false 781 | python-versions = ">=3.8" 782 | groups = ["dev"] 783 | markers = "python_version < \"3.11\"" 784 | files = [ 785 | {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, 786 | {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, 787 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, 788 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, 789 | {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, 790 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, 791 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, 792 | {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, 793 | {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, 794 | {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, 795 | {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, 796 | {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, 797 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, 798 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, 799 | {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, 800 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, 801 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, 802 | {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, 803 | {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, 804 | {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, 805 | {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, 806 | {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, 807 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, 808 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, 809 | {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, 810 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, 811 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, 812 | {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, 813 | {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, 814 | {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, 815 | {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, 816 | {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, 817 | ] 818 | 819 | [[package]] 820 | name = "types-requests" 821 | version = "2.32.0.20250306" 822 | description = "Typing stubs for requests" 823 | optional = false 824 | python-versions = ">=3.9" 825 | groups = ["dev"] 826 | files = [ 827 | {file = "types_requests-2.32.0.20250306-py3-none-any.whl", hash = "sha256:25f2cbb5c8710b2022f8bbee7b2b66f319ef14aeea2f35d80f18c9dbf3b60a0b"}, 828 | {file = "types_requests-2.32.0.20250306.tar.gz", hash = "sha256:0962352694ec5b2f95fda877ee60a159abdf84a0fc6fdace599f20acb41a03d1"}, 829 | ] 830 | 831 | [package.dependencies] 832 | urllib3 = ">=2" 833 | 834 | [[package]] 835 | name = "typing-extensions" 836 | version = "4.12.2" 837 | description = "Backported and Experimental Type Hints for Python 3.8+" 838 | optional = false 839 | python-versions = ">=3.8" 840 | groups = ["main", "dev"] 841 | files = [ 842 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 843 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 844 | ] 845 | markers = {main = "python_version < \"3.11\""} 846 | 847 | [[package]] 848 | name = "url-normalize" 849 | version = "1.4.3" 850 | description = "URL normalization for Python" 851 | optional = false 852 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 853 | groups = ["main"] 854 | files = [ 855 | {file = "url-normalize-1.4.3.tar.gz", hash = "sha256:d23d3a070ac52a67b83a1c59a0e68f8608d1cd538783b401bc9de2c0fac999b2"}, 856 | {file = "url_normalize-1.4.3-py2.py3-none-any.whl", hash = "sha256:ec3c301f04e5bb676d333a7fa162fa977ad2ca04b7e652bfc9fac4e405728eed"}, 857 | ] 858 | 859 | [package.dependencies] 860 | six = "*" 861 | 862 | [[package]] 863 | name = "urllib3" 864 | version = "2.3.0" 865 | description = "HTTP library with thread-safe connection pooling, file post, and more." 866 | optional = false 867 | python-versions = ">=3.9" 868 | groups = ["main", "dev"] 869 | files = [ 870 | {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, 871 | {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, 872 | ] 873 | 874 | [package.extras] 875 | brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] 876 | h2 = ["h2 (>=4,<5)"] 877 | socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] 878 | zstd = ["zstandard (>=0.18.0)"] 879 | 880 | [extras] 881 | docs = ["sphinx"] 882 | 883 | [metadata] 884 | lock-version = "2.1" 885 | python-versions = ">=3.9" 886 | content-hash = "d2efd209b18a9721cea94bb807cba67bab3f754a8e219b19993e531ea5d4d349" 887 | -------------------------------------------------------------------------------- /pypdns/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import argparse 4 | import json 5 | 6 | from .api import PyPDNS, PDNSRecord, TypedPDNSRecord # noqa 7 | from .errors import PDNSError, RateLimitError, UnauthorizedError, ForbiddenError, ServerError # noqa 8 | 9 | __all__ = ['PyPDNS', 'PDNSRecord', 'TypedPDNSRecord', 'PDNSError', 'RateLimitError', 'UnauthorizedError', 'ForbiddenError', 'ServerError'] 10 | 11 | 12 | def main() -> None: 13 | parser = argparse.ArgumentParser(description='Triggers a request againse CIRCL Passive DNS.') 14 | parser.add_argument('--username', required=True, help='The username of you account.') 15 | parser.add_argument('--password', required=True, help='The password of you account.') 16 | parser.add_argument('--query', required=True, help='The query, can be an IP. domain, hostname, TLD.') 17 | parser.add_argument('--rrtype', help='Filter the request based on the RR Type.') 18 | args = parser.parse_args() 19 | 20 | pdns = PyPDNS(basic_auth=(args.username, args.password)) 21 | 22 | for record in pdns.iter_query(q=args.query, filter_rrtype=args.rrtype if args.rrtype else None): 23 | print(json.dumps(record.record, indent=2)) 24 | -------------------------------------------------------------------------------- /pypdns/api.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from __future__ import annotations 4 | 5 | import json 6 | import logging 7 | 8 | from datetime import datetime, timezone 9 | from importlib.metadata import version 10 | from typing import Any, TypedDict, overload, Literal, Generator 11 | 12 | import requests 13 | from requests import Session, Response 14 | from dns.rdatatype import RdataType 15 | 16 | from pypdns.errors import PDNSError, UnauthorizedError, ForbiddenError, RateLimitError, ServerError, PDNSRecordTypeError 17 | 18 | try: 19 | import requests_cache 20 | from requests_cache import CachedSession 21 | from requests_cache.models import CachedResponse # type: ignore[attr-defined] 22 | HAS_CACHE = True 23 | except ImportError: 24 | HAS_CACHE = False 25 | 26 | logger = logging.getLogger("pypdns") 27 | 28 | sort_choice = ['count', 'rdata', 'rrname', 'rrtype', 'time_first', 'time_last'] 29 | 30 | 31 | class TypedPDNSRecord(TypedDict, total=False): 32 | '''A dict representing a Passive DNS record''' 33 | 34 | rrname: str 35 | rrtype: str 36 | rdata: str | list[str] 37 | time_first: int 38 | time_last: int 39 | count: int | None 40 | bailiwick: str | None 41 | sensor_id: str | None 42 | zone_time_first: int | None 43 | zone_time_last: int | None 44 | origin: str | None 45 | time_first_ms: int | None 46 | time_last_ms: int | None 47 | time_first_rfc3339: str | None 48 | time_last_rfc3339: str | None 49 | meta: dict[Any, Any] | None 50 | 51 | 52 | class PDNSRecord: 53 | '''A pythonesque Passive DNS record, 54 | see RFC for details: https://www.ietf.org/id/draft-dulaunoy-dnsop-passive-dns-cof-10.html 55 | ''' 56 | __slots__ = ('_raw_record', '_typed_record') 57 | 58 | def __init__(self, record: dict[str, str | int | bool | list[str] | dict[Any, Any] | None]): 59 | self._raw_record = record 60 | self._typed_record = self.__init_typed_record() 61 | 62 | @property 63 | def raw(self) -> dict[str, str | int | bool | list[str] | dict[Any, Any] | None]: 64 | '''The raw record''' 65 | return self._raw_record 66 | 67 | @property 68 | def record(self) -> TypedPDNSRecord: 69 | return self._typed_record 70 | 71 | def __init_typed_record(self) -> TypedPDNSRecord: 72 | '''The record as a python dictionary''' 73 | if not isinstance(self._raw_record['rrname'], str): 74 | raise PDNSRecordTypeError('rrname', 'str', self._raw_record["rrname"]) 75 | 76 | if not isinstance(self._raw_record['rrtype'], (str, int)): 77 | raise PDNSRecordTypeError('rrtype', 'str, int', self._raw_record["rrtype"]) 78 | 79 | if isinstance(self._raw_record['rrtype'], int): 80 | # Accordingly to the specs, the type can be a string OR an int. we normalize to str 81 | rrtype: str = RdataType(self._raw_record['rrtype']).name 82 | else: 83 | rrtype = RdataType[self._raw_record['rrtype']].name 84 | 85 | if not isinstance(self._raw_record['rdata'], (str, list)): 86 | raise PDNSRecordTypeError('rdata', 'str, list of string', self._raw_record["rdata"]) 87 | 88 | if not isinstance(self._raw_record['time_first'], int): 89 | raise PDNSRecordTypeError('time_first', 'int', self._raw_record["time_first"]) 90 | 91 | if not isinstance(self._raw_record['time_last'], int): 92 | raise PDNSRecordTypeError('time_last', 'int', self._raw_record["time_last"]) 93 | 94 | to_return: TypedPDNSRecord = {'rrname': self._raw_record['rrname'], 95 | 'rrtype': rrtype, 96 | 'rdata': self._raw_record['rdata'], 97 | 'time_first': self._raw_record["time_first"], 98 | 'time_last': self._raw_record["time_last"]} 99 | if 'count' in self._raw_record: 100 | if not isinstance(self._raw_record['count'], int): 101 | raise PDNSRecordTypeError('count', 'int', self._raw_record["count"]) 102 | to_return['count'] = self._raw_record["count"] 103 | 104 | if 'bailiwick' in self._raw_record: 105 | if not isinstance(self._raw_record['bailiwick'], str): 106 | raise PDNSRecordTypeError('bailiwick', 'str', self._raw_record["bailiwick"]) 107 | to_return['bailiwick'] = self._raw_record['bailiwick'] 108 | 109 | if 'sensor_id' in self._raw_record: 110 | if not isinstance(self._raw_record['sensor_id'], str): 111 | raise PDNSRecordTypeError('sensor_id', 'str', self._raw_record["sensor_id"]) 112 | to_return['sensor_id'] = self._raw_record['sensor_id'] 113 | 114 | if 'zone_time_first' in self._raw_record: 115 | if not isinstance(self._raw_record['zone_time_first'], int): 116 | raise PDNSRecordTypeError('zone_time_first', 'int', self._raw_record["zone_time_first"]) 117 | to_return['zone_time_first'] = self._raw_record['zone_time_first'] 118 | 119 | if 'zone_time_last' in self._raw_record: 120 | if not isinstance(self._raw_record['zone_time_last'], int): 121 | raise PDNSRecordTypeError('zone_time_last', 'int', self._raw_record["zone_time_last"]) 122 | to_return['zone_time_first'] = self._raw_record["zone_time_last"] 123 | 124 | if 'origin' in self._raw_record: 125 | if not isinstance(self._raw_record['origin'], str): 126 | raise PDNSRecordTypeError('origin', 'str', self._raw_record["origin"]) 127 | to_return['origin'] = self._raw_record["origin"] 128 | 129 | if 'time_first_ms' in self._raw_record: 130 | if not isinstance(self._raw_record['time_first_ms'], int): 131 | raise PDNSRecordTypeError('time_first_ms', 'int', self._raw_record["time_first_ms"]) 132 | to_return['time_first_ms'] = self._raw_record["time_first_ms"] 133 | 134 | if 'time_last_ms' in self._raw_record: 135 | if not isinstance(self._raw_record['time_last_ms'], int): 136 | raise PDNSRecordTypeError('time_last_ms', 'int', self._raw_record["time_last_ms"]) 137 | to_return['time_last_ms'] = self._raw_record['time_last_ms'] 138 | 139 | if 'time_first_rfc3339' in self._raw_record: 140 | if not isinstance(self._raw_record['time_first_rfc3339'], str): 141 | raise PDNSRecordTypeError('time_first_rfc3339', 'str', self._raw_record["time_first_rfc3339"]) 142 | to_return['time_first_rfc3339'] = self._raw_record['time_first_rfc3339'] 143 | 144 | if 'time_last_rfc3339' in self._raw_record: 145 | if not isinstance(self._raw_record['time_last_rfc3339'], str): 146 | raise PDNSRecordTypeError('time_last_rfc3339', 'str', self._raw_record["time_last_rfc3339"]) 147 | to_return['time_last_rfc3339'] = self._raw_record['time_last_rfc3339'] 148 | 149 | if 'meta' in self._raw_record: 150 | if not isinstance(self._raw_record['meta'], dict): 151 | raise PDNSRecordTypeError('meta', 'dict', self._raw_record["meta"]) 152 | to_return['meta'] = self._raw_record['meta'] 153 | return to_return 154 | 155 | @property 156 | def rrname(self) -> str: 157 | '''The name of the queried resource''' 158 | return self.record['rrname'] 159 | 160 | @property 161 | def rrtype(self) -> str: 162 | '''The resource record type as seen by the passive DNS''' 163 | return self.record['rrtype'] 164 | 165 | @property 166 | def rdata(self) -> str | list[str]: 167 | '''The resource records of the queried resource''' 168 | return self.record['rdata'] 169 | 170 | @property 171 | def time_first(self) -> int: 172 | '''The first time that the record / unique tuple (rrname, rrtype, rdata) 173 | has been seen by the passive DNS 174 | ''' 175 | return self.record['time_first'] 176 | 177 | @property 178 | def time_last(self) -> int: 179 | '''The last time that the record / unique tuple (rrname, rrtype, rdata) 180 | has been seen by the passive DNS 181 | ''' 182 | return self.record['time_last'] 183 | 184 | def __repr__(self) -> str: 185 | return f'PDNSRecord(rrname="{self.rrname}", rrtype="{self.rrtype}", rdata="{self.rdata}", time_first={self.time_first}, time_last={self.time_last})' 186 | 187 | @property 188 | def time_first_datetime(self) -> datetime: 189 | '''The first time that the record / unique tuple (rrname, rrtype, rdata) 190 | has been seen by the passive DNS, as a python datetime. 191 | ''' 192 | return datetime.fromtimestamp(self.time_first, timezone.utc) 193 | 194 | @property 195 | def time_last_datetime(self) -> datetime: 196 | '''The last time that the record / unique tuple (rrname, rrtype, rdata) 197 | has been seen by the passive DNS, as a python datetime. 198 | ''' 199 | return datetime.fromtimestamp(self.time_last, timezone.utc) 200 | 201 | @property 202 | def count(self) -> int | None: 203 | '''How many authoritative DNS answers were received at the Passive DNS Server's 204 | collectors with exactly the given set of values as answers 205 | ''' 206 | return self.record.get('count') 207 | 208 | @property 209 | def bailiwick(self) -> str | None: 210 | '''The best estimate of the apex of the zone where this data is authoritative''' 211 | return self.record.get('bailiwick') 212 | 213 | @property 214 | def sensor_id(self) -> str | None: 215 | '''The sensor information where the record was seen.''' 216 | return self.record.get('sensor_id') 217 | 218 | @property 219 | def zone_time_first(self) -> int | None: 220 | '''The first time that the unique tuple (rrname, rrtype, rdata) record 221 | has been seen via master file import 222 | ''' 223 | return self.record.get('zone_time_first') 224 | 225 | @property 226 | def zone_time_last(self) -> int | None: 227 | '''The last time that the unique tuple (rrname, rrtype, rdata) record 228 | has been seen via master file import 229 | ''' 230 | return self.record.get('zone_time_last') 231 | 232 | @property 233 | def origin(self) -> str | None: 234 | '''The resource origin of the Passive DNS response''' 235 | return self.record.get('origin') 236 | 237 | @property 238 | def time_first_ms(self) -> int | None: 239 | '''The first time that the record / unique tuple (rrname, rrtype, rdata) 240 | has been seen by the passive DNS, in miliseconds since 1st of January 1970 (UTC). 241 | ''' 242 | return self.record.get('time_first_ms') 243 | 244 | @property 245 | def time_last_ms(self) -> int | None: 246 | '''The first time that the record / unique tuple (rrname, rrtype, rdata) 247 | has been seen by the passive DNS, in miliseconds since 1st of January 1970 (UTC). 248 | ''' 249 | return self.record.get('time_last_ms') 250 | 251 | @property 252 | def time_first_rfc3339(self) -> str | None: 253 | return self.record.get('time_first_rfc3339') 254 | 255 | @property 256 | def time_last_rfc3339(self) -> str | None: 257 | return self.record.get('time_last_rfc3339') 258 | 259 | @property 260 | def meta(self) -> dict[Any, Any] | None: 261 | return self.record.get('meta') 262 | 263 | 264 | class PyPDNS: 265 | 266 | def __init__(self, url: str='https://www.circl.lu/pdns/query', 267 | basic_auth: tuple[str, str] | None=None, 268 | auth_token: str | None=None, 269 | enable_cache: bool=False, cache_expire_after: int=604800, 270 | cache_file: str='/tmp/pdns.cache', 271 | https_proxy_string: str | None=None, 272 | useragent: str | None=None, 273 | disable_active_query: bool=False, 274 | *, proxies: dict[str, str] | None=None): 275 | '''Connector to Passive DNS 276 | 277 | :param url: The URL of the service 278 | :param basic_auth: HTTP basic auth to cnnect to the service: ("username", "password") 279 | :param auth_token: HTTP basic auth but the token 280 | :param enable_cache: Cache responses locally 281 | :param cache_file: The file to cache the responses to 282 | :param https_proxy_string: The HTTP proxy to connect to the service (deprecated, use proxies instead) 283 | :param useragent: User Agent to submit to the server 284 | :param disable_active_query: THe passive DNS will attempt to resolve the request by default. Set to True if you don't want that. 285 | :param proxies: The proxies to use to connect to Passive DNS - More details: https://requests.readthedocs.io/en/latest/user/advanced/#proxies 286 | ''' 287 | 288 | self.url = url 289 | 290 | if enable_cache and not HAS_CACHE: 291 | raise PDNSError('Please install requests_cache if you want to use the caching capabilities.') 292 | 293 | self.session: CachedSession | Session 294 | if enable_cache is True: 295 | requests_cache.install_cache(cache_file, backend='sqlite', expire_after=cache_expire_after) 296 | self.session = requests_cache.CachedSession() 297 | else: 298 | self.session = requests.Session() 299 | self.session.headers['user-agent'] = useragent if useragent else f'PyPDNS / {version("pypdns")}' 300 | 301 | if basic_auth is not None: 302 | # basic_auth has do be a tuple ('user_name', 'password') 303 | self.session.auth = basic_auth 304 | elif auth_token is not None: 305 | self.session.headers.update({'Authorization': auth_token}) 306 | else: 307 | # No authentication defined. 308 | pass 309 | 310 | if disable_active_query: 311 | self.session.headers.update({'dribble-disable-active-query': '1'}) 312 | 313 | if https_proxy_string is not None: 314 | proxies = {'https': https_proxy_string} 315 | 316 | if proxies: 317 | self.session.proxies.update(proxies) 318 | 319 | def iter_query(self, q: str, 320 | filter_rrtype: str | None=None, 321 | break_on_errors: bool=False) -> Generator[PDNSRecord, None, dict[str, str | int] | None]: 322 | '''Iterate over all the recording matching your request, useful if there are a lot. 323 | Note: the order is non-deterministic. 324 | 325 | :param q: The query 326 | :param filter_rrtype: The filter, must be a valid RR Type or the response will be empty. 327 | :param break_on_errors: If there is an error, stop iterating and break immediately 328 | ''' 329 | cursor = -1 330 | query_headers = {'dribble-paginate-count': '50'} 331 | if filter_rrtype: 332 | query_headers['dribble-filter-rrtype'] = filter_rrtype 333 | while True: 334 | if cursor > 0: 335 | query_headers['dribble-paginate-cursor'] = str(cursor) 336 | response: Response | CachedResponse = self.session.get(f'{self.url}/{q}', timeout=15, headers=query_headers) 337 | if response.status_code != 200: 338 | self._handle_http_error(response) 339 | if break_on_errors: 340 | if e := self._handle_dribble_errors(response): 341 | return e 342 | 343 | for line in response.text.split('\n'): 344 | if len(line) == 0: 345 | continue 346 | try: 347 | if isinstance(response, CachedResponse) and response.from_cache is True: 348 | logger.debug("from cache query() q=[%s]", q) 349 | obj = json.loads(line) 350 | except Exception: 351 | logger.exception("except query() q=[%s]", q) 352 | raise PDNSError(f'Unable to decode JSON object: {line}') 353 | yield PDNSRecord(obj) 354 | 355 | if 'x-dribble-cursor' in response.headers: 356 | cursor = int(response.headers['x-dribble-cursor']) 357 | else: 358 | return None 359 | 360 | def _query(self, q: str, sort_by: str = 'time_last', 361 | *, 362 | filter_rrtype: str | None=None) -> tuple[list[dict[str, str | int | bool | list[str] | dict[Any, Any] | None]], 363 | dict[str, str | int]]: 364 | '''Internal method running a non-paginated query, can be sorted.''' 365 | logger.debug("start query() q=[%s]", q) 366 | if sort_by not in sort_choice: 367 | raise PDNSError(f'You can only sort by {", ".join(sort_choice)}') 368 | query_headers = {} 369 | if filter_rrtype: 370 | query_headers['dribble-filter-rrtype'] = filter_rrtype 371 | response: Response | CachedResponse = self.session.get(f'{self.url}/{q}', timeout=15, headers=query_headers) 372 | if response.status_code != 200: 373 | self._handle_http_error(response) 374 | errors = self._handle_dribble_errors(response) 375 | to_return = [] 376 | for line in response.text.split('\n'): 377 | if len(line) == 0: 378 | continue 379 | try: 380 | if isinstance(response, CachedResponse) and response.from_cache is True: 381 | logger.debug("from cache query() q=[%s]", q) 382 | obj = json.loads(line) 383 | except Exception: 384 | logger.exception("except query() q=[%s]", q) 385 | raise PDNSError(f'Unable to decode JSON object: {line}') 386 | to_return.append(obj) 387 | to_return = sorted(to_return, key=lambda k: k[sort_by]) 388 | return to_return, errors 389 | 390 | @overload 391 | def rfc_query(self, q: str, /, 392 | *, 393 | sort_by: str = 'time_last', 394 | filter_rrtype: str | None= None, 395 | with_errors: Literal[True]) -> tuple[list[PDNSRecord], dict[str, str | int]]: 396 | pass 397 | 398 | @overload 399 | def rfc_query(self, q: str, /, 400 | *, 401 | sort_by: str = 'time_last', 402 | filter_rrtype: str | None= None, 403 | with_errors: Literal[False]) -> list[PDNSRecord]: 404 | pass 405 | 406 | def rfc_query(self, q: str, /, 407 | *, 408 | sort_by: str = 'time_last', 409 | filter_rrtype: str | None= None, 410 | with_errors: bool=False) -> list[PDNSRecord] | tuple[list[PDNSRecord], dict[str, str | int]]: 411 | '''Triggers a non-paginated query, can be sorted but will raise an error if the response is too big. 412 | 413 | :param q: The query 414 | :param sort_by: The key to use to sort the records 415 | :param filter_rrtype: The filter, must be a valid RR Type or the response will be enpty. 416 | :param with_errors: Returns the errors (if any) 417 | ''' 418 | records, errors = self._query(q, sort_by, filter_rrtype=filter_rrtype) 419 | to_return_records = [PDNSRecord(record) for record in records] 420 | if not with_errors: 421 | return to_return_records 422 | return to_return_records, errors 423 | 424 | def query(self, q: str, sort_by: str = 'time_last', timeout: int | None = None) -> list[dict[str, Any]]: 425 | '''This method (almost) returns the response from the server but turns the times into python datetime. 426 | It was a bad design decision years ago. Use rfc_query instead for something saner. 427 | This method is deprecated. 428 | ''' 429 | records, errors = self._query(q, sort_by) 430 | for record in records: 431 | record['time_first'] = datetime.fromtimestamp(record['time_first'], timezone.utc) # type: ignore 432 | record['time_last'] = datetime.fromtimestamp(record['time_last'], timezone.utc) # type: ignore 433 | return records 434 | 435 | def _handle_dribble_errors(self, response: requests.Response) -> dict[str, str | int]: 436 | if 'x-dribble-errors' in response.headers: 437 | return json.loads(response.headers['x-dribble-errors']) 438 | return {} 439 | 440 | @staticmethod 441 | def _handle_http_error(response: requests.Response) -> None: 442 | if response.status_code == 401: 443 | raise UnauthorizedError("Not authenticated: is authentication correct?") 444 | if response.status_code == 403: 445 | raise ForbiddenError("Not authorized to access resource") 446 | if response.status_code == 429: 447 | raise RateLimitError("Quota exhausted") 448 | if 500 <= response.status_code < 600: 449 | raise ServerError("Server error") 450 | raise PDNSError("Something went wrong") 451 | -------------------------------------------------------------------------------- /pypdns/errors.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any 4 | 5 | 6 | class PDNSError(Exception): 7 | pass 8 | 9 | 10 | class RateLimitError(PDNSError): 11 | pass 12 | 13 | 14 | class UnauthorizedError(PDNSError): 15 | pass 16 | 17 | 18 | class ForbiddenError(PDNSError): 19 | pass 20 | 21 | 22 | class ServerError(PDNSError): 23 | pass 24 | 25 | 26 | class PDNSRecordTypeError(PDNSError): 27 | 28 | def __init__(self, field_name: str, expected_field_type: str, field: Any): 29 | self.message = f'Invalid record. {field_name} must be a {expected_field_type}, got {type(field)} - {field}' 30 | -------------------------------------------------------------------------------- /pypdns/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CIRCL/PyPDNS/ad8ea6f9a4c09d12b51a41f555ee27ac1f0b085e/pypdns/py.typed -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "pypdns" 3 | version = "2.2.8" 4 | description = "Python API for PDNS." 5 | authors = [ 6 | {name="Raphaël Vinot", email="raphael.vinot@circl.lu"} 7 | ] 8 | license = "GPL-3.0+" 9 | readme = "README.md" 10 | requires-python = ">=3.9" 11 | 12 | dynamic = [ "classifiers" ] 13 | 14 | dependencies = [ 15 | "requests-cache (>=1.2.1)", 16 | "dnspython (>=2.7.0)" 17 | ] 18 | 19 | [project.urls] 20 | repository = "https://github.com/CIRCL/PyPDNS" 21 | documentation = "https://pypdns.readthedocs.io" 22 | 23 | [tool.poetry] 24 | classifiers = [ 25 | 'Development Status :: 5 - Production/Stable', 26 | 'Environment :: Console', 27 | 'Intended Audience :: Science/Research', 28 | 'Intended Audience :: Telecommunications Industry', 29 | 'Topic :: Security', 30 | 'Topic :: Internet' 31 | ] 32 | 33 | [project.scripts] 34 | pdns = 'pypdns:main' 35 | 36 | [project.optional-dependencies] 37 | docs = [ "sphinx (>=8.2.3) ; python_version >= \"3.11\"" ] 38 | 39 | [tool.poetry.group.dev.dependencies] 40 | mypy = "^1.15.0" 41 | types-requests = "^2.32.0.20250306" 42 | pytest = "^8.3.5" 43 | 44 | [build-system] 45 | requires = ["poetry-core>=2.0"] 46 | build-backend = "poetry.core.masonry.api" 47 | -------------------------------------------------------------------------------- /tests/test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import annotations 4 | 5 | 6 | import os 7 | import unittest 8 | 9 | from pypdns import PyPDNS, UnauthorizedError 10 | 11 | 12 | class TestBasic(unittest.TestCase): 13 | 14 | login = os.environ.get('LOGIN', '') 15 | password = os.environ.get('PASSWORD', '') 16 | 17 | def test_not_auth(self) -> None: 18 | x = PyPDNS(basic_auth=('username', 'yourpassword')) 19 | with self.assertRaises(UnauthorizedError): 20 | x.query('www.microsoft.com') 21 | 22 | def test_auth(self) -> None: 23 | x = PyPDNS(basic_auth=(self.login, self.password)) 24 | for i in x.iter_query('circl.lu', filter_rrtype='A'): 25 | self.assertEqual(i.rrname, '185.194.93.14') 26 | for i in x.iter_query('circl.lu', filter_rrtype='AAAA'): 27 | self.assertEqual(i.rrname, '2a00:5980:93::14') 28 | ns_records = [i for i in x.iter_query('circl.lu', filter_rrtype='NS')] 29 | self.assertEqual(len(ns_records), 8, ns_records) 30 | sorted_query = x.query('circl.lu', sort_by='rrname') 31 | self.assertEqual(sorted_query[0]['rrname'], '10 cppy.circl.lu') 32 | 33 | 34 | if __name__ == '__main__': 35 | unittest.main() 36 | --------------------------------------------------------------------------------