diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4791a53 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,37 @@ +name: CI + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + +jobs: + test: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: ["3.10"] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1 + with: + version: 1.7.1 + virtualenvs-create: true + virtualenvs-in-project: true + + - name: Install dependencies + run: | + poetry install + + - name: Run linting + run: | + make lint \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e5c8165..1e058c7 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -32,7 +32,7 @@ jobs: - name: Build package run: python -m build - + - name: Create Release id: create_release uses: actions/create-release@v1 @@ -43,7 +43,7 @@ jobs: release_name: Release v${{ env.VERSION }} draft: false prerelease: false - + - name: Upload Wheel uses: actions/upload-release-asset@v1 env: @@ -53,7 +53,7 @@ jobs: asset_path: ./dist/zmapsdk-${{ env.VERSION }}-py3-none-any.whl asset_name: zmapsdk-${{ env.VERSION }}-py3-none-any.whl asset_content_type: application/octet-stream - + - name: Upload Source uses: actions/upload-release-asset@v1 env: @@ -63,7 +63,7 @@ jobs: asset_path: ./dist/zmapsdk-${{ env.VERSION }}.tar.gz asset_name: zmapsdk-${{ env.VERSION }}.tar.gz asset_content_type: application/gzip - + - name: Publish to PyPI env: TWINE_USERNAME: __token__ diff --git a/.gitignore b/.gitignore index 2145900..d1086e3 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,6 @@ Thumbs.db *.conf test_results/ scan_results/ + +# docs build +docs/_build/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..676ec39 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,23 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-toml +- repo: https://github.com/psf/black + rev: 24.2.0 + hooks: + - id: black + language_version: python3 +- repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort +- repo: https://github.com/asottile/pyupgrade + rev: v3.15.1 + hooks: + - id: pyupgrade + args: [--py311-plus] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..54ffd5b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,66 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, gender identity and expression, level of experience, +education, socio-economic status, nationality, personal appearance, race, +religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting the project team at {{ email }}. All +complaints will be reviewed and investigated and will result in a response that +is deemed necessary and appropriate to the circumstances. The project team is +obligated to maintain confidentiality with regard to the reporter of an incident. +Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..73a2388 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +.PHONY: install format lint clean + +install: + poetry install + pre-commit install + +format: + poetry run isort zmapsdk + poetry run black zmapsdk + +lint: + poetry run isort --check-only --diff . + poetry run black --check . + poetry run pyupgrade --py311-plus $$(find . -name '*.py' -not -path './.venv/*') + +clean: + rm -rf build/ + rm -rf dist/ + rm -rf *.egg-info + find . -type d -name __pycache__ -exec rm -rf {} + + find . -type f -name "*.pyc" -delete diff --git a/__init__.py b/__init__.py index 91bfb8a..1e67d56 100644 --- a/__init__.py +++ b/__init__.py @@ -1,8 +1,21 @@ -""" -ZMap SDK - Python SDK for the ZMap network scanner -""" - -from .zmapsdk import ZMap, ZMapError, ZMapCommandError - -__version__ = "0.1.0" -__all__ = ["ZMap", "ZMapError", "ZMapCommandError"] \ No newline at end of file +""" +ZMap SDK - Python SDK for the ZMap network scanner +""" + +from pathlib import Path + +import tomli + +from .zmapsdk import ZMap, ZMapCommandError, ZMapError + +# read version from pyproject.toml +try: + _PYPROJECT_PATH = Path(__file__).parent.parent / "pyproject.toml" + with open(_PYPROJECT_PATH, "rb") as f: + _PYPROJECT = tomli.load(f) + __version__ = _PYPROJECT["project"]["version"] +except (FileNotFoundError, KeyError, tomli.TOMLDecodeError): + # fallback for something went wrong + __version__ = "0.0.0" + +__all__ = ["ZMap", "ZMapError", "ZMapCommandError"] diff --git a/build-package.sh b/build-package.sh index daba47a..9b17f62 100644 --- a/build-package.sh +++ b/build-package.sh @@ -14,4 +14,4 @@ python -m build echo "Installing package..." python -m pip install -e . -echo "Build and installation complete!" \ No newline at end of file +echo "Build and installation complete!" diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..f5f8ad8 --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,52 @@ +# Configuration file for the Sphinx documentation builder. +# +# For the full list of built-in configuration values, see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Project information ----------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#project-information + +import tomlkit + + +def _get_project_details(): + """Get project meta details.""" + with open("../pyproject.toml") as pyproject: + content = pyproject.read() + + return tomlkit.parse(content)["tool"]["poetry"] + + +pkg_meta = _get_project_details() +project = str(pkg_meta["name"]) +version = str(pkg_meta["version"]) +copyright = "2025, Happy Hacking Space" +author = "Happy Hacking Space" +release = version + + +# -- General configuration --------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration +autoclass_content = "class" + +extensions = [ + "sphinx.ext.autodoc", + "sphinx.ext.viewcode", + "sphinx.ext.doctest", + "autoapi.extension", +] + +master_doc = "index" + +# autoapi +autoapi_dirs = ["../../zmapsdk/"] +autoapi_type = "python" +templates_path = ["_templates"] +exclude_patterns = [".DS_Store", "_build", "Thumbs.db"] + + +# -- Options for HTML output ------------------------------------------------- +# https://www.sphinx-doc.org/en/master/usage/configuration.html#options-for-html-output + +html_theme = "sphinx_rtd_theme" +html_static_path = ["_static"] diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..9276b72 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,16 @@ +.. ZMap-SDK documentation master file, created by + sphinx-quickstart on Fri Apr 25 17:32:20 2025. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +ZMap-SDK documentation +====================== + +Add your content using ``reStructuredText`` syntax. See the +`reStructuredText `_ +documentation for details. + + +.. toctree:: + :maxdepth: 2 + :caption: Contents: diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..954237b --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,35 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.https://www.sphinx-doc.org/ + exit /b 1 +) + +if "%1" == "" goto help + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% + +:end +popd diff --git a/examples/advanced-scan.py b/examples/advanced-scan.py index 6aded28..09c6b2b 100644 --- a/examples/advanced-scan.py +++ b/examples/advanced-scan.py @@ -4,9 +4,9 @@ config = ZMapScanConfig( target_port=80, bandwidth="10M", # Set bandwidth limit to 10 Mbps - rate=1000, # Limit to 1000 packets per second + rate=1000, # Limit to 1000 packets per second cooldown_time=5, # Wait 5 seconds between scan attempts - retries=2 # Retry failed connections twice + retries=2, # Retry failed connections twice ) zmap = ZMap() @@ -19,7 +19,7 @@ "192.168.2.0/24", ], output_file="scan_results.csv", - output_fields=["saddr", "daddr", "sport", "dport", "seqnum", "timestamp"] + output_fields=["saddr", "daddr", "sport", "dport", "seqnum", "timestamp"], ) # Print detailed results diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..a6e2017 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1589 @@ +# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. + +[[package]] +name = "alabaster" +version = "1.0.0" +description = "A light, configurable Sphinx theme" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b"}, + {file = "alabaster-1.0.0.tar.gz", hash = "sha256:c00dca57bca26fa62a6d7d0a9fcce65f3e026e9bfe33e9c538fd3fbb2144fd9e"}, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "anyio" +version = "4.5.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "anyio-4.5.2-py3-none-any.whl", hash = "sha256:c011ee36bc1e8ba40e5a81cb9df91925c218fe9b778554e0b56a21e1b5d4716f"}, + {file = "anyio-4.5.2.tar.gz", hash = "sha256:23009af4ed04ce05991845451e11ef02fc7c5ed29179ac9a420e5ad0ac7ddc5b"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7.4,<8.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21.0b1) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\""] +trio = ["trio (>=0.26.1)"] + +[[package]] +name = "astroid" +version = "3.3.9" +description = "An abstract syntax tree for Python with inference support." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248"}, + {file = "astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550"}, +] + +[package.dependencies] +typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} + +[[package]] +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, +] + +[package.extras] +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\""] + +[[package]] +name = "black" +version = "25.1.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, + {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, + {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, + {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, + {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, + {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, + {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, + {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, + {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, + {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, + {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, + {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, + {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, + {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, + {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, + {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, + {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, + {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, + {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, + {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, + {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, + {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.10)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "certifi" +version = "2025.1.31" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, + {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, +] + +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.1" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, + {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, + {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, + {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, + {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, + {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, + {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, + {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, + {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, + {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, +] + +[[package]] +name = "click" +version = "8.1.8" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["main", "dev"] +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] +markers = {main = "platform_system == \"Windows\""} + +[[package]] +name = "distlib" +version = "0.3.9" +description = "Distribution utilities" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] + +[[package]] +name = "docutils" +version = "0.21.2" +description = "Docutils -- Python Documentation Utilities" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, + {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, +] + +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +markers = "python_version < \"3.11\"" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.115.12" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"}, + {file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.40.0,<0.47.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=3.1.5)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.18)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] + +[[package]] +name = "filelock" +version = "3.18.0" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2) ; python_version < \"3.11\""] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httpcore" +version = "1.0.8" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"}, + {file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<1.0)"] + +[[package]] +name = "httpx" +version = "0.28.1" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"}, + {file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" + +[package.extras] +brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "identify" +version = "2.6.10" +description = "File identification library for Python" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25"}, + {file = "identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8"}, +] + +[package.extras] +license = ["ukkonen"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["main", "dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + +[[package]] +name = "imagesize" +version = "1.4.1" +description = "Getting image size from png/jpeg/jpeg2000/gif file" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +groups = ["dev"] +files = [ + {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, + {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, +] + +[[package]] +name = "isort" +version = "6.0.1" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.9.0" +groups = ["dev"] +files = [ + {file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, + {file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, +] + +[package.extras] +colors = ["colorama"] +plugins = ["setuptools"] + +[[package]] +name = "jinja2" +version = "3.1.6" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markupsafe" +version = "3.0.2" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, + {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "packaging" +version = "24.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.7" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"}, + {file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] + +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pre-commit" +version = "4.2.0" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd"}, + {file = "pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + +[[package]] +name = "psutil" +version = "7.0.0" +description = "Cross-platform lib for process and system monitoring in Python. NOTE: the syntax of this script MUST be kept compatible with Python 2.7." +optional = false +python-versions = ">=3.6" +groups = ["main"] +files = [ + {file = "psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25"}, + {file = "psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34"}, + {file = "psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993"}, + {file = "psutil-7.0.0-cp36-cp36m-win32.whl", hash = "sha256:84df4eb63e16849689f76b1ffcb36db7b8de703d1bc1fe41773db487621b6c17"}, + {file = "psutil-7.0.0-cp36-cp36m-win_amd64.whl", hash = "sha256:1e744154a6580bc968a0195fd25e80432d3afec619daf145b9e5ba16cc1d688e"}, + {file = "psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99"}, + {file = "psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553"}, + {file = "psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456"}, +] + +[package.extras] +dev = ["abi3audit", "black (==24.10.0)", "check-manifest", "coverage", "packaging", "pylint", "pyperf", "pypinfo", "pytest", "pytest-cov", "pytest-xdist", "requests", "rstcheck", "ruff", "setuptools", "sphinx", "sphinx_rtd_theme", "toml-sort", "twine", "virtualenv", "vulture", "wheel"] +test = ["pytest", "pytest-xdist", "setuptools"] + +[[package]] +name = "pydantic" +version = "2.10.6" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584"}, + {file = "pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.27.2" +typing-extensions = ">=4.12.2" + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""] + +[[package]] +name = "pydantic-core" +version = "2.27.2" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa"}, + {file = "pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a"}, + {file = "pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9"}, + {file = "pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4"}, + {file = "pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048"}, + {file = "pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474"}, + {file = "pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc"}, + {file = "pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0"}, + {file = "pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2"}, + {file = "pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4"}, + {file = "pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9"}, + {file = "pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b"}, + {file = "pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e"}, + {file = "pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee"}, + {file = "pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d3e8d504bdd3f10835468f29008d72fc8359d95c9c415ce6e767203db6127506"}, + {file = "pydantic_core-2.27.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:521eb9b7f036c9b6187f0b47318ab0d7ca14bd87f776240b90b21c1f4f149320"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85210c4d99a0114f5a9481b44560d7d1e35e32cc5634c656bc48e590b669b145"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d716e2e30c6f140d7560ef1538953a5cd1a87264c737643d481f2779fc247fe1"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f66d89ba397d92f840f8654756196d93804278457b5fbede59598a1f9f90b228"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:669e193c1c576a58f132e3158f9dfa9662969edb1a250c54d8fa52590045f046"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdbe7629b996647b99c01b37f11170a57ae675375b14b8c13b8518b8320ced5"}, + {file = "pydantic_core-2.27.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d262606bf386a5ba0b0af3b97f37c83d7011439e3dc1a9298f21efb292e42f1a"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:cabb9bcb7e0d97f74df8646f34fc76fbf793b7f6dc2438517d7a9e50eee4f14d"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_armv7l.whl", hash = "sha256:d2d63f1215638d28221f664596b1ccb3944f6e25dd18cd3b86b0a4c408d5ebb9"}, + {file = "pydantic_core-2.27.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:bca101c00bff0adb45a833f8451b9105d9df18accb8743b08107d7ada14bd7da"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win32.whl", hash = "sha256:f6f8e111843bbb0dee4cb6594cdc73e79b3329b526037ec242a3e49012495b3b"}, + {file = "pydantic_core-2.27.2-cp38-cp38-win_amd64.whl", hash = "sha256:fd1aea04935a508f62e0d0ef1f5ae968774a32afc306fb8545e06f5ff5cdf3ad"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c10eb4f1659290b523af58fa7cffb452a61ad6ae5613404519aee4bfbf1df993"}, + {file = "pydantic_core-2.27.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:ef592d4bad47296fb11f96cd7dc898b92e795032b4894dfb4076cfccd43a9308"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c61709a844acc6bf0b7dce7daae75195a10aac96a596ea1b776996414791ede4"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c5f762659e47fdb7b16956c71598292f60a03aa92f8b6351504359dbdba6cf"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4c9775e339e42e79ec99c441d9730fccf07414af63eac2f0e48e08fd38a64d76"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57762139821c31847cfb2df63c12f725788bd9f04bc2fb392790959b8f70f118"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0d1e85068e818c73e048fe28cfc769040bb1f475524f4745a5dc621f75ac7630"}, + {file = "pydantic_core-2.27.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:097830ed52fd9e427942ff3b9bc17fab52913b2f50f2880dc4a5611446606a54"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:044a50963a614ecfae59bb1eaf7ea7efc4bc62f49ed594e18fa1e5d953c40e9f"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:4e0b4220ba5b40d727c7f879eac379b822eee5d8fff418e9d3381ee45b3b0362"}, + {file = "pydantic_core-2.27.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5e4f4bb20d75e9325cc9696c6802657b58bc1dbbe3022f32cc2b2b632c3fbb96"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win32.whl", hash = "sha256:cca63613e90d001b9f2f9a9ceb276c308bfa2a43fafb75c8031c4f66039e8c6e"}, + {file = "pydantic_core-2.27.2-cp39-cp39-win_amd64.whl", hash = "sha256:77d1bca19b0f7021b3a982e6f903dcd5b2b06076def36a652e3907f596e29f67"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9"}, + {file = "pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c33939a82924da9ed65dab5a65d427205a73181d8098e79b6b426bdf8ad4e656"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:00bad2484fa6bda1e216e7345a798bd37c68fb2d97558edd584942aa41b7d278"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c817e2b40aba42bac6f457498dacabc568c3b7a986fc9ba7c8d9d260b71485fb"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:251136cdad0cb722e93732cb45ca5299fb56e1344a833640bf93b2803f8d1bfd"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2088237af596f0a524d3afc39ab3b036e8adb054ee57cbb1dcf8e09da5b29cc"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d4041c0b966a84b4ae7a09832eb691a35aec90910cd2dbe7a208de59be77965b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:8083d4e875ebe0b864ffef72a4304827015cff328a1be6e22cc850753bfb122b"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f141ee28a0ad2123b6611b6ceff018039df17f32ada8b534e6aa039545a3efb2"}, + {file = "pydantic_core-2.27.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7d0c8399fcc1848491f00e0314bd59fb34a9c008761bcb422a057670c3f65e35"}, + {file = "pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.19.1" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, + {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pytest" +version = "8.3.5" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"}, + {file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.26.0" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_asyncio-0.26.0-py3-none-any.whl", hash = "sha256:7b51ed894f4fbea1340262bdae5135797ebbe21d8638978e35d31c6d19f72fb0"}, + {file = "pytest_asyncio-0.26.0.tar.gz", hash = "sha256:c4df2a697648241ff39e7f0e4a73050b03f123f760673956cf0d72a4990e312f"}, +] + +[package.dependencies] +pytest = ">=8.2,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-sugar" +version = "1.0.0" +description = "pytest-sugar is a plugin for pytest that changes the default look and feel of pytest (e.g. progressbar, show tests that fail instantly)." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "pytest-sugar-1.0.0.tar.gz", hash = "sha256:6422e83258f5b0c04ce7c632176c7732cab5fdb909cb39cca5c9139f81276c0a"}, + {file = "pytest_sugar-1.0.0-py3-none-any.whl", hash = "sha256:70ebcd8fc5795dc457ff8b69d266a4e2e8a74ae0c3edc749381c64b5246c8dfd"}, +] + +[package.dependencies] +packaging = ">=21.3" +pytest = ">=6.2.0" +termcolor = ">=2.1.0" + +[package.extras] +dev = ["black", "flake8", "pre-commit"] + +[[package]] +name = "pyupgrade" +version = "3.19.1" +description = "A tool to automatically upgrade syntax for newer versions." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyupgrade-3.19.1-py2.py3-none-any.whl", hash = "sha256:8c5b0bfacae5ff30fa136a53eb7f22c34ba007450d4099e9da8089dabb9e67c9"}, + {file = "pyupgrade-3.19.1.tar.gz", hash = "sha256:d10e8c5f54b8327211828769e98d95d95e4715de632a3414f1eef3f51357b9e2"}, +] + +[package.dependencies] +tokenize-rt = ">=6.1.0" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "sniffio" +version = "1.3.1" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +groups = ["main", "dev"] +files = [ + {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, + {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, +] + +[[package]] +name = "snowballstemmer" +version = "2.2.0" +description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, + {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, +] + +[[package]] +name = "sphinx" +version = "8.0.0" +description = "Python documentation generator" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "sphinx-8.0.0-py3-none-any.whl", hash = "sha256:24e82f79de739d6a82785721d97eb9849641ebb5abd52a0fe31577d041c9723e"}, + {file = "sphinx-8.0.0.tar.gz", hash = "sha256:22551dc8fda6038a422bf1de59d91b31837b66afe45a3f30b2d8cc5aa9337343"}, +] + +[package.dependencies] +alabaster = ">=0.7.14" +babel = ">=2.13" +colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} +docutils = ">=0.20,<0.22" +imagesize = ">=1.3" +Jinja2 = ">=3.1" +packaging = ">=23.0" +Pygments = ">=2.17" +requests = ">=2.30.0" +snowballstemmer = ">=2.2" +sphinxcontrib-applehelp = "*" +sphinxcontrib-devhelp = "*" +sphinxcontrib-htmlhelp = ">=2.0.0" +sphinxcontrib-jsmath = "*" +sphinxcontrib-qthelp = "*" +sphinxcontrib-serializinghtml = ">=1.1.9" +tomli = {version = ">=2", markers = "python_version < \"3.11\""} + +[package.extras] +docs = ["sphinxcontrib-websupport"] +lint = ["flake8 (>=6.0)", "mypy (==1.11.0)", "pytest (>=6.0)", "ruff (==0.5.5)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-Pillow (==10.2.0.20240520)", "types-Pygments (==2.18.0.20240506)", "types-colorama (==0.4.15.20240311)", "types-defusedxml (==0.7.0.20240218)", "types-docutils (==0.21.0.20240724)", "types-requests (>=2.30.0)"] +test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] + +[[package]] +name = "sphinx-autoapi" +version = "3.6.0" +description = "Sphinx API documentation generator" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinx_autoapi-3.6.0-py3-none-any.whl", hash = "sha256:f3b66714493cab140b0e896d33ce7137654a16ac1edb6563edcbd47bf975f711"}, + {file = "sphinx_autoapi-3.6.0.tar.gz", hash = "sha256:c685f274e41d0842ae7e199460c322c4bd7fec816ccc2da8d806094b4f64af06"}, +] + +[package.dependencies] +astroid = [ + {version = ">=2.7", markers = "python_version < \"3.12\""}, + {version = ">=3", markers = "python_version >= \"3.12\""}, +] +Jinja2 = "*" +PyYAML = "*" +sphinx = ">=7.4.0" + +[[package]] +name = "sphinx-autobuild" +version = "2024.10.3" +description = "Rebuild Sphinx documentation on changes, with hot reloading in the browser." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinx_autobuild-2024.10.3-py3-none-any.whl", hash = "sha256:158e16c36f9d633e613c9aaf81c19b0fc458ca78b112533b20dafcda430d60fa"}, + {file = "sphinx_autobuild-2024.10.3.tar.gz", hash = "sha256:248150f8f333e825107b6d4b86113ab28fa51750e5f9ae63b59dc339be951fb1"}, +] + +[package.dependencies] +colorama = ">=0.4.6" +sphinx = "*" +starlette = ">=0.35" +uvicorn = ">=0.25" +watchfiles = ">=0.20" +websockets = ">=11" + +[package.extras] +test = ["httpx", "pytest (>=6)"] + +[[package]] +name = "sphinx-rtd-theme" +version = "3.0.2" +description = "Read the Docs theme for Sphinx" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, + {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, +] + +[package.dependencies] +docutils = ">0.18,<0.22" +sphinx = ">=6,<9" +sphinxcontrib-jquery = ">=4,<5" + +[package.extras] +dev = ["bump2version", "transifex-client", "twine", "wheel"] + +[[package]] +name = "sphinxcontrib-applehelp" +version = "2.0.0" +description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, + {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-devhelp" +version = "2.0.0" +description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, + {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "sphinxcontrib-htmlhelp" +version = "2.1.0" +description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, + {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["html5lib", "pytest"] + +[[package]] +name = "sphinxcontrib-jquery" +version = "4.1" +description = "Extension to include jQuery on newer Sphinx releases" +optional = false +python-versions = ">=2.7" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, + {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, +] + +[package.dependencies] +Sphinx = ">=1.8" + +[[package]] +name = "sphinxcontrib-jsmath" +version = "1.0.1" +description = "A sphinx extension which renders display math in HTML via JavaScript" +optional = false +python-versions = ">=3.5" +groups = ["dev"] +files = [ + {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, + {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, +] + +[package.extras] +test = ["flake8", "mypy", "pytest"] + +[[package]] +name = "sphinxcontrib-qthelp" +version = "2.0.0" +description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, + {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["defusedxml (>=0.7.1)", "pytest"] + +[[package]] +name = "sphinxcontrib-serializinghtml" +version = "2.0.0" +description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, + {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, +] + +[package.extras] +lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] +standalone = ["Sphinx (>=5)"] +test = ["pytest"] + +[[package]] +name = "starlette" +version = "0.44.0" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "starlette-0.44.0-py3-none-any.whl", hash = "sha256:19edeb75844c16dcd4f9dd72f22f9108c1539f3fc9c4c88885654fef64f85aea"}, + {file = "starlette-0.44.0.tar.gz", hash = "sha256:e35166950a3ccccc701962fe0711db0bc14f2ecd37c6f9fe5e3eae0cbaea8715"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"] + +[[package]] +name = "termcolor" +version = "3.0.1" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "termcolor-3.0.1-py3-none-any.whl", hash = "sha256:da1ed4ec8a5dc5b2e17476d859febdb3cccb612be1c36e64511a6f2485c10c69"}, + {file = "termcolor-3.0.1.tar.gz", hash = "sha256:a6abd5c6e1284cea2934443ba806e70e5ec8fd2449021be55c280f8a3731b611"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] + +[[package]] +name = "tokenize-rt" +version = "6.1.0" +description = "A wrapper around the stdlib `tokenize` which roundtrips." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "tokenize_rt-6.1.0-py2.py3-none-any.whl", hash = "sha256:d706141cdec4aa5f358945abe36b911b8cbdc844545da99e811250c0cee9b6fc"}, + {file = "tokenize_rt-6.1.0.tar.gz", hash = "sha256:e8ee836616c0877ab7c7b54776d2fefcc3bde714449a206762425ae114b53c86"}, +] + +[[package]] +name = "tomli" +version = "2.2.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] +markers = {dev = "python_version < \"3.12\""} + +[[package]] +name = "tomlkit" +version = "0.13.2" +description = "Style preserving TOML library" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, + {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, +] + +[[package]] +name = "typing-extensions" +version = "4.13.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"}, + {file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"}, +] +markers = {dev = "python_version < \"3.11\""} + +[[package]] +name = "urllib3" +version = "2.4.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"}, + {file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "uvicorn" +version = "0.33.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +groups = ["main", "dev"] +files = [ + {file = "uvicorn-0.33.0-py3-none-any.whl", hash = "sha256:2c30de4aeea83661a520abab179b24084a0019c0c1bbe137e5409f741cbde5f8"}, + {file = "uvicorn-0.33.0.tar.gz", hash = "sha256:3577119f82b7091cf4d3d4177bfda0bae4723ed92ab1439e8d779de880c9cc59"}, +] + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} + +[package.extras] +standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "virtualenv" +version = "20.30.0" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "virtualenv-20.30.0-py3-none-any.whl", hash = "sha256:e34302959180fca3af42d1800df014b35019490b119eba981af27f2fa486e5d6"}, + {file = "virtualenv-20.30.0.tar.gz", hash = "sha256:800863162bcaa5450a6e4d721049730e7f2dae07720e0902b0e4040bd6f9ada8"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8) ; platform_python_implementation == \"PyPy\" or platform_python_implementation == \"GraalVM\" or platform_python_implementation == \"CPython\" and sys_platform == \"win32\" and python_version >= \"3.13\"", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10) ; platform_python_implementation == \"CPython\""] + +[[package]] +name = "watchfiles" +version = "1.0.5" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"}, + {file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b551d4fb482fc57d852b4541f911ba28957d051c8776e79c3b4a51eb5e2a1b11"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:830aa432ba5c491d52a15b51526c29e4a4b92bf4f92253787f9726fe01519487"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a16512051a822a416b0d477d5f8c0e67b67c1a20d9acecb0aafa3aa4d6e7d256"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bfe0cbc787770e52a96c6fda6726ace75be7f840cb327e1b08d7d54eadc3bc85"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d363152c5e16b29d66cbde8fa614f9e313e6f94a8204eaab268db52231fe5358"}, + {file = "watchfiles-1.0.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7ee32c9a9bee4d0b7bd7cbeb53cb185cf0b622ac761efaa2eba84006c3b3a614"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29c7fd632ccaf5517c16a5188e36f6612d6472ccf55382db6c7fe3fcccb7f59f"}, + {file = "watchfiles-1.0.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8e637810586e6fe380c8bc1b3910accd7f1d3a9a7262c8a78d4c8fb3ba6a2b3d"}, + {file = "watchfiles-1.0.5-cp310-cp310-win32.whl", hash = "sha256:cd47d063fbeabd4c6cae1d4bcaa38f0902f8dc5ed168072874ea11d0c7afc1ff"}, + {file = "watchfiles-1.0.5-cp310-cp310-win_amd64.whl", hash = "sha256:86c0df05b47a79d80351cd179893f2f9c1b1cae49d96e8b3290c7f4bd0ca0a92"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:237f9be419e977a0f8f6b2e7b0475ababe78ff1ab06822df95d914a945eac827"}, + {file = "watchfiles-1.0.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0da39ff917af8b27a4bdc5a97ac577552a38aac0d260a859c1517ea3dc1a7c4"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cfcb3952350e95603f232a7a15f6c5f86c5375e46f0bd4ae70d43e3e063c13d"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68b2dddba7a4e6151384e252a5632efcaa9bc5d1c4b567f3cb621306b2ca9f63"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:95cf944fcfc394c5f9de794ce581914900f82ff1f855326f25ebcf24d5397418"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ecf6cd9f83d7c023b1aba15d13f705ca7b7d38675c121f3cc4a6e25bd0857ee9"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:852de68acd6212cd6d33edf21e6f9e56e5d98c6add46f48244bd479d97c967c6"}, + {file = "watchfiles-1.0.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5730f3aa35e646103b53389d5bc77edfbf578ab6dab2e005142b5b80a35ef25"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:18b3bd29954bc4abeeb4e9d9cf0b30227f0f206c86657674f544cb032296acd5"}, + {file = "watchfiles-1.0.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ba5552a1b07c8edbf197055bc9d518b8f0d98a1c6a73a293bc0726dce068ed01"}, + {file = "watchfiles-1.0.5-cp311-cp311-win32.whl", hash = "sha256:2f1fefb2e90e89959447bc0420fddd1e76f625784340d64a2f7d5983ef9ad246"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_amd64.whl", hash = "sha256:b6e76ceb1dd18c8e29c73f47d41866972e891fc4cc7ba014f487def72c1cf096"}, + {file = "watchfiles-1.0.5-cp311-cp311-win_arm64.whl", hash = "sha256:266710eb6fddc1f5e51843c70e3bebfb0f5e77cf4f27129278c70554104d19ed"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5eb568c2aa6018e26da9e6c86f3ec3fd958cee7f0311b35c2630fa4217d17f2"}, + {file = "watchfiles-1.0.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a04059f4923ce4e856b4b4e5e783a70f49d9663d22a4c3b3298165996d1377f"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e380c89983ce6e6fe2dd1e1921b9952fb4e6da882931abd1824c092ed495dec"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fe43139b2c0fdc4a14d4f8d5b5d967f7a2777fd3d38ecf5b1ec669b0d7e43c21"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ee0822ce1b8a14fe5a066f93edd20aada932acfe348bede8aa2149f1a4489512"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a0dbcb1c2d8f2ab6e0a81c6699b236932bd264d4cef1ac475858d16c403de74d"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a2014a2b18ad3ca53b1f6c23f8cd94a18ce930c1837bd891262c182640eb40a6"}, + {file = "watchfiles-1.0.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10f6ae86d5cb647bf58f9f655fcf577f713915a5d69057a0371bc257e2553234"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1a7bac2bde1d661fb31f4d4e8e539e178774b76db3c2c17c4bb3e960a5de07a2"}, + {file = "watchfiles-1.0.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ab626da2fc1ac277bbf752446470b367f84b50295264d2d313e28dc4405d663"}, + {file = "watchfiles-1.0.5-cp312-cp312-win32.whl", hash = "sha256:9f4571a783914feda92018ef3901dab8caf5b029325b5fe4558c074582815249"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_amd64.whl", hash = "sha256:360a398c3a19672cf93527f7e8d8b60d8275119c5d900f2e184d32483117a705"}, + {file = "watchfiles-1.0.5-cp312-cp312-win_arm64.whl", hash = "sha256:1a2902ede862969077b97523987c38db28abbe09fb19866e711485d9fbf0d417"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0b289572c33a0deae62daa57e44a25b99b783e5f7aed81b314232b3d3c81a11d"}, + {file = "watchfiles-1.0.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a056c2f692d65bf1e99c41045e3bdcaea3cb9e6b5a53dcaf60a5f3bd95fc9763"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9dca99744991fc9850d18015c4f0438865414e50069670f5f7eee08340d8b40"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:894342d61d355446d02cd3988a7326af344143eb33a2fd5d38482a92072d9563"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ab44e1580924d1ffd7b3938e02716d5ad190441965138b4aa1d1f31ea0877f04"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6f9367b132078b2ceb8d066ff6c93a970a18c3029cea37bfd7b2d3dd2e5db8f"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f2e55a9b162e06e3f862fb61e399fe9f05d908d019d87bf5b496a04ef18a970a"}, + {file = "watchfiles-1.0.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0125f91f70e0732a9f8ee01e49515c35d38ba48db507a50c5bdcad9503af5827"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:13bb21f8ba3248386337c9fa51c528868e6c34a707f729ab041c846d52a0c69a"}, + {file = "watchfiles-1.0.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:839ebd0df4a18c5b3c1b890145b5a3f5f64063c2a0d02b13c76d78fe5de34936"}, + {file = "watchfiles-1.0.5-cp313-cp313-win32.whl", hash = "sha256:4a8ec1e4e16e2d5bafc9ba82f7aaecfeec990ca7cd27e84fb6f191804ed2fcfc"}, + {file = "watchfiles-1.0.5-cp313-cp313-win_amd64.whl", hash = "sha256:f436601594f15bf406518af922a89dcaab416568edb6f65c4e5bbbad1ea45c11"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:2cfb371be97d4db374cba381b9f911dd35bb5f4c58faa7b8b7106c8853e5d225"}, + {file = "watchfiles-1.0.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a3904d88955fda461ea2531fcf6ef73584ca921415d5cfa44457a225f4a42bc1"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b7a21715fb12274a71d335cff6c71fe7f676b293d322722fe708a9ec81d91f5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dfd6ae1c385ab481766b3c61c44aca2b3cd775f6f7c0fa93d979ddec853d29d5"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b659576b950865fdad31fa491d31d37cf78b27113a7671d39f919828587b429b"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1909e0a9cd95251b15bff4261de5dd7550885bd172e3536824bf1cf6b121e200"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:832ccc221927c860e7286c55c9b6ebcc0265d5e072f49c7f6456c7798d2b39aa"}, + {file = "watchfiles-1.0.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85fbb6102b3296926d0c62cfc9347f6237fb9400aecd0ba6bbda94cae15f2b3b"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:15ac96dd567ad6c71c71f7b2c658cb22b7734901546cd50a475128ab557593ca"}, + {file = "watchfiles-1.0.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4b6227351e11c57ae997d222e13f5b6f1f0700d84b8c52304e8675d33a808382"}, + {file = "watchfiles-1.0.5-cp39-cp39-win32.whl", hash = "sha256:974866e0db748ebf1eccab17862bc0f0303807ed9cda465d1324625b81293a18"}, + {file = "watchfiles-1.0.5-cp39-cp39-win_amd64.whl", hash = "sha256:9848b21ae152fe79c10dd0197304ada8f7b586d3ebc3f27f43c506e5a52a863c"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f59b870db1f1ae5a9ac28245707d955c8721dd6565e7f411024fa374b5362d1d"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9475b0093767e1475095f2aeb1d219fb9664081d403d1dff81342df8cd707034"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc533aa50664ebd6c628b2f30591956519462f5d27f951ed03d6c82b2dfd9965"}, + {file = "watchfiles-1.0.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fed1cd825158dcaae36acce7b2db33dcbfd12b30c34317a88b8ed80f0541cc57"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:554389562c29c2c182e3908b149095051f81d28c2fec79ad6c8997d7d63e0009"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a74add8d7727e6404d5dc4dcd7fac65d4d82f95928bbee0cf5414c900e86773e"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cb1489f25b051a89fae574505cc26360c8e95e227a9500182a7fe0afcc500ce0"}, + {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"}, + {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "15.0.1" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"}, + {file = "websockets-15.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:5756779642579d902eed757b21b0164cd6fe338506a8083eb58af5c372e39d9a"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdfe3e2a29e4db3659dbd5bbf04560cea53dd9610273917799f1cde46aa725e"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c2529b320eb9e35af0fa3016c187dffb84a3ecc572bcee7c3ce302bfeba52bf"}, + {file = "websockets-15.0.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac1e5c9054fe23226fb11e05a6e630837f074174c4c2f0fe442996112a6de4fb"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:5df592cd503496351d6dc14f7cdad49f268d8e618f80dce0cd5a36b93c3fc08d"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:0a34631031a8f05657e8e90903e656959234f3a04552259458aac0b0f9ae6fd9"}, + {file = "websockets-15.0.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3d00075aa65772e7ce9e990cab3ff1de702aa09be3940d1dc88d5abf1ab8a09c"}, + {file = "websockets-15.0.1-cp310-cp310-win32.whl", hash = "sha256:1234d4ef35db82f5446dca8e35a7da7964d02c127b095e172e54397fb6a6c256"}, + {file = "websockets-15.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:39c1fec2c11dc8d89bba6b2bf1556af381611a173ac2b511cf7231622058af41"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:823c248b690b2fd9303ba00c4f66cd5e2d8c3ba4aa968b2779be9532a4dad431"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678999709e68425ae2593acf2e3ebcbcf2e69885a5ee78f9eb80e6e371f1bf57"}, + {file = "websockets-15.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d50fd1ee42388dcfb2b3676132c78116490976f1300da28eb629272d5d93e905"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d99e5546bf73dbad5bf3547174cd6cb8ba7273062a23808ffea025ecb1cf8562"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:66dd88c918e3287efc22409d426c8f729688d89a0c587c88971a0faa2c2f3792"}, + {file = "websockets-15.0.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dd8327c795b3e3f219760fa603dcae1dcc148172290a8ab15158cf85a953413"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8fdc51055e6ff4adeb88d58a11042ec9a5eae317a0a53d12c062c8a8865909e8"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:693f0192126df6c2327cce3baa7c06f2a117575e32ab2308f7f8216c29d9e2e3"}, + {file = "websockets-15.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:54479983bd5fb469c38f2f5c7e3a24f9a4e70594cd68cd1fa6b9340dadaff7cf"}, + {file = "websockets-15.0.1-cp311-cp311-win32.whl", hash = "sha256:16b6c1b3e57799b9d38427dda63edcbe4926352c47cf88588c0be4ace18dac85"}, + {file = "websockets-15.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:27ccee0071a0e75d22cb35849b1db43f2ecd3e161041ac1ee9d2352ddf72f065"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665"}, + {file = "websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5"}, + {file = "websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4"}, + {file = "websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597"}, + {file = "websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9"}, + {file = "websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675"}, + {file = "websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f"}, + {file = "websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d"}, + {file = "websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4"}, + {file = "websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa"}, + {file = "websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:5f4c04ead5aed67c8a1a20491d54cdfba5884507a48dd798ecaf13c74c4489f5"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abdc0c6c8c648b4805c5eacd131910d2a7f6455dfd3becab248ef108e89ab16a"}, + {file = "websockets-15.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a625e06551975f4b7ea7102bc43895b90742746797e2e14b70ed61c43a90f09b"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d591f8de75824cbb7acad4e05d2d710484f15f29d4a915092675ad3456f11770"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:47819cea040f31d670cc8d324bb6435c6f133b8c7a19ec3d61634e62f8d8f9eb"}, + {file = "websockets-15.0.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ac017dd64572e5c3bd01939121e4d16cf30e5d7e110a119399cf3133b63ad054"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:4a9fac8e469d04ce6c25bb2610dc535235bd4aa14996b4e6dbebf5e007eba5ee"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363c6f671b761efcb30608d24925a382497c12c506b51661883c3e22337265ed"}, + {file = "websockets-15.0.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2034693ad3097d5355bfdacfffcbd3ef5694f9718ab7f29c29689a9eae841880"}, + {file = "websockets-15.0.1-cp39-cp39-win32.whl", hash = "sha256:3b1ac0d3e594bf121308112697cf4b32be538fb1444468fb0a6ae4feebc83411"}, + {file = "websockets-15.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:b7643a03db5c95c799b89b31c036d5f27eeb4d259c798e878d6937d71832b1e4"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0c9e74d766f2818bb95f84c25be4dea09841ac0f734d1966f415e4edfc4ef1c3"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1009ee0c7739c08a0cd59de430d6de452a55e42d6b522de7aa15e6f67db0b8e1"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:76d1f20b1c7a2fa82367e04982e708723ba0e7b8d43aa643d3dcd404d74f1475"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f29d80eb9a9263b8d109135351caf568cc3f80b9928bccde535c235de55c22d9"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b359ed09954d7c18bbc1680f380c7301f92c60bf924171629c5db97febb12f04"}, + {file = "websockets-15.0.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:cad21560da69f4ce7658ca2cb83138fb4cf695a2ba3e475e0559e05991aa8122"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7f493881579c90fc262d9cdbaa05a6b54b3811c2f300766748db79f098db9940"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:47b099e1f4fbc95b701b6e85768e1fcdaf1630f3cbe4765fa216596f12310e2e"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f2b6de947f8c757db2db9c71527933ad0019737ec374a8a6be9a956786aaf9"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d08eb4c2b7d6c41da6ca0600c077e93f5adcfd979cd777d747e9ee624556da4b"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b826973a4a2ae47ba357e4e82fa44a463b8f168e1ca775ac64521442b19e87f"}, + {file = "websockets-15.0.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:21c1fa28a6a7e3cbdc171c694398b6df4744613ce9b36b1a498e816787e28123"}, + {file = "websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f"}, + {file = "websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee"}, +] + +[metadata] +lock-version = "2.1" +python-versions = ">=3.10" +content-hash = "95113d35aecf6ad1ef1e2791b09e306710391a1725b83a9719e37bf6faa11d5a" diff --git a/pyproject.toml b/pyproject.toml index 64c51a9..d1e158a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,40 +1,49 @@ [build-system] -requires = ["setuptools>=42", "wheel"] -build-backend = "setuptools.build_meta" +requires = ["poetry-core>=1.0.0"] +build-backend = "poetry.core.masonry.api" -[project] +[tool.poetry] name = "zmapsdk" -version = "0.1.3" +version = "0.1.4" description = "Python SDK for the ZMap network scanner" readme = "README.md" -authors = [ - {name = "Happy Hacking Space", email = "contact@happyhacking.space"} -] +authors = ["Happy Hacking Space "] license = "MIT" +packages = [{include = "zmapsdk"}] +repository = "https://github.com/HappyHackingSpace/ZmapSDK" classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Intended Audience :: System Administrators", + "Intended Audience :: System Administrators", "Intended Audience :: Information Technology", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", - "Programming Language :: Python :: 3.13", "Operating System :: POSIX :: Linux", "Topic :: System :: Networking", "Topic :: Security", ] -requires-python = ">=3.6" -dependencies = [ - "fastapi>=0.100.0", - "uvicorn>=0.22.0", - "psutil>=7.0.0" -] -[project.urls] -Repository = "https://github.com/HappyHackingSpace/ZmapSDK" \ No newline at end of file +[tool.poetry.dependencies] +python = ">=3.10" +fastapi = ">=0.100.0" +uvicorn = ">=0.22.0" +psutil = "^7.0.0" +click = ">=8.1.8,<9.0.0" +tomli = ">=2.0.1,<3.0.0" +tomlkit = ">=0.13.2,<0.14.0" +httpx = ">=0.28.1,<0.29.0" + +[tool.poetry.group.dev.dependencies] +black = ">=25.1.0" +isort = ">=6.0.1" +pyupgrade = ">=3.19.1" +pytest = ">=8.3.5,<9.0.0" +pytest-asyncio = ">=0.26.0,<0.27.0" +pytest-sugar = ">=1.0.0,<2.0.0" +pre-commit = ">=3.6.0" +sphinx = "==8.0.0" +sphinx-autoapi = ">=3.6.0,<4.0.0" +sphinx-autobuild = ">=2024.10.3,<2025.0.0" +sphinx-rtd-theme = ">=3.0.2,<4.0.0" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..cfde72a --- /dev/null +++ b/setup.cfg @@ -0,0 +1,27 @@ +[tool:pytest] +python_files = test*.py +addopts = --tb=native -p no:doctest -p no:warnings +norecursedirs = bin dist docs htmlcov script hooks node_modules +looponfailroots = zmapsdk tests +self-contained-html = true + +[coverage:run] +omit = + zmapsdk/cli.py +source = + zmapsdk + tests + +[black] +line-length = 100 +target-version = py311 +include = \.pyi?$ + +[isort] +profile = black +multi_line_output = 3 +line_length = 100 +include_trailing_comma = true +force_grid_wrap = 0 +use_parentheses = true +ensure_newline_before_comments = true diff --git a/setup.py b/setup.py index 516afbc..a1bad7a 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ -from setuptools import setup, find_packages +from setuptools import find_packages, setup -with open("README.md", "r", encoding="utf-8") as fh: +with open("README.md", encoding="utf-8") as fh: long_description = fh.read() setup( @@ -13,8 +13,8 @@ packages=find_packages(), install_requires=[ "pydantic>=1.8.0,<2.0.0", # For data validation - "fastapi>=0.68.0", # For REST API - "uvicorn>=0.15.0", # For serving the API + "fastapi>=0.68.0", # For REST API + "uvicorn>=0.15.0", # For serving the API ], extras_require={ "dev": [ @@ -43,4 +43,4 @@ "Topic :: System :: Networking", ], python_requires=">=3.7", -) \ No newline at end of file +) diff --git a/zmapsdk/__init__.py b/zmapsdk/__init__.py index fd34c09..4290d76 100644 --- a/zmapsdk/__init__.py +++ b/zmapsdk/__init__.py @@ -2,30 +2,37 @@ ZMap SDK - Python SDK for the ZMap network scanner """ -from .core import ZMap -from .exceptions import ZMapError, ZMapCommandError, ZMapConfigError, ZMapInputError, ZMapOutputError, ZMapParserError +from .api import APIServer +from .cli import main as cli_main from .config import ZMapScanConfig +from .core import ZMap +from .exceptions import ( + ZMapCommandError, + ZMapConfigError, + ZMapError, + ZMapInputError, + ZMapOutputError, + ZMapParserError, +) from .input import ZMapInput from .output import ZMapOutput -from .runner import ZMapRunner from .parser import ZMapParser -from .api import APIServer -from .cli import main as cli_main +from .runner import ZMapRunner __version__ = "0.1.2" __all__ = [ - "ZMap", - "ZMapError", + "ZMap", + "ZMapError", "ZMapCommandError", "ZMapConfigError", - "ZMapInputError", + "ZMapInputError", "ZMapOutputError", "ZMapParserError", "ZMapScanConfig", - "ZMapInput", + "ZMapInput", "ZMapOutput", "ZMapRunner", "ZMapParser", "APIServer", - "cli_main" -] \ No newline at end of file + "cli_main", +] diff --git a/zmapsdk/__main__.py b/zmapsdk/__main__.py index 8f189ed..96dd0db 100644 --- a/zmapsdk/__main__.py +++ b/zmapsdk/__main__.py @@ -3,7 +3,8 @@ """ import sys -from .cli import main + +from zmapsdk.cli import main if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + sys.exit(main()) diff --git a/zmapsdk/api.py b/zmapsdk/api.py index fcbef71..6cf4851 100644 --- a/zmapsdk/api.py +++ b/zmapsdk/api.py @@ -1,55 +1,26 @@ -""" -REST API module for ZMap SDK -""" - -from typing import List, Dict, Any, Optional -from pydantic import BaseModel, Field -import tempfile import os -from fastapi import FastAPI, HTTPException, BackgroundTasks -import uvicorn +import tempfile from contextlib import asynccontextmanager -import psutil -from .core import ZMap +import psutil +import uvicorn +from fastapi import FastAPI, HTTPException +from fastapi.responses import FileResponse as StarletteFileResponse + +from zmapsdk.schemas import ( + BlocklistRequest, + FileResponse, + ScanRequest, + ScanResult, + StandardBlocklistRequest, +) -# Models for API endpoints -class ScanRequest(BaseModel): - target_port: Optional[int] = Field(None, description="Port number to scan") - subnets: Optional[List[str]] = Field(None, description="List of subnets to scan") - output_file: Optional[str] = Field(None, description="Output file path") - blocklist_file: Optional[str] = Field(None, description="Path to blocklist file") - allowlist_file: Optional[str] = Field(None, description="Path to allowlist file") - bandwidth: Optional[str] = Field(None, description="Bandwidth cap for scan") - probe_module: Optional[str] = Field(None, description="Probe module to use") - rate: Optional[int] = Field(None, description="Packets per second to send") - seed: Optional[int] = Field(None, description="Random seed") - verbosity: Optional[int] = Field(None, description="Verbosity level") - return_results: Optional[bool] = Field(False, description="Return results directly in response instead of writing to file") - # Add other relevant parameters as needed - -class ScanResult(BaseModel): - scan_id: str - status: str - ips_found: Optional[List[str]] = None - output_file: Optional[str] = None - error: Optional[str] = None - -class BlocklistRequest(BaseModel): - subnets: List[str] - output_file: Optional[str] = None - -class StandardBlocklistRequest(BaseModel): - output_file: Optional[str] = None - -class FileResponse(BaseModel): - file_path: str - message: str - +from .core import ZMap # Scan tracking dictionary active_scans = {} + # Initialize ZMap instance for API @asynccontextmanager async def lifespan(app: FastAPI): @@ -64,7 +35,7 @@ async def lifespan(app: FastAPI): title="ZMap SDK API", description="REST API for ZMap network scanner", version="0.1.0", - lifespan=lifespan + lifespan=lifespan, ) @@ -74,89 +45,99 @@ async def root(): return { "name": "ZMap SDK API", "version": app.state.zmap.get_version(), - "description": "REST API for ZMap network scanner" + "description": "REST API for ZMap network scanner", } -@app.get("/probe-modules", tags=["Info"], response_model=List[str]) +@app.get("/probe-modules", tags=["Info"], response_model=list[str]) async def get_probe_modules(): """Get available probe modules""" return app.state.zmap.get_probe_modules() -@app.get("/output-modules", tags=["Info"], response_model=List[str]) +@app.get("/output-modules", tags=["Info"], response_model=list[str]) async def get_output_modules(): """Get available output modules""" return app.state.zmap.get_output_modules() -@app.get("/output-fields", tags=["Info"], response_model=List[str]) -async def get_output_fields(probe_module: Optional[str] = None): +@app.get("/output-fields", tags=["Info"], response_model=list[str]) +async def get_output_fields(probe_module: str | None = None): """Get available output fields for a probe module""" return app.state.zmap.get_output_fields(probe_module) -@app.get("/interfaces", tags=["Info"], response_model=List[str]) +@app.get("/interfaces", tags=["Info"], response_model=list[str]) async def get_interfaces(): """Get available network interfaces""" return [iface for iface in psutil.net_if_addrs().keys()] -@app.post("/blocklist", tags=["Input"], response_model=FileResponse) -async def create_blocklist(request: BlocklistRequest): +@app.post("/blocklist", tags=["Input"]) +async def create_blocklist(request: BlocklistRequest) -> FileResponse: """Create a blocklist file from a list of subnets""" try: # Use provided output file or create temporary one output_file = request.output_file if not output_file: - temp_fd, output_file = tempfile.mkstemp(prefix="zmap_blocklist_", suffix=".txt") + temp_fd, output_file = tempfile.mkstemp( + prefix="zmap_blocklist_", + suffix=".txt", + ) os.close(temp_fd) - + file_path = app.state.zmap.create_blocklist_file(request.subnets, output_file) - + return FileResponse( file_path=file_path, - message=f"Blocklist file created with {len(request.subnets)} subnets" + message=f"Blocklist file created with {len(request.subnets)} subnets", ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@app.post("/standard-blocklist", tags=["Input"], response_model=FileResponse) -async def generate_standard_blocklist(request: StandardBlocklistRequest): +@app.post("/standard-blocklist", tags=["Input"]) +async def generate_standard_blocklist( + request: StandardBlocklistRequest, +) -> FileResponse: """Generate a standard blocklist file""" try: # Use provided output file or create temporary one output_file = request.output_file if not output_file: - temp_fd, output_file = tempfile.mkstemp(prefix="zmap_std_blocklist_", suffix=".txt") + temp_fd, output_file = tempfile.mkstemp( + prefix="zmap_std_blocklist_", + suffix=".txt", + ) os.close(temp_fd) - + file_path = app.state.zmap.generate_standard_blocklist(output_file) - + return FileResponse( - file_path=file_path, - message="Standard blocklist file created" + file_path=file_path, message="Standard blocklist file created" ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) -@app.post("/allowlist", tags=["Input"], response_model=FileResponse) -async def create_allowlist(request: BlocklistRequest): +@app.post("/allowlist", tags=["Input"]) +async def create_allowlist(request: BlocklistRequest) -> FileResponse: """Create an allowlist file from a list of subnets""" try: # Use provided output file or create temporary one output_file = request.output_file if not output_file: - temp_fd, output_file = tempfile.mkstemp(prefix="zmap_allowlist_", suffix=".txt") + temp_fd, output_file = tempfile.mkstemp( + prefix="zmap_allowlist_", + suffix=".txt", + ) os.close(temp_fd) - + file_path = app.state.zmap.create_allowlist_file(request.subnets, output_file) - + return FileResponse( file_path=file_path, - message=f"Allowlist file created with {len(request.subnets)} subnets" + message=f"Allowlist file created with {len(request.subnets)} subnets", ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -170,23 +151,27 @@ async def sync_scan(scan_request: ScanRequest): if not output_file: temp_fd, output_file = tempfile.mkstemp(prefix="zmap_api_", suffix=".txt") os.close(temp_fd) - + try: # Convert model to dict and remove None values - params = {k: v for k, v in scan_request.dict().items() if v is not None and k != 'return_results'} - + params = { + k: v + for k, v in scan_request.dict().items() + if v is not None and k != "return_results" + } + # Ensure output file is set - params['output_file'] = output_file - + params["output_file"] = output_file + # Run scan synchronously results = app.state.zmap.scan(**params) - + # Return results directly return ScanResult( scan_id="direct_scan", status="completed", - ips_found=results - ) + ips_found=results, + ) except Exception as e: raise HTTPException(status_code=500, detail=str(e)) @@ -195,11 +180,11 @@ class APIServer: """ Server class for running the ZMap SDK API """ - + def __init__(self, host: str = "127.0.0.1", port: int = 8000): """ Initialize the API server - + Args: host: Host to bind the server to port: Port to bind the server to @@ -207,9 +192,9 @@ def __init__(self, host: str = "127.0.0.1", port: int = 8000): self.host = host self.port = port self.app = app - + def run(self): """ Run the API server """ - uvicorn.run(app, host=self.host, port=self.port) \ No newline at end of file + uvicorn.run(app, host=self.host, port=self.port) diff --git a/zmapsdk/cli.py b/zmapsdk/cli.py index 684232c..209188c 100644 --- a/zmapsdk/cli.py +++ b/zmapsdk/cli.py @@ -2,69 +2,85 @@ Command-line interface for ZMap SDK """ -import argparse -import sys import logging -from typing import List, Optional +import sys + +import click + +from zmapsdk.api import APIServer +from zmapsdk.core import ZMap -from .api import APIServer -from .core import ZMap +def setup_logging(verbose: bool) -> None: + """Configure logging with the specified verbosity level""" + log_level = logging.DEBUG if verbose else logging.INFO + logging.basicConfig( + level=log_level, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + ) -def run_api_server(args: argparse.Namespace) -> int: - """Run the API server with the specified options""" + +def print_version(ctx: click.Context, _, value: bool) -> None: + """Print ZMap version and exit if --version is specified""" + if not value or ctx.resilient_parsing: + return try: - # Configure logging - log_level = logging.DEBUG if args.verbose else logging.INFO - logging.basicConfig( - level=log_level, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' - ) - - # Create and run API server - server = APIServer(host=args.host, port=args.port) - print(f"Starting ZMap SDK API server on http://{args.host}:{args.port}") - print(f"API documentation available at http://{args.host}:{args.port}/docs") + zmap = ZMap() + version = zmap.get_version() + click.echo(f"ZMap version: {version}") + except Exception as e: + click.echo(f"Warning: Could not detect ZMap version: {e}", err=True) + ctx.exit() + + +@click.group() +@click.option( + "--version", + is_flag=True, + callback=print_version, + expose_value=False, + is_eager=True, + help="Show ZMap version and exit.", +) +def cli() -> None: + """ZMap SDK - Command-line interface for network scanning operations""" + pass + + +@cli.command() +@click.option("--host", default="127.0.0.1", help="Host address to bind the server to.") +@click.option( + "--port", + default=8000, + type=int, + help="Port number to bind the server to.", +) +@click.option("--verbose", "-v", is_flag=True, help="Enable verbose output.") +def api(host: str, port: int, verbose: bool) -> None: + """Run the ZMap SDK API server. + + This command starts a FastAPI-based HTTP server that provides + a REST API interface to ZMap functionality. + """ + try: + setup_logging(verbose) + server = APIServer(host=host, port=port) + click.echo(f"Starting ZMap SDK API server on http://{host}:{port}") + click.echo(f"API documentation available at http://{host}:{port}/docs") server.run() - - return 0 except Exception as e: - print(f"Error starting API server: {e}", file=sys.stderr) - if args.verbose: + click.echo(f"Error starting API server: {e}", err=True) + if verbose: import traceback + traceback.print_exc() - return 1 + sys.exit(1) -def main(argv: Optional[List[str]] = None) -> int: +def main() -> None: """Main entry point for the CLI""" - parser = argparse.ArgumentParser(description="ZMap SDK command-line interface") - subparsers = parser.add_subparsers(dest="command", help="Command to run") - - # API server command - api_parser = subparsers.add_parser("api", help="Run the ZMap SDK API server") - api_parser.add_argument("--host", default="127.0.0.1", help="Host to bind to") - api_parser.add_argument("--port", type=int, default=8000, help="Port to bind to") - api_parser.add_argument("--verbose", "-v", action="store_true", help="Enable verbose output") - - # Parse arguments - args = parser.parse_args(argv) - - # Check ZMap version (for all commands) - if args.command: - try: - zmap = ZMap() - print(f"ZMap version: {zmap.get_version()}") - except Exception as e: - print(f"Warning: Could not detect ZMap version: {e}", file=sys.stderr) - - # Run appropriate command - if args.command == "api": - return run_api_server(args) - else: - parser.print_help() - return 0 + cli(auto_envvar_prefix="ZMAPSDK") if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file + main() diff --git a/zmapsdk/config.py b/zmapsdk/config.py index 2739d99..10202d2 100644 --- a/zmapsdk/config.py +++ b/zmapsdk/config.py @@ -1,154 +1,170 @@ -""" -Configuration module for ZMap SDK -""" - -from typing import Optional, Dict, Any, List, Union -from dataclasses import dataclass, field, asdict -import json - -from .exceptions import ZMapConfigError - - -@dataclass -class ZMapScanConfig: - """ - Configuration for a ZMap scan - - Args: - target_port: Port number to scan (for TCP and UDP scans) - bandwidth: Set send rate in bits/second (supports suffixes G, M and K) - rate: Set send rate in packets/sec - cooldown_time: How long to continue receiving after sending last probe - interface: Specify network interface to use - source_ip: Source address(es) for scan packets - source_port: Source port(s) for scan packets - gateway_mac: Specify gateway MAC address - source_mac: Source MAC address - target_mac: Target MAC address (when ARP is disabled) - vpn: Sends IP packets instead of Ethernet (for VPNs) - max_targets: Cap number of targets to probe - max_runtime: Cap length of time for sending packets - max_results: Cap number of results to return - probes: Number of probes to send to each IP - retries: Max number of times to try to send packet if send fails - dryrun: Don't actually send packets - seed: Seed used to select address permutation - shards: Set the total number of shards - shard: Set which shard this scan is (0 indexed) - sender_threads: Threads used to send packets - cores: Comma-separated list of cores to pin to - ignore_invalid_hosts: Ignore invalid hosts in allowlist/blocklist file - max_sendto_failures: Maximum NIC sendto failures before scan is aborted - min_hitrate: Minimum hitrate that scan can hit before scan is aborted - notes: Inject user-specified notes into scan metadata - user_metadata: Inject user-specified JSON metadata into scan metadata - """ - # Core Options - target_port: Optional[int] = None - bandwidth: Optional[str] = None - rate: Optional[int] = None - cooldown_time: Optional[int] = None - interface: Optional[str] = None - source_ip: Optional[str] = None - source_port: Optional[Union[int, str]] = None - gateway_mac: Optional[str] = None - source_mac: Optional[str] = None - target_mac: Optional[str] = None - vpn: bool = False - - # Scan Control Options - max_targets: Optional[Union[int, str]] = None - max_runtime: Optional[int] = None - max_results: Optional[int] = None - probes: Optional[int] = None - retries: Optional[int] = None - dryrun: bool = False - seed: Optional[int] = None - shards: Optional[int] = None - shard: Optional[int] = None - - # Advanced Options - sender_threads: Optional[int] = None - cores: Optional[Union[List[int], str]] = None - ignore_invalid_hosts: bool = False - max_sendto_failures: Optional[int] = None - min_hitrate: Optional[float] = None - - # Metadata Options - notes: Optional[str] = None - user_metadata: Optional[Union[Dict, str]] = None - - def __post_init__(self): - """Validate configuration after initialization""" - self._validate() - - def _validate(self) -> None: - """Validate the configuration""" - if self.target_port is not None and not (0 <= self.target_port <= 65535): - raise ZMapConfigError(f"Invalid target port: {self.target_port}. Must be between 0 and 65535.") - - if self.rate is not None and self.bandwidth is not None: - raise ZMapConfigError("Cannot specify both rate and bandwidth.") - - if self.source_port is not None: - if isinstance(self.source_port, str) and "-" in self.source_port: - parts = self.source_port.split("-") - if len(parts) != 2 or not all(p.isdigit() for p in parts): - raise ZMapConfigError(f"Invalid source port range: {self.source_port}.") - start, end = map(int, parts) - if not (0 <= start <= end <= 65535): - raise ZMapConfigError(f"Invalid source port range: {self.source_port}. Must be between 0 and 65535.") - elif isinstance(self.source_port, int) and not (0 <= self.source_port <= 65535): - raise ZMapConfigError(f"Invalid source port: {self.source_port}. Must be between 0 and 65535.") - - if self.max_targets is not None and isinstance(self.max_targets, str): - if not self.max_targets.endswith("%"): - try: - int(self.max_targets) - except ValueError: - raise ZMapConfigError(f"Invalid max_targets: {self.max_targets}. Must be an integer or percentage.") - - # Validate MAC addresses - for mac_field in ['gateway_mac', 'source_mac', 'target_mac']: - mac = getattr(self, mac_field) - if mac is not None and not self._is_valid_mac(mac): - raise ZMapConfigError(f"Invalid {mac_field}: {mac}. Must be in format 'XX:XX:XX:XX:XX:XX'.") - - @staticmethod - def _is_valid_mac(mac: str) -> bool: - """Check if a string is a valid MAC address""" - import re - return bool(re.match(r'^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$', mac)) - - def to_dict(self) -> Dict[str, Any]: - """Convert configuration to a dictionary, removing None values""" - result = {} - for key, value in asdict(self).items(): - if value is not None: - result[key] = value - return result - - def to_json(self) -> str: - """Convert configuration to a JSON string""" - return json.dumps(self.to_dict(), indent=2) - - @classmethod - def from_dict(cls, data: Dict[str, Any]) -> 'ZMapScanConfig': - """Create a configuration from a dictionary""" - return cls(**data) - - @classmethod - def from_json(cls, json_str: str) -> 'ZMapScanConfig': - """Create a configuration from a JSON string""" - return cls.from_dict(json.loads(json_str)) - - def save_to_file(self, filename: str) -> None: - """Save configuration to a file as JSON""" - with open(filename, 'w') as f: - f.write(self.to_json()) - - @classmethod - def load_from_file(cls, filename: str) -> 'ZMapScanConfig': - """Load configuration from a JSON file""" - with open(filename, 'r') as f: - return cls.from_json(f.read()) \ No newline at end of file +""" +Configuration module for ZMap SDK +""" + +import json +from dataclasses import asdict, dataclass +from typing import Any + +from .exceptions import ZMapConfigError + + +@dataclass +class ZMapScanConfig: + """ + Configuration for a ZMap scan + + Args: + target_port: Port number to scan (for TCP and UDP scans) + bandwidth: Set send rate in bits/second (supports suffixes G, M and K) + rate: Set send rate in packets/sec + cooldown_time: How long to continue receiving after sending last probe + interface: Specify network interface to use + source_ip: Source address(es) for scan packets + source_port: Source port(s) for scan packets + gateway_mac: Specify gateway MAC address + source_mac: Source MAC address + target_mac: Target MAC address (when ARP is disabled) + vpn: Sends IP packets instead of Ethernet (for VPNs) + max_targets: Cap number of targets to probe + max_runtime: Cap length of time for sending packets + max_results: Cap number of results to return + probes: Number of probes to send to each IP + retries: Max number of times to try to send packet if send fails + dryrun: Don't actually send packets + seed: Seed used to select address permutation + shards: Set the total number of shards + shard: Set which shard this scan is (0 indexed) + sender_threads: Threads used to send packets + cores: Comma-separated list of cores to pin to + ignore_invalid_hosts: Ignore invalid hosts in allowlist/blocklist file + max_sendto_failures: Maximum NIC sendto failures before scan is aborted + min_hitrate: Minimum hitrate that scan can hit before scan is aborted + notes: Inject user-specified notes into scan metadata + user_metadata: Inject user-specified JSON metadata into scan metadata + """ + + # Core Options + target_port: int | None = None + bandwidth: str | None = None + rate: int | None = None + cooldown_time: int | None = None + interface: str | None = None + source_ip: str | None = None + source_port: int | str | None = None + gateway_mac: str | None = None + source_mac: str | None = None + target_mac: str | None = None + vpn: bool = False + + # Scan Control Options + max_targets: int | str | None = None + max_runtime: int | None = None + max_results: int | None = None + probes: int | None = None + retries: int | None = None + dryrun: bool = False + seed: int | None = None + shards: int | None = None + shard: int | None = None + + # Advanced Options + sender_threads: int | None = None + cores: list[int] | str | None = None + ignore_invalid_hosts: bool = False + max_sendto_failures: int | None = None + min_hitrate: float | None = None + + # Metadata Options + notes: str | None = None + user_metadata: dict[str, Any] | str | None = None + + def __post_init__(self): + """Validate configuration after initialization""" + self._validate() + + def _validate(self) -> None: + """Validate the configuration""" + if self.target_port is not None and not (0 <= self.target_port <= 65535): + raise ZMapConfigError( + f"Invalid target port: {self.target_port}. Must be between 0 and 65535.", + ) + + if self.rate is not None and self.bandwidth is not None: + raise ZMapConfigError("Cannot specify both rate and bandwidth.") + + if self.source_port is not None: + if isinstance(self.source_port, str) and "-" in self.source_port: + parts = self.source_port.split("-") + if len(parts) != 2 or not all(p.isdigit() for p in parts): + raise ZMapConfigError( + f"Invalid source port range: {self.source_port}.", + ) + start, end = map(int, parts) + if not (0 <= start <= end <= 65535): + raise ZMapConfigError( + f"Invalid source port range: {self.source_port}. Must be between 0 and 65535.", + ) + elif isinstance(self.source_port, int) and not ( + 0 <= self.source_port <= 65535 + ): + raise ZMapConfigError( + f"Invalid source port: {self.source_port}. Must be between 0 and 65535.", + ) + + if self.max_targets is not None and isinstance(self.max_targets, str): + if not self.max_targets.endswith("%"): + try: + int(self.max_targets) + except ValueError: + raise ZMapConfigError( + f"Invalid max_targets: {self.max_targets}. Must be an integer or percentage.", + ) + + # Validate MAC addresses + for mac_field in ["gateway_mac", "source_mac", "target_mac"]: + mac = getattr(self, mac_field) + if mac is not None and not self._is_valid_mac(mac): + raise ZMapConfigError( + f"Invalid {mac_field}: {mac}. Must be in format 'XX:XX:XX:XX:XX:XX'.", + ) + + @staticmethod + def _is_valid_mac(mac: str) -> bool: + """Check if a string is a valid MAC address""" + import re + + return bool(re.match(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$", mac)) + + def to_dict(self) -> dict[str, Any]: + """Convert configuration to a dictionary, removing None values""" + result = {} + for key, value in asdict(self).items(): + if value is not None: + result[key] = value + return result + + def to_json(self) -> str: + """Convert configuration to a JSON string""" + return json.dumps(self.to_dict(), indent=2) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> "ZMapScanConfig": + """Create a configuration from a dictionary""" + return cls(**data) + + @classmethod + def from_json(cls, json_str: str) -> "ZMapScanConfig": + """Create a configuration from a JSON string""" + return cls.from_dict(json.loads(json_str)) + + def save_to_file(self, filename: str) -> None: + """Save configuration to a file as JSON""" + with open(filename, "w") as f: + f.write(self.to_json()) + + @classmethod + def load_from_file(cls, filename: str) -> "ZMapScanConfig": + """Load configuration from a JSON file""" + with open(filename) as f: + return cls.from_json(f.read()) diff --git a/zmapsdk/core.py b/zmapsdk/core.py index 67610da..e4a2419 100644 --- a/zmapsdk/core.py +++ b/zmapsdk/core.py @@ -1,282 +1,332 @@ -""" -Core module for ZMap SDK -""" - -import os -import tempfile -from typing import List, Dict, Any, Optional, Union, Tuple, Callable - -from .config import ZMapScanConfig -from .input import ZMapInput -from .output import ZMapOutput -from .runner import ZMapRunner -from .parser import ZMapParser -from .exceptions import ZMapError, ZMapCommandError - - -class ZMap: - """ - Main class for the ZMap SDK - """ - - def __init__(self, zmap_path: str = "zmap"): - """ - Initialize the ZMap SDK - - Args: - zmap_path: Path to the zmap executable (defaults to "zmap", assuming it's in PATH) - """ - self.runner = ZMapRunner(zmap_path) - self.config = ZMapScanConfig() - self.input = ZMapInput() - self.output = ZMapOutput() - - def scan(self, - target_port: Optional[int] = None, - subnets: Optional[List[str]] = None, - output_file: Optional[str] = None, - callback: Optional[Callable[[str], None]] = None, - **kwargs) -> List[str]: - """ - Perform a scan and return the results - - Args: - target_port: Port number to scan - subnets: List of subnets to scan (defaults to scanning the internet) - output_file: Output file (if not specified, a temporary file will be used) - callback: Optional callback function for real-time output - **kwargs: Additional parameters to pass to ZMap - - Returns: - List of IP addresses that responded - """ - # Create a new configuration for this scan - scan_config = ZMapScanConfig() - scan_input = ZMapInput() - scan_output = ZMapOutput() - - # Set target port - if target_port is not None: - scan_config.target_port = target_port - - # Set subnets - if subnets: - scan_input.add_subnets(subnets) - - # Set output file - if output_file: - scan_output.set_output_file(output_file) - - # Process other parameters and assign them to the appropriate config object - for key, value in kwargs.items(): - # Input options - if key in ['blocklist_file', 'allowlist_file', 'input_file', 'ignore_blocklist', 'ignore_invalid_hosts']: - setattr(scan_input, key, value) - # Output options - elif key in ['output_fields', 'output_module', 'output_filter', 'output_args', - 'log_file', 'log_directory', 'metadata_file', 'status_updates_file', - 'verbosity', 'quiet', 'disable_syslog']: - setattr(scan_output, key, value) - # Config options (everything else) - else: - setattr(scan_config, key, value) - - # Run the scan - return self.runner.scan( - config=scan_config, - input_config=scan_input, - output_config=scan_output, - callback=callback - ) - - def run(self, **kwargs) -> Tuple[int, str, str]: - """ - Run ZMap with the specified parameters - - Args: - **kwargs: Command-line options as keyword arguments - - Returns: - Tuple of (return code, stdout, stderr) - """ - return self.runner.run_command(**kwargs) - - def get_probe_modules(self) -> List[str]: - """ - Get list of available probe modules - - Returns: - List of available probe module names - """ - return self.runner.get_probe_modules() - - def get_output_modules(self) -> List[str]: - """ - Get list of available output modules - - Returns: - List of available output module names - """ - return self.runner.get_output_modules() - - def get_output_fields(self, probe_module: Optional[str] = None) -> List[str]: - """ - Get list of available output fields for the specified probe module - - Args: - probe_module: Probe module to get output fields for (optional) - - Returns: - List of available output field names - """ - return self.runner.get_output_fields(probe_module) - - def get_interfaces(self) -> List[str]: - """ - Get list of available network interfaces - - Returns: - List of available interface names - """ - return self.runner.get_interfaces() - - def get_version(self) -> str: - """ - Get ZMap version - - Returns: - Version string - """ - return self.runner.get_version() - - def blocklist_from_file(self, blocklist_file: str) -> None: - """ - Validate and use a blocklist file - - Args: - blocklist_file: Path to the blocklist file - """ - self.input.set_blocklist_file(blocklist_file) - - def allowlist_from_file(self, allowlist_file: str) -> None: - """ - Validate and use a allowlist file - - Args: - allowlist_file: Path to the allowlist file - """ - self.input.set_allowlist_file(allowlist_file) - - def create_blocklist_file(self, subnets: List[str], output_file: str) -> str: - """ - Create a blocklist file from a list of subnets - - Args: - subnets: List of subnet CIDRs to blocklist - output_file: Path to save the blocklist file - - Returns: - Path to the created blocklist file - """ - return self.input.create_blocklist_file(subnets, output_file) - - def create_allowlist_file(self, subnets: List[str], output_file: str) -> str: - """ - Create a allowlist file from a list of subnets - - Args: - subnets: List of subnet CIDRs to allowlist - output_file: Path to save the allowlist file - - Returns: - Path to the created allowlist file - """ - return self.input.create_allowlist_file(subnets, output_file) - - def create_target_file(self, targets: List[str], output_file: str) -> str: - """ - Create an input file for specific target IPs - - Args: - targets: List of IP addresses to scan - output_file: Path to save the input file - - Returns: - Path to the created input file - """ - return self.input.create_target_file(targets, output_file) - - def generate_standard_blocklist(self, output_file: str) -> str: - """ - Generate a blocklist file with standard private network ranges - - Args: - output_file: Path to save the blocklist file - - Returns: - Path to the created blocklist file - """ - return self.input.generate_standard_blocklist(output_file) - - def parse_results(self, file_path: str, fields: Optional[List[str]] = None) -> List[Dict[str, str]]: - """ - Parse scan results from a file - - Args: - file_path: Path to the results file - fields: List of field names (if not provided, will try to read from header) - - Returns: - List of dictionaries, each representing a row with field names as keys - """ - return ZMapParser.parse_csv_results(file_path, fields) - - def parse_metadata(self, file_path: str) -> Dict[str, Any]: - """ - Parse a ZMap metadata file - - Args: - file_path: Path to the metadata file - - Returns: - Dictionary containing metadata - """ - return ZMapParser.parse_metadata(file_path) - - def extract_ips(self, results: List[Dict[str, Any]], ip_field: str = 'saddr') -> List[str]: - """ - Extract IP addresses from parsed results - - Args: - results: List of result dictionaries - ip_field: Field name containing IP addresses - - Returns: - List of IP addresses - """ - return ZMapParser.extract_ips(results, ip_field) - - def stream_results(self, file_path: str, fields: Optional[List[str]] = None): - """ - Stream results from a file without loading everything into memory - - Args: - file_path: Path to the results file - fields: List of field names (if not provided, will try to read from header) - - Returns: - Iterator yielding dictionaries, each representing a row with field names as keys - """ - return ZMapParser.stream_results(file_path, fields) - - def count_results(self, file_path: str) -> int: - """ - Count the number of results in a file - - Args: - file_path: Path to the results file - - Returns: - Number of result rows - """ - return ZMapParser.count_results(file_path) \ No newline at end of file +""" +Core module for ZMap SDK +""" + +from collections.abc import Callable +from typing import Any + +from zmapsdk.config import ZMapScanConfig +from zmapsdk.input import ZMapInput +from zmapsdk.output import ZMapOutput +from zmapsdk.parser import ZMapParser +from zmapsdk.runner import ZMapRunner + + +# Option categories for ZMap configuration +class ZMapOptionCategories: + """Configuration option categories for ZMap""" + + INPUT_OPTIONS = frozenset( + { + "blocklist_file", + "allowlist_file", + "input_file", + "ignore_blocklist", + "ignore_invalid_hosts", + }, + ) + + OUTPUT_OPTIONS = frozenset( + { + "output_fields", + "output_module", + "output_filter", + "output_args", + "log_file", + "log_directory", + "metadata_file", + "status_updates_file", + "verbosity", + "quiet", + "disable_syslog", + }, + ) + + +class ZMap: + """ + Main class for the ZMap SDK + """ + + def __init__(self, zmap_path: str = "zmap"): + """ + Initialize the ZMap SDK + + Args: + zmap_path: Path to the zmap executable (defaults to "zmap", assuming it's in PATH) + """ + self.runner = ZMapRunner(zmap_path) + self.config = ZMapScanConfig() + self.input = ZMapInput() + self.output = ZMapOutput() + + def scan( + self, + target_port: int | None = None, + subnets: list[str] | None = None, + output_file: str | None = None, + callback: Callable[[str], None] | None = None, + **kwargs: Any, + ) -> list[str]: + """ + Perform a scan and return the results + + Args: + target_port: Port number to scan + subnets: List of subnets to scan (defaults to scanning the internet) + output_file: Output file (if not specified, a temporary file will be used) + callback: Optional callback function for real-time output + **kwargs: Additional parameters to pass to ZMap + + Returns: + List of IP addresses that responded + """ + # Initialize scan configurations + scan_config = ZMapScanConfig() + scan_input = ZMapInput() + scan_output = ZMapOutput() + + # Set core scan parameters + if target_port is not None: + scan_config.target_port = target_port + + if subnets: + scan_input.add_subnets(subnets) + + if output_file: + scan_output.set_output_file(output_file) + + # Distribute additional parameters to appropriate configuration objects + self._process_scan_options(kwargs, scan_config, scan_input, scan_output) + + # Execute the scan + return self.runner.scan( + config=scan_config, + input_config=scan_input, + output_config=scan_output, + callback=callback, + ) + + @staticmethod + def _process_scan_options( + options: dict[str, Any], + scan_config: ZMapScanConfig, + scan_input: ZMapInput, + scan_output: ZMapOutput, + ) -> None: + """ + Process and distribute scan options to appropriate configuration objects. + + Args: + options: Dictionary of scan options + scan_config: ZMap scan configuration object + scan_input: ZMap input configuration object + scan_output: ZMap output configuration object + """ + for key, value in options.items(): + if key in ZMapOptionCategories.INPUT_OPTIONS: + setattr(scan_input, key, value) + elif key in ZMapOptionCategories.OUTPUT_OPTIONS: + setattr(scan_output, key, value) + else: + setattr(scan_config, key, value) + + def run(self, **kwargs: Any) -> tuple[int, str, str]: + """ + Run ZMap with the specified parameters + + Args: + **kwargs: Command-line options as keyword arguments + + Returns: + Tuple of (return code, stdout, stderr) + """ + return self.runner.run_command(**kwargs) + + def get_probe_modules(self) -> list[str]: + """ + Get list of available probe modules + + Returns: + List of available probe module names + """ + return self.runner.get_probe_modules() + + def get_output_modules(self) -> list[str]: + """ + Get list of available output modules + + Returns: + List of available output module names + """ + return self.runner.get_output_modules() + + def get_output_fields(self, probe_module: str | None = None) -> list[str]: + """ + Get list of available output fields for the specified probe module + + Args: + probe_module: Probe module to get output fields for (optional) + + Returns: + List of available output field names + """ + return self.runner.get_output_fields(probe_module) + + def get_interfaces(self) -> list[str]: + """ + Get list of available network interfaces + + Returns: + List of available interface names + """ + return self.runner.get_interfaces() + + def get_version(self) -> str: + """ + Get ZMap version + + Returns: + Version string + """ + return self.runner.get_version() + + def blocklist_from_file(self, blocklist_file: str) -> None: + """ + Validate and use a blocklist file + + Args: + blocklist_file: Path to the blocklist file + """ + self.input.set_blocklist_file(blocklist_file) + + def allowlist_from_file(self, allowlist_file: str) -> None: + """ + Validate and use a allowlist file + + Args: + allowlist_file: Path to the allowlist file + """ + self.input.set_allowlist_file(allowlist_file) + + def create_blocklist_file(self, subnets: list[str], output_file: str) -> str: + """ + Create a blocklist file from a list of subnets + + Args: + subnets: List of subnet CIDRs to blocklist + output_file: Path to save the blocklist file + + Returns: + Path to the created blocklist file + """ + return self.input.create_blocklist_file(subnets, output_file) + + def create_allowlist_file(self, subnets: list[str], output_file: str) -> str: + """ + Create a allowlist file from a list of subnets + + Args: + subnets: List of subnet CIDRs to allowlist + output_file: Path to save the allowlist file + + Returns: + Path to the created allowlist file + """ + return self.input.create_allowlist_file(subnets, output_file) + + def create_target_file(self, targets: list[str], output_file: str) -> str: + """ + Create an input file for specific target IPs + + Args: + targets: List of IP addresses to scan + output_file: Path to save the input file + + Returns: + Path to the created input file + """ + return self.input.create_target_file(targets, output_file) + + def generate_standard_blocklist(self, output_file: str) -> str: + """ + Generate a blocklist file with standard private network ranges + + Args: + output_file: Path to save the blocklist file + + Returns: + Path to the created blocklist file + """ + return self.input.generate_standard_blocklist(output_file) + + def parse_results( + self, + file_path: str, + fields: list[str] | None = None, + ) -> list[dict[str, str]]: + """ + Parse scan results from a file + + Args: + file_path: Path to the results file + fields: List of field names (if not provided, will try to read from header) + + Returns: + List of dictionaries, each representing a row with field names as keys + """ + return ZMapParser.parse_csv_results(file_path, fields) + + def parse_metadata(self, file_path: str) -> dict[str, Any]: + """ + Parse a ZMap metadata file + + Args: + file_path: Path to the metadata file + + Returns: + Dictionary containing metadata + """ + return ZMapParser.parse_metadata(file_path) + + def extract_ips( + self, + results: list[dict[str, Any]], + ip_field: str = "saddr", + ) -> list[str]: + """ + Extract IP addresses from parsed results + + Args: + results: List of result dictionaries + ip_field: Field name containing IP addresses + + Returns: + List of IP addresses + """ + return ZMapParser.extract_ips(results, ip_field) + + def stream_results(self, file_path: str, fields: list[str] | None = None): + """ + Stream results from a file without loading everything into memory + + Args: + file_path: Path to the results file + fields: List of field names (if not provided, will try to read from header) + + Returns: + Iterator yielding dictionaries, each representing a row with field names as keys + """ + return ZMapParser.stream_results(file_path, fields) + + def count_results(self, file_path: str) -> int: + """ + Count the number of results in a file + + Args: + file_path: Path to the results file + + Returns: + Number of result rows + """ + return ZMapParser.count_results(file_path) diff --git a/zmapsdk/exceptions.py b/zmapsdk/exceptions.py index a34ee54..39ce0fc 100644 --- a/zmapsdk/exceptions.py +++ b/zmapsdk/exceptions.py @@ -2,14 +2,16 @@ Exceptions for the ZMap SDK """ + class ZMapError(Exception): """Base exception for ZMap SDK errors""" + pass class ZMapCommandError(ZMapError): """Exception raised when zmap command fails""" - + def __init__(self, command: str, returncode: int, stderr: str): self.command = command self.returncode = returncode @@ -20,19 +22,23 @@ def __init__(self, command: str, returncode: int, stderr: str): class ZMapConfigError(ZMapError): """Exception raised for configuration errors""" + pass class ZMapInputError(ZMapError): """Exception raised for input file errors""" + pass class ZMapOutputError(ZMapError): """Exception raised for output-related errors""" + pass class ZMapParserError(ZMapError): """Exception raised when parsing ZMap output fails""" - pass \ No newline at end of file + + pass diff --git a/zmapsdk/input.py b/zmapsdk/input.py index 3605e1d..fd6a851 100644 --- a/zmapsdk/input.py +++ b/zmapsdk/input.py @@ -1,243 +1,243 @@ -""" -Input handling module for ZMap SDK -""" - -import os -import ipaddress -from typing import List, Optional, Set, Union, Dict, Any - -from .exceptions import ZMapInputError - - -class ZMapInput: - """ - Class for handling ZMap input options like target lists, blocklists, and allowlists - """ - - def __init__(self): - """Initialize the input handler""" - self.blocklist_file: Optional[str] = None - self.allowlist_file: Optional[str] = None - self.input_file: Optional[str] = None - self.target_subnets: List[str] = [] - self.ignore_blocklist: bool = False - self.ignore_invalid_hosts: bool = False - - def add_subnet(self, subnet: str) -> None: - """ - Add a subnet to the target list - - Args: - subnet: Subnet in CIDR notation (e.g., '192.168.0.0/16') - - Raises: - ZMapInputError: If the subnet is invalid - """ - try: - ipaddress.ip_network(subnet) - self.target_subnets.append(subnet) - except ValueError as e: - raise ZMapInputError(f"Invalid subnet: {subnet} - {str(e)}") - - def add_subnets(self, subnets: List[str]) -> None: - """ - Add multiple subnets to the target list - - Args: - subnets: List of subnets in CIDR notation - - Raises: - ZMapInputError: If any subnet is invalid - """ - for subnet in subnets: - self.add_subnet(subnet) - - def set_blocklist_file(self, file_path: str) -> None: - """ - Set the blocklist file - - Args: - file_path: Path to the blocklist file - - Raises: - ZMapInputError: If the file doesn't exist or isn't readable - """ - if not os.path.isfile(file_path): - raise ZMapInputError(f"Blocklist file not found: {file_path}") - if not os.access(file_path, os.R_OK): - raise ZMapInputError(f"Blocklist file not readable: {file_path}") - self.blocklist_file = file_path - - def set_allowlist_file(self, file_path: str) -> None: - """ - Set the allowlist file - - Args: - file_path: Path to the allowlist file - - Raises: - ZMapInputError: If the file doesn't exist or isn't readable - """ - if not os.path.isfile(file_path): - raise ZMapInputError(f"Allowlist file not found: {file_path}") - if not os.access(file_path, os.R_OK): - raise ZMapInputError(f"Allowlist file not readable: {file_path}") - self.allowlist_file = file_path - - def set_input_file(self, file_path: str) -> None: - """ - Set the input file for targets - - Args: - file_path: Path to the input file - - Raises: - ZMapInputError: If the file doesn't exist or isn't readable - """ - if not os.path.isfile(file_path): - raise ZMapInputError(f"Input file not found: {file_path}") - if not os.access(file_path, os.R_OK): - raise ZMapInputError(f"Input file not readable: {file_path}") - self.input_file = file_path - - def create_blocklist_file(self, subnets: List[str], output_file: str) -> str: - """ - Create a blocklist file from a list of subnets - - Args: - subnets: List of subnet CIDRs to blocklist - output_file: Path to save the blocklist file - - Returns: - Path to the created blocklist file - - Raises: - ZMapInputError: If a subnet is invalid or file can't be created - """ - # Validate subnets - for subnet in subnets: - try: - ipaddress.ip_network(subnet) - except ValueError as e: - raise ZMapInputError(f"Invalid subnet in blocklist: {subnet} - {str(e)}") - - # Write to file - try: - with open(output_file, 'w') as f: - f.write('\n'.join(subnets)) - except IOError as e: - raise ZMapInputError(f"Failed to create blocklist file: {str(e)}") - - self.blocklist_file = output_file - return output_file - - def create_allowlist_file(self, subnets: List[str], output_file: str) -> str: - """ - Create a allowlist file from a list of subnets - - Args: - subnets: List of subnet CIDRs to allowlist - output_file: Path to save the allowlist file - - Returns: - Path to the created allowlist file - - Raises: - ZMapInputError: If a subnet is invalid or file can't be created - """ - # Validate subnets - for subnet in subnets: - try: - ipaddress.ip_network(subnet) - except ValueError as e: - raise ZMapInputError(f"Invalid subnet in allowlist: {subnet} - {str(e)}") - - # Write to file - try: - with open(output_file, 'w') as f: - f.write('\n'.join(subnets)) - except IOError as e: - raise ZMapInputError(f"Failed to create allowlist file: {str(e)}") - - self.allowlist_file = output_file - return output_file - - def create_target_file(self, targets: List[str], output_file: str) -> str: - """ - Create an input file for specific target IPs - - Args: - targets: List of IP addresses to scan - output_file: Path to save the input file - - Returns: - Path to the created input file - - Raises: - ZMapInputError: If an IP is invalid or file can't be created - """ - # Validate IPs - for ip in targets: - try: - ipaddress.ip_address(ip) - except ValueError as e: - raise ZMapInputError(f"Invalid IP address: {ip} - {str(e)}") - - # Write to file - try: - with open(output_file, 'w') as f: - f.write('\n'.join(targets)) - except IOError as e: - raise ZMapInputError(f"Failed to create target file: {str(e)}") - - self.input_file = output_file - return output_file - - def generate_standard_blocklist(self, output_file: str) -> str: - """ - Generate a blocklist file with standard private network ranges - - Args: - output_file: Path to save the blocklist file - - Returns: - Path to the created blocklist file - """ - private_ranges = [ - "10.0.0.0/8", # RFC1918 private network - "172.16.0.0/12", # RFC1918 private network - "192.168.0.0/16", # RFC1918 private network - "127.0.0.0/8", # Loopback - "169.254.0.0/16", # Link-local - "224.0.0.0/4", # Multicast - "240.0.0.0/4", # Reserved - "192.0.2.0/24", # TEST-NET for documentation - "198.51.100.0/24", # TEST-NET-2 for documentation - "203.0.113.0/24", # TEST-NET-3 for documentation - ] - - return self.create_blocklist_file(private_ranges, output_file) - - def to_dict(self) -> Dict[str, Any]: - """Convert input configuration to a dictionary for command-line options""" - result = {} - - if self.blocklist_file: - result["blocklist_file"] = self.blocklist_file - - if self.allowlist_file: - result["allowlist_file"] = self.allowlist_file - - if self.input_file: - result["input_file"] = self.input_file - - if self.ignore_blocklist: - result["ignore_blocklist"] = True - - if self.ignore_invalid_hosts: - result["ignore_invalid_hosts"] = True - - if self.target_subnets: - result["subnets"] = self.target_subnets - - return result \ No newline at end of file +""" +Input handling module for ZMap SDK +""" + +import ipaddress +import os +from typing import Any + +from zmapsdk.exceptions import ZMapInputError + + +class ZMapInput: + """ + Class for handling ZMap input options like target lists, blocklists, and allowlists + """ + + def __init__(self): + """Initialize the input handler""" + self.blocklist_file: str | None = None + self.allowlist_file: str | None = None + self.input_file: str | None = None + self.target_subnets: list[str] = [] + self.ignore_blocklist: bool = False + self.ignore_invalid_hosts: bool = False + + def add_subnet(self, subnet: str) -> None: + """ + Add a subnet to the target list + + Args: + subnet: Subnet in CIDR notation (e.g., '192.168.0.0/16') + + Raises: + ZMapInputError: If the subnet is invalid + """ + try: + ipaddress.ip_network(subnet) + self.target_subnets.append(subnet) + except ValueError as e: + raise ZMapInputError(f"Invalid subnet: {subnet} - {e!s}") + + def add_subnets(self, subnets: list[str]) -> None: + """ + Add multiple subnets to the target list + + Args: + subnets: List of subnets in CIDR notation + + Raises: + ZMapInputError: If any subnet is invalid + """ + for subnet in subnets: + self.add_subnet(subnet) + + def set_blocklist_file(self, file_path: str) -> None: + """ + Set the blocklist file + + Args: + file_path: Path to the blocklist file + + Raises: + ZMapInputError: If the file doesn't exist or isn't readable + """ + if not os.path.isfile(file_path): + raise ZMapInputError(f"Blocklist file not found: {file_path}") + if not os.access(file_path, os.R_OK): + raise ZMapInputError(f"Blocklist file not readable: {file_path}") + self.blocklist_file = file_path + + def set_allowlist_file(self, file_path: str) -> None: + """ + Set the allowlist file + + Args: + file_path: Path to the allowlist file + + Raises: + ZMapInputError: If the file doesn't exist or isn't readable + """ + if not os.path.isfile(file_path): + raise ZMapInputError(f"Allowlist file not found: {file_path}") + if not os.access(file_path, os.R_OK): + raise ZMapInputError(f"Allowlist file not readable: {file_path}") + self.allowlist_file = file_path + + def set_input_file(self, file_path: str) -> None: + """ + Set the input file for targets + + Args: + file_path: Path to the input file + + Raises: + ZMapInputError: If the file doesn't exist or isn't readable + """ + if not os.path.isfile(file_path): + raise ZMapInputError(f"Input file not found: {file_path}") + if not os.access(file_path, os.R_OK): + raise ZMapInputError(f"Input file not readable: {file_path}") + self.input_file = file_path + + def create_blocklist_file(self, subnets: list[str], output_file: str) -> str: + """ + Create a blocklist file from a list of subnets + + Args: + subnets: List of subnet CIDRs to blocklist + output_file: Path to save the blocklist file + + Returns: + Path to the created blocklist file + + Raises: + ZMapInputError: If a subnet is invalid or file can't be created + """ + # Validate subnets + for subnet in subnets: + try: + ipaddress.ip_network(subnet) + except ValueError as e: + raise ZMapInputError(f"Invalid subnet in blocklist: {subnet} - {e!s}") + + # Write to file + try: + with open(output_file, "w") as f: + f.write("\n".join(subnets)) + except OSError as e: + raise ZMapInputError(f"Failed to create blocklist file: {e!s}") + + self.blocklist_file = output_file + return output_file + + def create_allowlist_file(self, subnets: list[str], output_file: str) -> str: + """ + Create a allowlist file from a list of subnets + + Args: + subnets: List of subnet CIDRs to allowlist + output_file: Path to save the allowlist file + + Returns: + Path to the created allowlist file + + Raises: + ZMapInputError: If a subnet is invalid or file can't be created + """ + # Validate subnets + for subnet in subnets: + try: + ipaddress.ip_network(subnet) + except ValueError as e: + raise ZMapInputError(f"Invalid subnet in allowlist: {subnet} - {e!s}") + + # Write to file + try: + with open(output_file, "w") as f: + f.write("\n".join(subnets)) + except OSError as e: + raise ZMapInputError(f"Failed to create allowlist file: {e!s}") + + self.allowlist_file = output_file + return output_file + + def create_target_file(self, targets: list[str], output_file: str) -> str: + """ + Create an input file for specific target IPs + + Args: + targets: List of IP addresses to scan + output_file: Path to save the input file + + Returns: + Path to the created input file + + Raises: + ZMapInputError: If an IP is invalid or file can't be created + """ + # Validate IPs + for ip in targets: + try: + ipaddress.ip_address(ip) + except ValueError as e: + raise ZMapInputError(f"Invalid IP address: {ip} - {e!s}") + + # Write to file + try: + with open(output_file, "w") as f: + f.write("\n".join(targets)) + except OSError as e: + raise ZMapInputError(f"Failed to create target file: {e!s}") + + self.input_file = output_file + return output_file + + def generate_standard_blocklist(self, output_file: str) -> str: + """ + Generate a blocklist file with standard private network ranges + + Args: + output_file: Path to save the blocklist file + + Returns: + Path to the created blocklist file + """ + private_ranges = [ + "10.0.0.0/8", # RFC1918 private network + "172.16.0.0/12", # RFC1918 private network + "192.168.0.0/16", # RFC1918 private network + "127.0.0.0/8", # Loopback + "169.254.0.0/16", # Link-local + "224.0.0.0/4", # Multicast + "240.0.0.0/4", # Reserved + "192.0.2.0/24", # TEST-NET for documentation + "198.51.100.0/24", # TEST-NET-2 for documentation + "203.0.113.0/24", # TEST-NET-3 for documentation + ] + + return self.create_blocklist_file(private_ranges, output_file) + + def to_dict(self) -> dict[str, Any]: + """Convert input configuration to a dictionary for command-line options""" + result = {} + + if self.blocklist_file: + result["blocklist_file"] = self.blocklist_file + + if self.allowlist_file: + result["allowlist_file"] = self.allowlist_file + + if self.input_file: + result["input_file"] = self.input_file + + if self.ignore_blocklist: + result["ignore_blocklist"] = True + + if self.ignore_invalid_hosts: + result["ignore_invalid_hosts"] = True + + if self.target_subnets: + result["subnets"] = self.target_subnets + + return result diff --git a/zmapsdk/output.py b/zmapsdk/output.py index 614d058..8ea3d01 100644 --- a/zmapsdk/output.py +++ b/zmapsdk/output.py @@ -1,241 +1,247 @@ -""" -Output handling module for ZMap SDK -""" - -import os -from typing import List, Optional, Dict, Any, Union, Set - -from .exceptions import ZMapOutputError - - -class ZMapOutput: - """ - Class for handling ZMap output options - """ - - def __init__(self): - """Initialize the output handler""" - self.output_file: Optional[str] = None - self.output_fields: Optional[Union[List[str], str]] = None - self.output_module: Optional[str] = None - self.output_filter: Optional[str] = None - self.output_args: Optional[str] = None - self.log_file: Optional[str] = None - self.log_directory: Optional[str] = None - self.metadata_file: Optional[str] = None - self.status_updates_file: Optional[str] = None - self.verbosity: Optional[int] = None - self.quiet: bool = False - self.disable_syslog: bool = False - - def set_output_file(self, file_path: str) -> None: - """ - Set the output file for scan results - - Args: - file_path: Path to the output file - - Raises: - ZMapOutputError: If the directory doesn't exist or isn't writable - """ - directory = os.path.dirname(file_path) - if directory and not os.path.isdir(directory): - raise ZMapOutputError(f"Output directory does not exist: {directory}") - if directory and not os.access(directory, os.W_OK): - raise ZMapOutputError(f"Output directory is not writable: {directory}") - self.output_file = file_path - - def set_output_fields(self, fields: Union[List[str], str]) -> None: - """ - Set the output fields to include in results - - Args: - fields: List of field names or comma-separated string of field names - """ - self.output_fields = fields - - def set_output_module(self, module: str) -> None: - """ - Set the output module - - Args: - module: Name of the output module - """ - self.output_module = module - - def set_output_filter(self, filter_expr: str) -> None: - """ - Set a filter for output results - - Args: - filter_expr: Filter expression - """ - self.output_filter = filter_expr - - def set_log_file(self, file_path: str) -> None: - """ - Set the log file - - Args: - file_path: Path to the log file - - Raises: - ZMapOutputError: If the directory doesn't exist or isn't writable - """ - directory = os.path.dirname(file_path) - if directory and not os.path.isdir(directory): - raise ZMapOutputError(f"Log directory does not exist: {directory}") - if directory and not os.access(directory, os.W_OK): - raise ZMapOutputError(f"Log directory is not writable: {directory}") - self.log_file = file_path - - def set_log_directory(self, directory: str) -> None: - """ - Set the log directory for timestamped logs - - Args: - directory: Path to the log directory - - Raises: - ZMapOutputError: If the directory doesn't exist or isn't writable - """ - if not os.path.isdir(directory): - raise ZMapOutputError(f"Log directory does not exist: {directory}") - if not os.access(directory, os.W_OK): - raise ZMapOutputError(f"Log directory is not writable: {directory}") - self.log_directory = directory - - def set_metadata_file(self, file_path: str) -> None: - """ - Set the metadata output file - - Args: - file_path: Path to the metadata file - - Raises: - ZMapOutputError: If the directory doesn't exist or isn't writable - """ - directory = os.path.dirname(file_path) - if directory and not os.path.isdir(directory): - raise ZMapOutputError(f"Metadata directory does not exist: {directory}") - if directory and not os.access(directory, os.W_OK): - raise ZMapOutputError(f"Metadata directory is not writable: {directory}") - self.metadata_file = file_path - - def set_status_updates_file(self, file_path: str) -> None: - """ - Set the status updates file - - Args: - file_path: Path to the status updates file - - Raises: - ZMapOutputError: If the directory doesn't exist or isn't writable - """ - directory = os.path.dirname(file_path) - if directory and not os.path.isdir(directory): - raise ZMapOutputError(f"Status updates directory does not exist: {directory}") - if directory and not os.access(directory, os.W_OK): - raise ZMapOutputError(f"Status updates directory is not writable: {directory}") - self.status_updates_file = file_path - - def set_verbosity(self, level: int) -> None: - """ - Set the verbosity level - - Args: - level: Verbosity level (0-5) - - Raises: - ZMapOutputError: If the level is out of range - """ - if not 0 <= level <= 5: - raise ZMapOutputError(f"Verbosity level must be between 0 and 5, got {level}") - self.verbosity = level - - def enable_quiet_mode(self) -> None: - """Enable quiet mode (no status updates)""" - self.quiet = True - - def disable_quiet_mode(self) -> None: - """Disable quiet mode""" - self.quiet = False - - def enable_syslog(self) -> None: - """Enable syslog logging""" - self.disable_syslog = False - - def disable_syslog_logging(self) -> None: - """Disable syslog logging""" - self.disable_syslog = True - - def to_dict(self) -> Dict[str, Any]: - """Convert output configuration to a dictionary for command-line options""" - result = {} - - if self.output_file: - result["output_file"] = self.output_file - - if self.output_fields: - result["output_fields"] = self.output_fields - - if self.output_module: - result["output_module"] = self.output_module - - if self.output_filter: - result["output_filter"] = self.output_filter - - if self.output_args: - result["output_args"] = self.output_args - - if self.log_file: - result["log_file"] = self.log_file - - if self.log_directory: - result["log_directory"] = self.log_directory - - if self.metadata_file: - result["metadata_file"] = self.metadata_file - - if self.status_updates_file: - result["status_updates_file"] = self.status_updates_file - - if self.verbosity is not None: - result["verbosity"] = self.verbosity - - if self.quiet: - result["quiet"] = True - - if self.disable_syslog: - result["disable_syslog"] = True - - return result - - @staticmethod - def get_common_output_fields() -> Dict[str, str]: - """ - Get a dictionary of common output fields and their descriptions - - Returns: - Dictionary mapping field names to descriptions - """ - return { - "saddr": "Source IP address", - "daddr": "Destination IP address", - "sport": "Source port", - "dport": "Destination port", - "seqnum": "TCP sequence number", - "acknum": "TCP acknowledgement number", - "window": "TCP window", - "classification": "Response classification (e.g., synack, rst)", - "success": "Whether the probe was successful", - "repeat": "Whether this response is a repeat", - "cooldown": "Cooldown time in seconds", - "timestamp_ts": "Timestamp in seconds", - "timestamp_us": "Microsecond component of timestamp", - "icmp_type": "ICMP type", - "icmp_code": "ICMP code", - "icmp_unreach_str": "ICMP unreachable string", - "data": "Application response data", - "ttl": "Time to live", - } \ No newline at end of file +""" +Output handling module for ZMap SDK +""" + +import os +from typing import Any + +from zmapsdk.exceptions import ZMapOutputError + + +class ZMapOutput: + """ + Class for handling ZMap output options + """ + + def __init__(self): + """Initialize the output handler""" + self.output_file: str | None = None + self.output_fields: list[str] | str | None = None + self.output_module: str | None = None + self.output_filter: str | None = None + self.output_args: str | None = None + self.log_file: str | None = None + self.log_directory: str | None = None + self.metadata_file: str | None = None + self.status_updates_file: str | None = None + self.verbosity: int | None = None + self.quiet: bool = False + self.disable_syslog: bool = False + + def set_output_file(self, file_path: str) -> None: + """ + Set the output file for scan results + + Args: + file_path: Path to the output file + + Raises: + ZMapOutputError: If the directory doesn't exist or isn't writable + """ + directory = os.path.dirname(file_path) + if directory and not os.path.isdir(directory): + raise ZMapOutputError(f"Output directory does not exist: {directory}") + if directory and not os.access(directory, os.W_OK): + raise ZMapOutputError(f"Output directory is not writable: {directory}") + self.output_file = file_path + + def set_output_fields(self, fields: list[str] | str) -> None: + """ + Set the output fields to include in results + + Args: + fields: List of field names or comma-separated string of field names + """ + self.output_fields = fields + + def set_output_module(self, module: str) -> None: + """ + Set the output module + + Args: + module: Name of the output module + """ + self.output_module = module + + def set_output_filter(self, filter_expr: str) -> None: + """ + Set a filter for output results + + Args: + filter_expr: Filter expression + """ + self.output_filter = filter_expr + + def set_log_file(self, file_path: str) -> None: + """ + Set the log file + + Args: + file_path: Path to the log file + + Raises: + ZMapOutputError: If the directory doesn't exist or isn't writable + """ + directory = os.path.dirname(file_path) + if directory and not os.path.isdir(directory): + raise ZMapOutputError(f"Log directory does not exist: {directory}") + if directory and not os.access(directory, os.W_OK): + raise ZMapOutputError(f"Log directory is not writable: {directory}") + self.log_file = file_path + + def set_log_directory(self, directory: str) -> None: + """ + Set the log directory for timestamped logs + + Args: + directory: Path to the log directory + + Raises: + ZMapOutputError: If the directory doesn't exist or isn't writable + """ + if not os.path.isdir(directory): + raise ZMapOutputError(f"Log directory does not exist: {directory}") + if not os.access(directory, os.W_OK): + raise ZMapOutputError(f"Log directory is not writable: {directory}") + self.log_directory = directory + + def set_metadata_file(self, file_path: str) -> None: + """ + Set the metadata output file + + Args: + file_path: Path to the metadata file + + Raises: + ZMapOutputError: If the directory doesn't exist or isn't writable + """ + directory = os.path.dirname(file_path) + if directory and not os.path.isdir(directory): + raise ZMapOutputError(f"Metadata directory does not exist: {directory}") + if directory and not os.access(directory, os.W_OK): + raise ZMapOutputError(f"Metadata directory is not writable: {directory}") + self.metadata_file = file_path + + def set_status_updates_file(self, file_path: str) -> None: + """ + Set the status updates file + + Args: + file_path: Path to the status updates file + + Raises: + ZMapOutputError: If the directory doesn't exist or isn't writable + """ + directory = os.path.dirname(file_path) + if directory and not os.path.isdir(directory): + raise ZMapOutputError( + f"Status updates directory does not exist: {directory}", + ) + if directory and not os.access(directory, os.W_OK): + raise ZMapOutputError( + f"Status updates directory is not writable: {directory}", + ) + self.status_updates_file = file_path + + def set_verbosity(self, level: int) -> None: + """ + Set the verbosity level + + Args: + level: Verbosity level (0-5) + + Raises: + ZMapOutputError: If the level is out of range + """ + if not 0 <= level <= 5: + raise ZMapOutputError( + f"Verbosity level must be between 0 and 5, got {level}", + ) + self.verbosity = level + + def enable_quiet_mode(self) -> None: + """Enable quiet mode (no status updates)""" + self.quiet = True + + def disable_quiet_mode(self) -> None: + """Disable quiet mode""" + self.quiet = False + + def enable_syslog(self) -> None: + """Enable syslog logging""" + self.disable_syslog = False + + def disable_syslog_logging(self) -> None: + """Disable syslog logging""" + self.disable_syslog = True + + def to_dict(self) -> dict[str, Any]: + """Convert output configuration to a dictionary for command-line options""" + result = {} + + if self.output_file: + result["output_file"] = self.output_file + + if self.output_fields: + result["output_fields"] = self.output_fields + + if self.output_module: + result["output_module"] = self.output_module + + if self.output_filter: + result["output_filter"] = self.output_filter + + if self.output_args: + result["output_args"] = self.output_args + + if self.log_file: + result["log_file"] = self.log_file + + if self.log_directory: + result["log_directory"] = self.log_directory + + if self.metadata_file: + result["metadata_file"] = self.metadata_file + + if self.status_updates_file: + result["status_updates_file"] = self.status_updates_file + + if self.verbosity is not None: + result["verbosity"] = self.verbosity + + if self.quiet: + result["quiet"] = True + + if self.disable_syslog: + result["disable_syslog"] = True + + return result + + @staticmethod + def get_common_output_fields() -> dict[str, str]: + """ + Get a dictionary of common output fields and their descriptions + + Returns: + Dictionary mapping field names to descriptions + """ + return { + "saddr": "Source IP address", + "daddr": "Destination IP address", + "sport": "Source port", + "dport": "Destination port", + "seqnum": "TCP sequence number", + "acknum": "TCP acknowledgement number", + "window": "TCP window", + "classification": "Response classification (e.g., synack, rst)", + "success": "Whether the probe was successful", + "repeat": "Whether this response is a repeat", + "cooldown": "Cooldown time in seconds", + "timestamp_ts": "Timestamp in seconds", + "timestamp_us": "Microsecond component of timestamp", + "icmp_type": "ICMP type", + "icmp_code": "ICMP code", + "icmp_unreach_str": "ICMP unreachable string", + "data": "Application response data", + "ttl": "Time to live", + } diff --git a/zmapsdk/parser.py b/zmapsdk/parser.py index 6312391..b845d38 100644 --- a/zmapsdk/parser.py +++ b/zmapsdk/parser.py @@ -5,43 +5,47 @@ import csv import json import os -from typing import List, Dict, Any, Optional, Union, Iterator +from collections.abc import Iterator +from typing import Any -from .exceptions import ZMapParserError +from zmapsdk.exceptions import ZMapParserError class ZMapParser: """ Class for parsing ZMap output files """ - + @staticmethod - def parse_csv_results(file_path: str, fields: Optional[List[str]] = None) -> List[Dict[str, str]]: + def parse_csv_results( + file_path: str, + fields: list[str] | None = None, + ) -> list[dict[str, str]]: """ Parse a CSV results file from ZMap - + Args: file_path: Path to the CSV file fields: List of field names (if not provided, will try to read from header) - + Returns: List of dictionaries, each representing a row with field names as keys - + Raises: ZMapParserError: If the file can't be parsed """ if not os.path.isfile(file_path): raise ZMapParserError(f"Results file not found: {file_path}") - + try: results = [] - with open(file_path, 'r') as f: + with open(file_path) as f: # Check if file has a header first_line = f.readline().strip() f.seek(0) # Reset to beginning of file - + # If fields not provided and first line contains commas, try to use it as header - if fields is None and ',' in first_line: + if fields is None and "," in first_line: reader = csv.DictReader(f) for row in reader: results.append(dict(row)) @@ -51,9 +55,11 @@ def parse_csv_results(file_path: str, fields: Optional[List[str]] = None) -> Lis for row in reader: if len(row) != len(fields): raise ZMapParserError( - f"CSV row has {len(row)} fields, but {len(fields)} field names provided" + f"CSV row has {len(row)} fields, but {len(fields)} field names provided", ) - results.append({field: value for field, value in zip(fields, row)}) + results.append( + {field: value for field, value in zip(fields, row)}, + ) # If only IPs (single column), treat each line as an IP else: reader = csv.reader(f) @@ -62,35 +68,35 @@ def parse_csv_results(file_path: str, fields: Optional[List[str]] = None) -> Lis results.append({"saddr": row[0]}) else: raise ZMapParserError( - "CSV has multiple columns but no field names provided" + "CSV has multiple columns but no field names provided", ) - + return results - - except (csv.Error, IOError) as e: - raise ZMapParserError(f"Failed to parse CSV results: {str(e)}") - + + except (csv.Error, OSError) as e: + raise ZMapParserError(f"Failed to parse CSV results: {e!s}") + @staticmethod - def parse_json_results(file_path: str) -> List[Dict[str, Any]]: + def parse_json_results(file_path: str) -> list[dict[str, Any]]: """ Parse a JSON results file from ZMap - + Args: file_path: Path to the JSON file - + Returns: List of dictionaries from the JSON file - + Raises: ZMapParserError: If the file can't be parsed """ if not os.path.isfile(file_path): raise ZMapParserError(f"Results file not found: {file_path}") - + try: - with open(file_path, 'r') as f: + with open(file_path) as f: data = json.load(f) - + # Handle both array and object formats if isinstance(data, list): return data @@ -98,82 +104,85 @@ def parse_json_results(file_path: str) -> List[Dict[str, Any]]: return [data] else: raise ZMapParserError(f"Unexpected JSON format: {type(data)}") - - except (json.JSONDecodeError, IOError) as e: - raise ZMapParserError(f"Failed to parse JSON results: {str(e)}") - + + except (json.JSONDecodeError, OSError) as e: + raise ZMapParserError(f"Failed to parse JSON results: {e!s}") + @staticmethod - def parse_metadata(file_path: str) -> Dict[str, Any]: + def parse_metadata(file_path: str) -> dict[str, Any]: """ Parse a ZMap metadata file (JSON format) - + Args: file_path: Path to the metadata file - + Returns: Dictionary containing metadata - + Raises: ZMapParserError: If the file can't be parsed """ if not os.path.isfile(file_path): raise ZMapParserError(f"Metadata file not found: {file_path}") - + try: - with open(file_path, 'r') as f: + with open(file_path) as f: return json.load(f) - - except (json.JSONDecodeError, IOError) as e: - raise ZMapParserError(f"Failed to parse metadata: {str(e)}") - + + except (json.JSONDecodeError, OSError) as e: + raise ZMapParserError(f"Failed to parse metadata: {e!s}") + @staticmethod - def parse_status_updates(file_path: str) -> List[Dict[str, Any]]: + def parse_status_updates(file_path: str) -> list[dict[str, Any]]: """ Parse a ZMap status updates file (CSV format) - + Args: file_path: Path to the status updates file - + Returns: List of dictionaries, each representing a status update - + Raises: ZMapParserError: If the file can't be parsed """ if not os.path.isfile(file_path): raise ZMapParserError(f"Status updates file not found: {file_path}") - + try: updates = [] - with open(file_path, 'r') as f: + with open(file_path) as f: reader = csv.DictReader(f) for row in reader: # Convert numeric fields to appropriate types for key, value in row.items(): - if key in ['time', 'sent', 'recv', 'hits', 'cooldown_secs']: + if key in ["time", "sent", "recv", "hits", "cooldown_secs"]: try: - row[key] = float(value) if '.' in value else int(value) + row[key] = float(value) if "." in value else int(value) except ValueError: pass # Keep as string if conversion fails updates.append(row) - + return updates - - except (csv.Error, IOError) as e: - raise ZMapParserError(f"Failed to parse status updates: {str(e)}") - + + except (csv.Error, OSError) as e: + raise ZMapParserError(f"Failed to parse status updates: {e!s}") + @staticmethod - def extract_ips(results: List[Dict[str, Any]], ip_field: str = 'saddr') -> List[str]: + def extract_ips( + results: list[dict[str, Any]], + ip_field: str = "saddr", + ) -> list[str]: """ Extract IP addresses from parsed results - + Args: results: List of result dictionaries ip_field: Field name containing IP addresses - + Returns: List of IP addresses - + Raises: ZMapParserError: If the IP field is missing from any result """ @@ -181,33 +190,36 @@ def extract_ips(results: List[Dict[str, Any]], ip_field: str = 'saddr') -> List[ return [result[ip_field] for result in results] except KeyError: raise ZMapParserError(f"IP field '{ip_field}' not found in results") - + @staticmethod - def stream_results(file_path: str, fields: Optional[List[str]] = None) -> Iterator[Dict[str, str]]: + def stream_results( + file_path: str, + fields: list[str] | None = None, + ) -> Iterator[dict[str, str]]: """ Stream results from a CSV file without loading everything into memory - + Args: file_path: Path to the CSV file fields: List of field names (if not provided, will try to read from header) - + Yields: Dictionaries, each representing a row with field names as keys - + Raises: ZMapParserError: If the file can't be parsed """ if not os.path.isfile(file_path): raise ZMapParserError(f"Results file not found: {file_path}") - + try: - with open(file_path, 'r') as f: + with open(file_path) as f: # Check if file has a header first_line = f.readline().strip() f.seek(0) # Reset to beginning of file - + # If fields not provided and first line contains commas, try to use it as header - if fields is None and ',' in first_line: + if fields is None and "," in first_line: reader = csv.DictReader(f) for row in reader: yield dict(row) @@ -217,7 +229,7 @@ def stream_results(file_path: str, fields: Optional[List[str]] = None) -> Iterat for row in reader: if len(row) != len(fields): raise ZMapParserError( - f"CSV row has {len(row)} fields, but {len(fields)} field names provided" + f"CSV row has {len(row)} fields, but {len(fields)} field names provided", ) yield {field: value for field, value in zip(fields, row)} # If only IPs (single column), treat each line as an IP @@ -228,43 +240,43 @@ def stream_results(file_path: str, fields: Optional[List[str]] = None) -> Iterat yield {"saddr": row[0]} else: raise ZMapParserError( - "CSV has multiple columns but no field names provided" + "CSV has multiple columns but no field names provided", ) - - except (csv.Error, IOError) as e: - raise ZMapParserError(f"Failed to parse CSV results: {str(e)}") - + + except (csv.Error, OSError) as e: + raise ZMapParserError(f"Failed to parse CSV results: {e!s}") + @staticmethod def count_results(file_path: str) -> int: """ Count the number of results in a file without loading everything into memory - + Args: file_path: Path to the results file - + Returns: Number of result rows - + Raises: ZMapParserError: If the file can't be read """ if not os.path.isfile(file_path): raise ZMapParserError(f"Results file not found: {file_path}") - + try: - with open(file_path, 'r') as f: + with open(file_path) as f: # Check if it's a CSV with header first_line = f.readline().strip() - has_header = ',' in first_line - + has_header = "," in first_line + # Start count at 0 if header, 1 if already counted first row count = 0 if has_header else 1 - + # Count remaining lines for _ in f: count += 1 - + return count - - except IOError as e: - raise ZMapParserError(f"Failed to read results file: {str(e)}") \ No newline at end of file + + except OSError as e: + raise ZMapParserError(f"Failed to read results file: {e!s}") diff --git a/zmapsdk/runner.py b/zmapsdk/runner.py index 1041784..6bfb2cf 100644 --- a/zmapsdk/runner.py +++ b/zmapsdk/runner.py @@ -2,64 +2,63 @@ Runner module for ZMap SDK """ -import subprocess -import json import os +import subprocess import tempfile -import time -from typing import List, Dict, Any, Optional, Union, Tuple, Callable +from collections.abc import Callable -from .exceptions import ZMapCommandError -from .config import ZMapScanConfig -from .input import ZMapInput -from .output import ZMapOutput +from zmapsdk.config import ZMapScanConfig +from zmapsdk.exceptions import ZMapCommandError +from zmapsdk.input import ZMapInput +from zmapsdk.output import ZMapOutput class ZMapRunner: """ Class for running ZMap commands """ - + def __init__(self, zmap_path: str = "zmap"): """ Initialize the ZMap runner - + Args: zmap_path: Path to the zmap executable (defaults to "zmap", assuming it's in PATH) """ self.zmap_path = zmap_path self._check_zmap_exists() - + def _check_zmap_exists(self) -> None: """Check if zmap executable exists and is accessible""" try: - subprocess.run([self.zmap_path, "--version"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - check=True) + subprocess.run( + [self.zmap_path, "--version"], + capture_output=True, + check=True, + ) except (subprocess.SubprocessError, FileNotFoundError) as e: raise ZMapCommandError(command=self.zmap_path, returncode=-1, stderr=str(e)) - - def _build_command(self, **kwargs) -> List[str]: + + def _build_command(self, **kwargs) -> list[str]: """ Build zmap command from parameters - + Args: **kwargs: Command-line options as keyword arguments - + Returns: List of command parts """ cmd = [self.zmap_path] - + # Process all the parameters for key, value in kwargs.items(): if value is None: continue - + # Convert underscores to hyphens for flags - key = key.replace('_', '-') - + key = key.replace("_", "-") + # Boolean flags if isinstance(value, bool): if value: @@ -73,19 +72,21 @@ def _build_command(self, **kwargs) -> List[str]: # All other parameters else: cmd.append(f"--{key}={value}") - + return cmd - - def run_command(self, - config: Optional[ZMapScanConfig] = None, - input_config: Optional[ZMapInput] = None, - output_config: Optional[ZMapOutput] = None, - capture_output: bool = True, - callback: Optional[Callable[[str], None]] = None, - **kwargs) -> Tuple[int, str, str]: + + def run_command( + self, + config: ZMapScanConfig | None = None, + input_config: ZMapInput | None = None, + output_config: ZMapOutput | None = None, + capture_output: bool = True, + callback: Callable[[str], None] | None = None, + **kwargs, + ) -> tuple[int, str, str]: """ Run a ZMap command with the specified parameters - + Args: config: Configuration object input_config: Input configuration object @@ -93,31 +94,31 @@ def run_command(self, capture_output: Whether to capture and return command output callback: Optional callback function for real-time output **kwargs: Additional parameters to pass to zmap - + Returns: Tuple of (return code, stdout, stderr) """ # Combine all parameters combined_params = {} - + # Add config parameters if provided if config: combined_params.update(config.to_dict()) - + # Add input parameters if provided if input_config: combined_params.update(input_config.to_dict()) - + # Add output parameters if provided if output_config: combined_params.update(output_config.to_dict()) - + # Add any additional parameters combined_params.update(kwargs) - + # Build command cmd = self._build_command(**combined_params) - + # Run command try: if callback: @@ -129,52 +130,59 @@ def run_command(self, text=True, bufsize=1, # Line buffered ) - + stdout_data = [] stderr_data = [] - + # Read and process stdout in real-time if process.stdout: - for line in iter(process.stdout.readline, ''): + for line in iter(process.stdout.readline, ""): if not line: break stdout_data.append(line) callback(line) - + # Read stderr if process.stderr: - for line in iter(process.stderr.readline, ''): + for line in iter(process.stderr.readline, ""): if not line: break stderr_data.append(line) - + process.wait() - - return process.returncode, ''.join(stdout_data), ''.join(stderr_data) - + + return process.returncode, "".join(stdout_data), "".join(stderr_data) + elif capture_output: # Simple execution with output capture - result = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True) + result = subprocess.run( + cmd, + capture_output=True, + text=True, + check=False, + ) return result.returncode, result.stdout, result.stderr - + else: # Just run the command without capturing output - result = subprocess.run(cmd) + result = subprocess.run(cmd, check=False) return result.returncode, "", "" - + except subprocess.SubprocessError as e: raise ZMapCommandError(command=" ".join(cmd), returncode=-1, stderr=str(e)) - - def scan(self, - config: Optional[ZMapScanConfig] = None, - input_config: Optional[ZMapInput] = None, - output_config: Optional[ZMapOutput] = None, - temp_output_file: bool = False, - callback: Optional[Callable[[str], None]] = None, - **kwargs) -> List[str]: + + def scan( + self, + config: ZMapScanConfig | None = None, + input_config: ZMapInput | None = None, + output_config: ZMapOutput | None = None, + temp_output_file: bool = False, + callback: Callable[[str], None] | None = None, + **kwargs, + ) -> list[str]: """ Perform a scan and return the results - + Args: config: Configuration object input_config: Input configuration object @@ -182,14 +190,14 @@ def scan(self, temp_output_file: Whether to use a temporary output file callback: Optional callback function for real-time output **kwargs: Additional parameters to pass to zmap - + Returns: List of IP addresses that responded """ # Ensure we have an output configuration if output_config is None: output_config = ZMapOutput() - + # Create temporary output file if requested temp_file = None try: @@ -197,42 +205,44 @@ def scan(self, temp_fd, temp_file = tempfile.mkstemp(prefix="zmap_", suffix=".txt") os.close(temp_fd) # Close the file descriptor output_config.set_output_file(temp_file) - + # Set default output module and fields if not specified if not output_config.output_module: - output_config.set_output_module('csv') + output_config.set_output_module("csv") if not output_config.output_fields: - output_config.set_output_fields('saddr') - if not output_config.output_filter and output_config.output_module == 'csv': + output_config.set_output_fields("saddr") + if not output_config.output_filter and output_config.output_module == "csv": output_config.set_output_filter("success = 1 && repeat = 0") - + # Run the scan returncode, stdout, stderr = self.run_command( config=config, input_config=input_config, output_config=output_config, callback=callback, - **kwargs + **kwargs, ) - + if returncode != 0: raise ZMapCommandError( - command=" ".join(self._build_command( - **(config.to_dict() if config else {}), - **(input_config.to_dict() if input_config else {}), - **(output_config.to_dict() if output_config else {}), - **kwargs - )), + command=" ".join( + self._build_command( + **(config.to_dict() if config else {}), + **(input_config.to_dict() if input_config else {}), + **(output_config.to_dict() if output_config else {}), + **kwargs, + ), + ), returncode=returncode, - stderr=stderr + stderr=stderr, ) - + # Read results from file - with open(output_config.output_file, 'r') as f: + with open(output_config.output_file) as f: results = [line.strip() for line in f if line.strip()] - + return results - + finally: # Clean up the temporary file if we created one if temp_file and os.path.exists(temp_file): @@ -240,11 +250,11 @@ def scan(self, os.unlink(temp_file) except OSError: pass - - def get_probe_modules(self) -> List[str]: + + def get_probe_modules(self) -> list[str]: """ Get list of available probe modules - + Returns: List of available probe module names """ @@ -253,9 +263,9 @@ def get_probe_modules(self) -> List[str]: raise ZMapCommandError( command=f"{self.zmap_path} --list-probe-modules", returncode=returncode, - stderr=stderr + stderr=stderr, ) - + # Parse output to extract module names modules = [] for line in stdout.splitlines(): @@ -266,13 +276,13 @@ def get_probe_modules(self) -> List[str]: parts = line.split(None, 1) if parts: modules.append(parts[0]) - + return modules - - def get_output_modules(self) -> List[str]: + + def get_output_modules(self) -> list[str]: """ Get list of available output modules - + Returns: List of available output module names """ @@ -281,9 +291,9 @@ def get_output_modules(self) -> List[str]: raise ZMapCommandError( command=f"{self.zmap_path} --list-output-modules", returncode=returncode, - stderr=stderr + stderr=stderr, ) - + # Parse output to extract module names modules = [] for line in stdout.splitlines(): @@ -294,31 +304,31 @@ def get_output_modules(self) -> List[str]: parts = line.split(None, 1) if parts: modules.append(parts[0]) - + return modules - - def get_output_fields(self, probe_module: Optional[str] = None) -> List[str]: + + def get_output_fields(self, probe_module: str | None = None) -> list[str]: """ Get list of available output fields for the specified probe module - + Args: probe_module: Probe module to get output fields for (optional) - + Returns: List of available output field names """ cmd = {"list_output_fields": True} if probe_module: cmd["probe_module"] = probe_module - + returncode, stdout, stderr = self.run_command(**cmd) if returncode != 0: raise ZMapCommandError( command=f"{self.zmap_path} --list-output-fields", returncode=returncode, - stderr=stderr + stderr=stderr, ) - + # Parse output to extract field names fields = [] for line in stdout.splitlines(): @@ -329,37 +339,40 @@ def get_output_fields(self, probe_module: Optional[str] = None) -> List[str]: parts = line.split(None, 1) if parts: fields.append(parts[0]) - + return fields - - def get_interfaces(self) -> List[str]: + + def get_interfaces(self) -> list[str]: """ Get list of available network interfaces - + Returns: List of available interface names """ try: # Using psutil to get network interfaces import psutil + return [iface for iface in psutil.net_if_addrs().keys()] except ImportError: # Fallback to socket for basic interface detection + import platform import socket import subprocess - import platform - + os_name = platform.system().lower() interfaces = [] - + # Get interfaces based on the operating system if os_name == "linux" or os_name == "darwin": # Use ifconfig on Unix-like systems try: - proc = subprocess.run(["ifconfig", "-a"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True) + proc = subprocess.run( + ["ifconfig", "-a"], + capture_output=True, + text=True, + check=False, + ) if proc.returncode == 0: for line in proc.stdout.splitlines(): if ": " in line: @@ -370,10 +383,12 @@ def get_interfaces(self) -> List[str]: elif os_name == "windows": # Use ipconfig on Windows try: - proc = subprocess.run(["ipconfig"], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True) + proc = subprocess.run( + ["ipconfig"], + capture_output=True, + text=True, + check=False, + ) if proc.returncode == 0: for line in proc.stdout.splitlines(): if "adapter" in line.lower(): @@ -383,18 +398,18 @@ def get_interfaces(self) -> List[str]: interfaces.append(adapter_name) except (subprocess.SubprocessError, FileNotFoundError): pass - + # If we still don't have interfaces, try to get the hostname if not interfaces: # At minimum, return the hostname interface interfaces.append(socket.gethostname()) - + return interfaces - + def get_version(self) -> str: """ Get ZMap version - + Returns: Version string """ @@ -403,9 +418,9 @@ def get_version(self) -> str: raise ZMapCommandError( command=f"{self.zmap_path} --version", returncode=returncode, - stderr=stderr + stderr=stderr, ) - + # Extract version from output for line in stdout.splitlines(): line = line.strip() @@ -414,6 +429,6 @@ def get_version(self) -> str: for i, part in enumerate(parts): if part.lower() == "version" and i + 1 < len(parts): return parts[i + 1] - + # If we couldn't parse the version, return the first line of output - return stdout.splitlines()[0].strip() if stdout else "Unknown version" \ No newline at end of file + return stdout.splitlines()[0].strip() if stdout else "Unknown version" diff --git a/zmapsdk/schemas.py b/zmapsdk/schemas.py new file mode 100644 index 0000000..e3c8442 --- /dev/null +++ b/zmapsdk/schemas.py @@ -0,0 +1,41 @@ +from pydantic import BaseModel, Field + + +class ScanRequest(BaseModel): + target_port: int | None = Field(None, description="Port number to scan") + subnets: list[str] | None = Field(None, description="List of subnets to scan") + output_file: str | None = Field(None, description="Output file path") + blocklist_file: str | None = Field(None, description="Path to blocklist file") + allowlist_file: str | None = Field(None, description="Path to allowlist file") + bandwidth: str | None = Field(None, description="Bandwidth cap for scan") + probe_module: str | None = Field(None, description="Probe module to use") + rate: int | None = Field(None, description="Packets per second to send") + seed: int | None = Field(None, description="Random seed") + verbosity: int | None = Field(None, description="Verbosity level") + return_results: bool = Field( + False, + description="Return results directly in response instead of writing to file", + ) + # Add other relevant parameters as needed + + +class ScanResult(BaseModel): + scan_id: str + status: str + ips_found: list[str] | None = None + output_file: list[str] | None = None + error: list[str] | None = None + + +class BlocklistRequest(BaseModel): + subnets: list[str] + output_file: str | None = None + + +class StandardBlocklistRequest(BaseModel): + output_file: str | None = None + + +class FileResponse(BaseModel): + file_path: str + message: str