diff --git a/.bandit.yml b/.bandit.yml deleted file mode 100644 index 56f7a83b..00000000 --- a/.bandit.yml +++ /dev/null @@ -1,6 +0,0 @@ ---- -skips: [] -# No need to check for security issues in the test scripts! -exclude_dirs: - - "./tests/" - - "./.venv/" diff --git a/.cookiecutter.json b/.cookiecutter.json new file mode 100644 index 00000000..9fb4b1d8 --- /dev/null +++ b/.cookiecutter.json @@ -0,0 +1,30 @@ +{ + "cookiecutter": { + "codeowner_github_usernames": "@jeffkala @pszulczewski @pke11y", + "full_name": "Network to Code, LLC", + "email": "info@networktocode.com", + "github_org": "networktocode", + "description": "Python library focused on tasks related to device level and OS management.", + "project_name": "pyntc", + "project_slug": "pyntc", + "repo_url": "https://github.com/networktocode/pyntc", + "base_url": "pyntc", + "project_python_name": "pyntc", + "project_python_base_version": "3.10", + "project_with_config_settings": "no", + "generate_docs": "no", + "version": "2.0.2", + "original_publish_year": "2015", + "_drift_manager": { + "template": "https://github.com/networktocode-llc/cookiecutter-ntc.git", + "template_dir": "python", + "template_ref": "main", + "cookie_dir": "", + "pull_request_strategy": "create", + "post_actions": [], + "draft": false, + "baked_commit_ref": "a5add2662f26a3b877635bbf1fd787682414d2bf", + "drift_managed_branch": "develop" + } + } +} diff --git a/.flake8 b/.flake8 deleted file mode 100644 index 1587fc6c..00000000 --- a/.flake8 +++ /dev/null @@ -1,6 +0,0 @@ -[flake8] -# E501: Line length is enforced by Black, so flake8 doesn't need to check it -# W503: Black disagrees with this rule, as does PEP 8; Black wins -ignore = E501, W503 -exclude = - .venv diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index e4553f13..fcb97c51 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1,2 @@ # Default owner(s) of all files in this repository -* @jeffkala @pszulczewski @pke11y \ No newline at end of file +* @jeffkala @pszulczewski @pke11y diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index 1a03085d..a5e70455 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -4,7 +4,7 @@ about: Report a reproducible bug in the current release of pyntc --- ### Environment -* Python version: +* Python version: * pyntc version: diff --git a/.github/styles/dummy.txt b/.github/styles/dummy.txt deleted file mode 100644 index aa86833c..00000000 --- a/.github/styles/dummy.txt +++ /dev/null @@ -1,5 +0,0 @@ -I am a placeholder file to make Vale function. - -Please do not delete me. - -K thanx bai! \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 333ac43d..a9d5704b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,242 +8,171 @@ on: # yamllint disable-line rule:truthy rule:comments branches: - "main" - "develop" - tags: - - "v*" pull_request: ~ env: - IMAGE_NAME: "pyntc" + INVOKE_PYNTC_IMAGE_NAME: "pyntc" + INVOKE_PYNTC_IMAGE_VER: "latest" jobs: - black: - runs-on: "ubuntu-20.04" + ruff-format: + runs-on: "ubuntu-latest" env: - INVOKE_LOCAL: "True" + INVOKE_PYNTC_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" - - name: "Linting: black" - run: "poetry run invoke black" - bandit: - runs-on: "ubuntu-20.04" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" + - name: "Linting: ruff format" + run: "poetry run invoke ruff --action format" + ruff-lint: + runs-on: "ubuntu-latest" env: - INVOKE_LOCAL: "True" + INVOKE_PYNTC_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" - - name: "Linting: bandit" - run: "poetry run invoke bandit" - needs: - - "black" - pydocstyle: - runs-on: "ubuntu-20.04" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" + - name: "Linting: ruff" + run: "poetry run invoke ruff --action lint" + check-docs-build: + runs-on: "ubuntu-latest" env: - INVOKE_LOCAL: "True" + INVOKE_PYNTC_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" - - name: "Linting: pydocstyle" - run: "poetry run invoke pydocstyle" - needs: - - "black" - flake8: - runs-on: "ubuntu-20.04" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" + poetry-install-options: "--only dev,docs" + - name: "Check Docs Build" + run: "poetry run invoke build-and-check-docs" + poetry: + runs-on: "ubuntu-latest" env: - INVOKE_LOCAL: "True" + INVOKE_PYNTC_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" - - name: "Linting: flake8" - run: "poetry run invoke flake8" - needs: - - "black" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" + - name: "Checking: poetry lock file" + run: "poetry run invoke lock --check" yamllint: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" env: - INVOKE_LOCAL: "True" + INVOKE_PYNTC_LOCAL: "True" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" - name: "Linting: yamllint" run: "poetry run invoke yamllint" + check-in-docker: needs: - - "black" - build: - strategy: - fail-fast: true - matrix: - python-version: ["3.8", "3.11"] - runs-on: "ubuntu-20.04" - env: - PYTHON_VER: "${{ matrix.python-version }}" - steps: - - name: "Check out repository code" - uses: "actions/checkout@v2" - - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" - - name: "Get image version" - run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" - - name: "Set up Docker Buildx" - id: "buildx" - uses: "docker/setup-buildx-action@v1" - - name: "Build" - uses: "docker/build-push-action@v2" - with: - builder: "${{ steps.buildx.outputs.name }}" - context: "./" - push: false - tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" - file: "./Dockerfile" - cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - build-args: | - PYTHON_VER=${{ env.PYTHON_VER }} - needs: - - "bandit" - - "pydocstyle" - - "flake8" + - "ruff-format" + - "ruff-lint" + - "poetry" - "yamllint" - pylint: - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-latest" strategy: fail-fast: true matrix: - python-version: ["3.8"] + python-version: ["3.10", "3.13"] env: - PYTHON_VER: "${{ matrix.python-version }}" + INVOKE_PYNTC_PYTHON_VER: "${{ matrix.python-version }}" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" - name: "Get image version" - run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" + run: "echo INVOKE_PYNTC_IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" - name: "Set up Docker Buildx" id: "buildx" - uses: "docker/setup-buildx-action@v1" - - name: "Load the image from cache" - uses: "docker/build-push-action@v2" + uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" # v3.10.0 + - name: "Build" + uses: "docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25" # v5.4.0 with: builder: "${{ steps.buildx.outputs.name }}" context: "./" push: false load: true - tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" + tags: "${{ env.INVOKE_PYNTC_IMAGE_NAME }}:${{ env.INVOKE_PYNTC_IMAGE_VER }}" file: "./Dockerfile" - cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" + cache-from: "type=gha,scope=${{ env.INVOKE_PYNTC_IMAGE_NAME }}-${{ env.INVOKE_PYNTC_IMAGE_VER }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ env.INVOKE_PYNTC_IMAGE_NAME }}-${{ env.INVOKE_PYNTC_IMAGE_VER }}-py${{ matrix.python-version }}" build-args: | - PYTHON_VER=${{ env.PYTHON_VER }} - - name: "Debug: Show docker images" - run: "docker image ls" + PYTHON_VER=${{ matrix.python-version }} - name: "Linting: Pylint" run: "poetry run invoke pylint" - needs: - - "build" pytest: + needs: + - "check-in-docker" strategy: fail-fast: true matrix: - python-version: ["3.8", "3.11"] - runs-on: "ubuntu-20.04" + python-version: ["3.10", "3.11", "3.12", "3.13"] + runs-on: "ubuntu-latest" env: - PYTHON_VER: "${{ matrix.python-version }}" + INVOKE_PYNTC_PYTHON_VER: "${{ matrix.python-version }}" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" + uses: "actions/checkout@v4" - name: "Setup environment" - uses: "networktocode/gh-action-setup-poetry-environment@v2" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" - name: "Get image version" - run: "echo IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" + run: "echo INVOKE_PYNTC_IMAGE_VER=`poetry version -s`-py${{ matrix.python-version }} >> $GITHUB_ENV" - name: "Set up Docker Buildx" id: "buildx" - uses: "docker/setup-buildx-action@v1" - - name: "Load the image from cache" - uses: "docker/build-push-action@v2" + uses: "docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2" # v3.10.0 + - name: "Build" + uses: "docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25" # v5.4.0 with: builder: "${{ steps.buildx.outputs.name }}" context: "./" push: false load: true - tags: "${{ env.IMAGE_NAME }}:${{ env.IMAGE_VER }}" + tags: "${{ env.INVOKE_PYNTC_IMAGE_NAME }}:${{ env.INVOKE_PYNTC_IMAGE_VER }}" file: "./Dockerfile" - cache-from: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" - cache-to: "type=gha,scope=${{ env.IMAGE_NAME }}-${{ env.IMAGE_VER }}-py${{ matrix.python-version }}" + cache-from: "type=gha,scope=${{ env.INVOKE_PYNTC_IMAGE_NAME }}-${{ env.INVOKE_PYNTC_IMAGE_VER }}-py${{ matrix.python-version }}" + cache-to: "type=gha,scope=${{ env.INVOKE_PYNTC_IMAGE_NAME }}-${{ env.INVOKE_PYNTC_IMAGE_VER }}-py${{ matrix.python-version }}" build-args: | - PYTHON_VER=${{ env.PYTHON_VER }} - - name: "Debug: Show docker images" - run: "docker image ls" + PYTHON_VER=${{ matrix.python-version }} - name: "Run Tests" run: "poetry run invoke pytest" - needs: - - "pylint" - - "bandit" - - "pydocstyle" - - "flake8" - - "yamllint" - publish_gh: - name: "Publish to GitHub" - runs-on: "ubuntu-20.04" - if: "startsWith(github.ref, 'refs/tags/v')" - steps: - - name: "Check out repository code" - uses: "actions/checkout@v2" - - name: "Set up Python" - uses: "actions/setup-python@v2" - with: - python-version: "3.9" - - name: "Install Python Packages" - run: "pip install poetry" - - name: "Set env" - run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV" - - name: "Run Poetry Version" - run: "poetry version $RELEASE_VERSION" - - name: "Run Poetry Build" - run: "poetry build" - - name: "Upload binaries to release" - uses: "svenstaro/upload-release-action@v2" - with: - repo_token: "${{ secrets.NTC_GITHUB_TOKEN }}" - file: "dist/*" - tag: "${{ github.ref }}" - overwrite: true - file_glob: true - needs: - - "pytest" - publish_pypi: - name: "Push Package to PyPI" - runs-on: "ubuntu-20.04" - if: "startsWith(github.ref, 'refs/tags/v')" + changelog: + if: > + contains(fromJson('["develop"]'), github.base_ref) && + (github.head_ref != 'main') && (!startsWith(github.head_ref, 'release')) + runs-on: "ubuntu-latest" steps: - name: "Check out repository code" - uses: "actions/checkout@v2" - - name: "Set up Python" - uses: "actions/setup-python@v2" + uses: "actions/checkout@v4" with: - python-version: "3.9" - - name: "Install Python Packages" - run: "pip install poetry" - - name: "Set env" - run: "echo RELEASE_VERSION=${GITHUB_REF:10} >> $GITHUB_ENV" - - name: "Run Poetry Version" - run: "poetry version $RELEASE_VERSION" - - name: "Run Poetry Build" - run: "poetry build" - - name: "Push to PyPI" - uses: "pypa/gh-action-pypi-publish@release/v1" + fetch-depth: "0" + - name: "Setup environment" + uses: "networktocode/gh-action-setup-poetry-environment@v6" with: - user: "__token__" - password: "${{ secrets.PYPI_API_TOKEN }}" - needs: - - "pytest" + poetry-version: "2.1.3" + - name: "Check for changelog entry" + run: | + git fetch --no-tags origin +refs/heads/${{ github.base_ref }}:refs/remotes/origin/${{ github.base_ref }} + poetry run towncrier check --compare-with origin/${{ github.base_ref }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..7f8a7c15 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,106 @@ +--- +name: "Release" +on: # yamllint disable-line rule:truthy rule:comments + release: + types: ["published"] + +jobs: + build: + name: "Build package with poetry" + runs-on: "ubuntu-latest" + if: "startsWith(github.ref, 'refs/tags/v')" + steps: + - uses: "actions/checkout@v4" + - name: "Setup environment" + uses: "networktocode/gh-action-setup-poetry-environment@v6" + with: + poetry-version: "2.1.3" + python-version: "3.13" + poetry-install-options: "--no-root" + - name: "Run Poetry Build" + run: "poetry build" + + - name: "Check that the release tag matches the version in pyproject.toml" + run: | + if [ "${{ github.ref_name }}" != "v$(poetry version -s)" ]; then exit 1; fi + + - uses: "actions/upload-artifact@v4" + with: + name: "distfiles" + path: "dist/" + if-no-files-found: "error" + + publish-github: + name: "Publish to GitHub" + runs-on: "ubuntu-latest" + if: "startsWith(github.ref, 'refs/tags/v')" + permissions: + contents: "write" + needs: "build" + steps: + - uses: "actions/checkout@v4" + - name: "Retrieve built package from cache" + uses: "actions/download-artifact@v4" + with: + name: "distfiles" + path: "dist/" + + - name: "Upload binaries to release" + run: "gh release upload ${{ github.ref_name }} dist/*.{tar.gz,whl}" + env: + GH_TOKEN: "${{ secrets.NTC_GITHUB_TOKEN }}" + + publish-pypi: + name: "Push Package to PyPI" + runs-on: "ubuntu-latest" + if: "startsWith(github.ref, 'refs/tags/v')" + needs: "build" + environment: "pypi" + # Steps to publish to PyPI. + steps: + - name: "Retrieve built package from cache" + uses: "actions/download-artifact@v4" + with: + name: "distfiles" + path: "dist/" + - name: "Publish package distributions to PyPI" + uses: "pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e" # v1.13.0 + ## Used for networktocode org since trusted publisher isn't supported for GitHub Plan. + with: + user: "__token__" + password: "${{ secrets.PYPI_API_TOKEN }}" + # End publish to PyPI job. + + slack-notify: + needs: + - "publish-github" + - "publish-pypi" + runs-on: "ubuntu-latest" + env: + # Secrets cannot be directly referenced in if: conditionals. They must be set as a job env var first. + # Ref: https://docs.github.com/en/actions/writing-workflows/workflow-syntax-for-github-actions#example-using-secrets + SLACK_WEBHOOK_URL: "${{ secrets.OSS_PYPI_SLACK_WEBHOOK_URL }}" + SLACK_WEBHOOK_TYPE: "INCOMING_WEBHOOK" + SLACK_MESSAGE: >- + *NOTIFICATION: NEW-RELEASE-PUBLISHED*\n + Repository: <${{ github.server_url }}/${{ github.repository }}|${{ github.repository }}>\n + Release: <${{ github.server_url }}/${{ github.repository }}/releases/tag/${{ github.ref_name }}|${{ github.ref_name }}>\n + Published by: <${{ github.server_url }}/${{ github.actor }}|${{ github.actor }}> + steps: + - name: "Send a notification to Slack" + if: "${{ env.SLACK_WEBHOOK_URL != '' }}" + uses: "slackapi/slack-github-action@fcfb566f8b0aab22203f066d80ca1d7e4b5d05b3" # v1.27.1 + with: + payload: | + { + "text": "${{ env.SLACK_MESSAGE }}", + "blocks": [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": "${{ env.SLACK_MESSAGE }}" + } + } + ] + } diff --git a/.gitignore b/.gitignore index cbb9133b..b770b141 100644 --- a/.gitignore +++ b/.gitignore @@ -306,3 +306,6 @@ invoke.yml docs/README.md docs/CHANGELOG.md public +/compose.yaml +/dump.sql +/pyntc/static/pyntc/docs diff --git a/.readthedocs.yml b/.readthedocs.yml index a9d358ef..36f32f00 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -6,17 +6,22 @@ # Required version: 2 -# Set the version of Python in the build environment. +# Setup the build environment. build: - os: "ubuntu-22.04" + os: "ubuntu-lts-latest" tools: - python: "3.10" + python: "3.13" + jobs: + post_install: + # Install poetry + # https://python-poetry.org/docs/#installing-manually + - "pip install poetry" + # Install dependencies 'docs' dependency group + # https://python-poetry.org/docs/managing-dependencies/#dependency-groups + # VIRTUAL_ENV needs to be set manually for now. + # See https://github.com/readthedocs/readthedocs.org/pull/11152/ + - "VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --only docs" mkdocs: configuration: "mkdocs.yml" - # fail_on_warning: true - -# Use our docs/requirements.txt during installation. -python: - install: - - requirements: "docs/requirements.txt" + fail_on_warning: true diff --git a/.vale.ini b/.vale.ini deleted file mode 100644 index a5ed5104..00000000 --- a/.vale.ini +++ /dev/null @@ -1,20 +0,0 @@ -StylesPath = .github/styles -MinAlertLevel = suggestion - -[*.md] -BasedOnStyles = Microsoft, write-good, proselint, alex - -# Microsoft Style Guide disabled rules -Microsoft.Contractions = No -Microsoft.HeadingPunctuation = No -Microsoft.Headings = No -Microsoft.Foreign = No - -# write-good disabled rules -write-good.E-Prime = No - -# alex disabled rules -alex.Condescending = No - -# proseling disabled rules -proselint.Hyperbole = No diff --git a/.yamllint.yml b/.yamllint.yml index b49e490c..b68ca0f5 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -1,8 +1,9 @@ --- extends: "default" rules: - comments: "enable" - empty-values: "disable" + comments: + min-spaces-from-content: 1 + empty-values: "enable" indentation: indent-sequences: "consistent" line-length: "disable" diff --git a/Dockerfile b/Dockerfile index 070c298a..3c1fdca0 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,32 +1,26 @@ -ARG PYTHON_VER +ARG PYTHON_VER="3.10" FROM python:${PYTHON_VER}-slim -# Install all OS package upgrades and dependencies needed to run Nautobot in production -# hadolint ignore=DL3005,DL3008,DL3013 -RUN apt-get update && \ - apt-get upgrade -y && \ - apt-get install --no-install-recommends -y git mime-support curl libxml2 libmariadb3 openssl && \ - apt-get autoremove -y && \ - apt-get clean all && \ - rm -rf /var/lib/apt/lists/* && \ - pip --no-cache-dir install --upgrade pip wheel - -RUN pip install --upgrade pip - -RUN curl -sSL https://install.python-poetry.org -o /tmp/install-poetry.py && \ - python /tmp/install-poetry.py --version 1.6.0 && \ - rm -f /tmp/install-poetry.py +# Install Poetry manually via its installer script; +# if we instead used "pip install poetry" it would install its own dependencies globally which may conflict with ours. +# https://python-poetry.org/docs/master/#installing-with-the-official-installer +# This also makes it so that Poetry will *not* be included in the "final" image since it's not installed to /usr/local/ +ARG POETRY_HOME=/opt/poetry +ARG POETRY_INSTALLER_PARALLEL=true +ARG POETRY_VERSION=2.1.3 +ARG POETRY_VIRTUALENVS_CREATE=false +ADD https://install.python-poetry.org /tmp/install-poetry.py +RUN python /tmp/install-poetry.py # Add poetry install location to the $PATH -ENV PATH="${PATH}:/root/.local/bin" +ENV PATH="${POETRY_HOME}/bin:${PATH}" -WORKDIR /local -COPY pyproject.toml poetry.lock /local/ +RUN poetry config virtualenvs.create ${POETRY_VIRTUALENVS_CREATE} && \ + poetry config installer.parallel "${POETRY_INSTALLER_PARALLEL}" -RUN poetry config virtualenvs.create false \ - && poetry install --no-interaction --no-ansi +WORKDIR /local +COPY . /local -# Do not break dependency caching before installing project -COPY . . -RUN poetry install +# Install the app +RUN poetry install --with dev --all-extras diff --git a/LICENSE b/LICENSE index 58a975f8..d647ae42 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ Apache Software License 2.0 -Copyright (c) 2015, Network to Code, LLC +Copyright (c) 2015-2026, Network to Code, LLC Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -12,4 +12,4 @@ Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and -limitations under the License. \ No newline at end of file +limitations under the License. diff --git a/README.md b/README.md index 6b2549cd..0dab1fdd 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,11 @@ # Pyntc +


- +
@@ -24,11 +25,11 @@ Full web-based HTML documentation for this library can be found over on the [Pyn - [Release Notes / Changelog](https://pyntc.readthedocs.io/en/latest/admin/release_notes/). - [Frequently Asked Questions](https://pyntc.readthedocs.io/en/latest/user/faq/). -### Contributing to the Docs +### Contributing to the Documentation -All the Markdown source for the library documentation can be found under the [docs](https://github.com/networktocode/pyntc/tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient - clone the repository and edit away. +You can find all the Markdown source for the App documentation under the [`docs`](https://github.com/networktocode/pyntc/tree/develop/docs) folder in this repository. For simple edits, a Markdown capable editor is sufficient: clone the repository and edit away. -If you need to view the fully generated documentation site, you can build it with [mkdocs](https://www.mkdocs.org/). A container hosting the docs will be started using the invoke commands (details in the [Development Environment Guide](https://pyntc.readthedocs.io/en/latest/dev/dev_environment/#docker-development-environment)) on [http://localhost:8001](http://localhost:8001). As your changes are saved, the live docs will be automatically reloaded. +If you need to view the fully-generated documentation site, you can build it with [MkDocs](https://www.mkdocs.org/). A container hosting the documentation can be started using the `invoke` commands (details in the [Development Environment Guide](https://pyntc/dev/dev_environment/#docker-development-environment)) on [http://localhost:8001](http://localhost:8001). Using this container, as your changes to the documentation are saved, they will be automatically rebuilt and any pages currently being viewed will be reloaded in your browser. Any PRs with fixes or improvements are very welcome! diff --git a/changes/.gitignore b/changes/.gitignore new file mode 100644 index 00000000..f935021a --- /dev/null +++ b/changes/.gitignore @@ -0,0 +1 @@ +!.gitignore diff --git a/docs/admin/install.md b/docs/admin/install.md index 6bb1d6dc..7efcc289 100644 --- a/docs/admin/install.md +++ b/docs/admin/install.md @@ -2,21 +2,21 @@ Option 1: Install from PyPI. -``` +```bash pip install pyntc ``` Option 2: Manually install via Poetry. -``` +```bash git clone https://github.com/networktocode/pyntc.git cd pyntc -pip install poetry +curl -sSL https://install.python-poetry.org | python3 - poetry install ``` Option 3: Install from a GitHub branch, such as develop as shown below. ```bash -$ pip install git+https://github.com/networktocode/pyntc.git@develop +pip install git+https://github.com/networktocode/pyntc.git@develop ``` diff --git a/docs/admin/release_notes/version_0_0.md b/docs/admin/release_notes/version_0.0.md similarity index 100% rename from docs/admin/release_notes/version_0_0.md rename to docs/admin/release_notes/version_0.0.md diff --git a/docs/admin/release_notes/version_0_14.md b/docs/admin/release_notes/version_0.14.md similarity index 100% rename from docs/admin/release_notes/version_0_14.md rename to docs/admin/release_notes/version_0.14.md diff --git a/docs/admin/release_notes/version_0_15.md b/docs/admin/release_notes/version_0.15.md similarity index 100% rename from docs/admin/release_notes/version_0_15.md rename to docs/admin/release_notes/version_0.15.md diff --git a/docs/admin/release_notes/version_0_16.md b/docs/admin/release_notes/version_0.16.md similarity index 100% rename from docs/admin/release_notes/version_0_16.md rename to docs/admin/release_notes/version_0.16.md diff --git a/docs/admin/release_notes/version_0_17.md b/docs/admin/release_notes/version_0.17.md similarity index 100% rename from docs/admin/release_notes/version_0_17.md rename to docs/admin/release_notes/version_0.17.md diff --git a/docs/admin/release_notes/version_0_18.md b/docs/admin/release_notes/version_0.18.md similarity index 100% rename from docs/admin/release_notes/version_0_18.md rename to docs/admin/release_notes/version_0.18.md diff --git a/docs/admin/release_notes/version_0_19.md b/docs/admin/release_notes/version_0.19.md similarity index 100% rename from docs/admin/release_notes/version_0_19.md rename to docs/admin/release_notes/version_0.19.md diff --git a/docs/admin/release_notes/version_0_20.md b/docs/admin/release_notes/version_0.20.md similarity index 100% rename from docs/admin/release_notes/version_0_20.md rename to docs/admin/release_notes/version_0.20.md diff --git a/docs/admin/release_notes/version_1_0.md b/docs/admin/release_notes/version_1.0.md similarity index 100% rename from docs/admin/release_notes/version_1_0.md rename to docs/admin/release_notes/version_1.0.md diff --git a/docs/admin/release_notes/version_2_0.md b/docs/admin/release_notes/version_2.0.md similarity index 100% rename from docs/admin/release_notes/version_2_0.md rename to docs/admin/release_notes/version_2.0.md diff --git a/docs/admin/release_notes/version_2.1.md b/docs/admin/release_notes/version_2.1.md new file mode 100644 index 00000000..02e86fa5 --- /dev/null +++ b/docs/admin/release_notes/version_2.1.md @@ -0,0 +1,22 @@ + +# v2.1 Release Notes + +This document describes all new features and changes in the release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Release Overview + +- Adding the ability to perform file downloads in Cisco IOS. This enhances OS Upgrades and file operations greatly! + +## [v2.1.0 (2026-03-03)](https://github.com/networktocode/pyntc/releases/tag/v2.1.0) + +### Added + +- [#345](https://github.com/networktocode/pyntc/issues/345) - Added the ability to download files from within a Cisco IOS device. + +### Housekeeping + +- [#335](https://github.com/networktocode/pyntc/issues/335) - Replaced black, bandit, flake8 and pydocstyle with ruff. +- [#335](https://github.com/networktocode/pyntc/issues/335) - Updated tasks.py with newest task list. +- [#335](https://github.com/networktocode/pyntc/issues/335) - Updated to using pyinvoke for development environment definition. +- Fixed docs build and code-reference issues. +- Rebaked from the cookie `main`. diff --git a/docs/admin/uninstall.md b/docs/admin/uninstall.md index 0dcf3849..0ccbd7b8 100644 --- a/docs/admin/uninstall.md +++ b/docs/admin/uninstall.md @@ -3,5 +3,5 @@ Uninstall from environment. ```bash -$ pip uninstall pyntc +pip uninstall pyntc ``` diff --git a/docs/admin/upgrade.md b/docs/admin/upgrade.md index 88c9a334..69a6b98b 100644 --- a/docs/admin/upgrade.md +++ b/docs/admin/upgrade.md @@ -3,5 +3,5 @@ Upgrade from PyPI. ```bash -$ pip install pyntc --upgrade +pip install pyntc --upgrade ``` diff --git a/docs/dev/contributing.md b/docs/dev/contributing.md index eef6e52c..ae2f5cb9 100644 --- a/docs/dev/contributing.md +++ b/docs/dev/contributing.md @@ -4,15 +4,44 @@ Pull requests are welcomed and automatically built and tested against multiple v Except for unit tests, testing is only supported on Python 3.9. -The project is packaged with a light development environment based on `Docker` to help with the local development of the project and to run tests within GitHub Actions. +The project is packaged with a light development environment based on `Docker` to help with the local development of the project and to run tests within GitHub Actions. -The project is following Network to Code software development guidelines and are leveraging the following: +The project is following Network to Code software development guidelines and is leveraging the following: -- Black, Pylint, Bandit, flake8, and pydocstyle for Python linting and formatting. -- pytest, coverage, and unittest for unit tests. +- Python linting and formatting: `pylint` and `ruff`. +- YAML linting is done with `yamllint`. Documentation is built using [mkdocs](https://www.mkdocs.org/). The [Docker based development environment](dev_environment.md#docker-development-environment) can be started by running `invoke docs` [http://localhost:8001](http://localhost:8001) that auto-refreshes when you make any changes to your local files. +## Creating Changelog Fragments + +All pull requests to `next` or `develop` must include a changelog fragment file in the `./changes` directory. To create a fragment, use your GitHub issue number and fragment type as the filename. For example, `2362.added`. Valid fragment types are `added`, `changed`, `deprecated`, `fixed`, `removed`, and `security`. The change summary is added to the file in plain text. Change summaries should be complete sentences, starting with a capital letter and ending with a period, and be in past tense. Each line of the change fragment will generate a single change entry in the release notes. Use multiple lines in the same file if your change needs to generate multiple release notes in the same category. If the change needs to create multiple entries in separate categories, create multiple files. + +!!! example + + **Wrong** + ```plaintext title="changes/1234.fixed" + fix critical bug in documentation + ``` + + **Right** + ```plaintext title="changes/1234.fixed" + Fixed critical bug in documentation. + ``` + +!!! example "Multiple Entry Example" + + This will generate 2 entries in the `fixed` category and one entry in the `changed` category. + + ```plaintext title="changes/1234.fixed" + Fixed critical bug in documentation. + Fixed release notes generation. + ``` + + ```plaintext title="changes/1234.changed" + Changed release notes generation. + ``` + ## Branching Policy The branching policy includes the following tenets: @@ -45,4 +74,4 @@ When a new release is created the following should happen. - A post release PR is created with. - Change the version from `..` to `..-beta` pyproject.toml. - Set the PR to the `develop`. - - Once tests pass, merge. \ No newline at end of file + - Once tests pass, merge. diff --git a/docs/dev/dev_environment.md b/docs/dev/dev_environment.md index 23292492..512dd9ea 100644 --- a/docs/dev/dev_environment.md +++ b/docs/dev/dev_environment.md @@ -32,7 +32,6 @@ Once you have Poetry and Docker installed you can run the following commands (in ```shell poetry shell poetry install -cp development/creds.example.env development/creds.env invoke build invoke start ``` @@ -46,12 +45,12 @@ To either stop or destroy the development environment use the following options. ## Poetry -Poetry is used in lieu of the "virtualenv" commands and is leveraged in both environments. The virtual environment will provide all of the Python packages required to manage the development environment such as **Invoke**. See the [Local Development Environment](#local-poetry-development-environment) section to see how to install Pyntc if you're going to be developing locally (i.e. not using the Docker container). +Poetry is used in lieu of the "virtualenv" commands and is leveraged in both environments. The virtual environment will provide all of the Python packages required to manage the development environment such as **Invoke**. See the [Local Development Environment](#full-docker-development-environment) section to see how to install Pyntc if you're going to be developing locally (i.e. not using the Docker container). The `pyproject.toml` file outlines all of the relevant dependencies for the project: - `tool.poetry.dependencies` - the main list of dependencies. -- `tool.poetry.dev-dependencies` - development dependencies, to facilitate linting, testing, and documentation building. +- `tool.poetry.group.dev.dependencies` - development dependencies, to facilitate linting, testing, and documentation building. The `poetry shell` command is used to create and enable a virtual environment managed by Poetry, so all commands ran going forward are executed within the virtual environment. This is similar to running the `source venv/bin/activate` command with virtualenvs. To install project dependencies in the virtual environment, you should run `poetry install` - this will install **both** project and development dependencies. @@ -82,21 +81,18 @@ Each command can be executed with `invoke `. Each command also has its ### Utility ``` - cli Enter the image to perform troubleshooting or dev work. - clean-container Remove stopped containers that source for image `pyntc:` + cli Enter the image to perform troubleshooting or dev work. + clean Remove stopped containers that source for image `pyntc:` + generate-release-notes Generate Release Notes using Towncrier. ``` ### Testing ``` - bandit Run bandit to validate basic static code security analysis. - black Run black to check that Python files adhere to its style standards. - coverage Run the coverage report against pytest. - flake8 Run flake8 to check that Python files adhere to its style standards. - mypy Run mypy to validate typing-hints. - pylint Run pylint code analysis. - pydocstyle Run pydocstyle to validate docstring formatting adheres to NTC defined standards. - pytest Run pytest for the specified name and Python version. - tests Run all tests for the specified name and Python version. - yamllint Run yamllint to validate formatting adheres to NTC defined YAML standards. + autoformat (a) Run code autoformatting. + pylint Run pylint for the specified name and Python version. + ruff Run ruff to perform code formatting and/or linting. + pytest Run pytest for the specified name and Python version. + tests Run all tests for the specified name and Python version. + yamllint Run yamllint to validate formatting adheres to NTC defined YAML standards. ``` \ No newline at end of file diff --git a/docs/generate_code_reference_pages.py b/docs/generate_code_reference_pages.py new file mode 100644 index 00000000..0f1bed31 --- /dev/null +++ b/docs/generate_code_reference_pages.py @@ -0,0 +1,20 @@ +"""Generate code reference pages.""" + +from pathlib import Path + +import mkdocs_gen_files + +for file_path in Path("pyntc").rglob("*.py"): + module_path = file_path.with_suffix("") + doc_path = file_path.with_suffix(".md") + full_doc_path = Path("code-reference", doc_path) + + parts = list(module_path.parts) + if parts[-1] == "__init__": + parts = parts[:-1] + + with mkdocs_gen_files.open(full_doc_path, "w") as fd: + IDENTIFIER = ".".join(parts) + print(f"::: {IDENTIFIER}", file=fd) + + mkdocs_gen_files.set_edit_path(full_doc_path, file_path) diff --git a/docs/requirements.txt b/docs/requirements.txt index f403e1aa..e5187d1d 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,13 @@ -mkdocs==1.3.1 -# Material for MkDocs theme -mkdocs-material==8.3.9 -# Render custom markdown for version added/changed/remove notes -mkdocs-version-annotations==1.0.0 -# Automatic documentation from sources, for MkDocs -mkdocstrings==0.19 -mkdocstrings-python==0.7.1 \ No newline at end of file +mkdocs==1.6.1 +markdown-data-tables==1.0.0 +markdown-version-annotations==1.0.1 +mkdocs-gen-files==0.5.0 +mkdocs-glightbox==0.4.0 +mkdocs-macros-plugin==1.3.7 +mkdocs-material==9.6.15 +mkdocs-redirects==1.2.2 +mkdocs-section-index==0.3.10 +mkdocs-redirects==1.2.2 +mkdocs-section-index==0.3.10 +mkdocstrings==0.27.0 +mkdocstrings-python==1.13.0 diff --git a/docs/user/lib_getting_started.md b/docs/user/lib_getting_started.md index f164d661..bd530fdd 100644 --- a/docs/user/lib_getting_started.md +++ b/docs/user/lib_getting_started.md @@ -250,6 +250,35 @@ interface GigabitEthernet1 >>> ``` +#### Remote File Copy (Download to Device) + +Some devices support copying files directly from a URL to the device. This is useful for larger files like OS images. To do this, you need to use the `FileCopyModel` data model to specify the source file information and then pass that to the `remote_file_copy` method. Currently only supported on Cisco IOS devices. Tested with ftp, http, https, sftp, and tftp urls. + +- `remote_file_copy` method + +```python +from pyntc.utils.models import FileCopyModel + +>>> source_file = FileCopyModel( +... download_url='sftp://example.com/newconfig.cfg', +... checksum='abc123def456', +... hashing_algorithm='md5', +... file_name='newconfig.cfg', + vrf='Mgmt-vrf' +... ) +>>> for device in devices: +... device.remote_file_copy(source_file) +... +>>> +``` + +Before using this feature you may need to configure a client on the device. For instance, on a Cisco IOS device you would need to set the source interface for the ip http client when using http or https urls. You can do this with the `config` method: + +```python +>>> csr1.config('ip http client source-interface GigabitEthernet1') +>>> +``` + ### Save Configs - `save` method diff --git a/example.invoke.yml b/example.invoke.yml new file mode 100644 index 00000000..dfc31518 --- /dev/null +++ b/example.invoke.yml @@ -0,0 +1,7 @@ +--- +"pyntc": + python_ver: "3.10" + local: false + # image_name: "pyntc" + # image_ver: "latest" + # pwd: "." diff --git a/mkdocs.yml b/mkdocs.yml index 85eb9e2f..bf706005 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -1,6 +1,6 @@ --- dev_addr: "127.0.0.1:8001" -edit_uri: "edit/develop/docs" +edit_uri: "edit/main/pyntc/docs" site_dir: "pyntc/static/pyntc/docs" site_name: "Pyntc Documentation" site_url: "https://pyntc.readthedocs.io/en/latest/" @@ -14,13 +14,19 @@ theme: - "python" - "yaml" features: - - "navigation.tracking" + - "content.code.annotate" + - "content.tabs.link" + - "navigation.footer" + - "content.action.edit" + - "content.action.view" + - "content.code.copy" + - "navigation.indexes" - "navigation.tabs" - "navigation.tabs.sticky" - - "search.suggest" + - "navigation.tracking" - "search.highlight" - "search.share" - - "navigation.indexes" + - "search.suggest" favicon: "assets/favicon.ico" logo: "assets/networktocode_logo.svg" palette: @@ -42,12 +48,6 @@ theme: extra_css: - "assets/extra.css" -# needed for RTD version flyout menu -# jquery is not (yet) injected by RTD automatically and it might be dropped -# as a dependency in the future -extra_javascript: - - "https://code.jquery.com/jquery-3.6.0.min.js" - extra: generator: false ntc_sponsor: true @@ -68,29 +68,57 @@ extra: link: "https://twitter.com/networktocode" name: "Network to Code Twitter" markdown_extensions: + - "markdown_version_annotations": + admonition_tag: "???" - "admonition" - "toc": permalink: true - "attr_list" - "md_in_html" + - "markdown_data_tables": + base_path: "docs" + - "pymdownx.details" + # Need pymdownx.emoji for Grid icon search + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg - "pymdownx.highlight": anchor_linenums: true - "pymdownx.inlinehilite" - "pymdownx.snippets" - - "pymdownx.superfences" - - "footnotes" + - "pymdownx.superfences": + custom_fences: + - name: "mermaid" + class: "mermaid" + format: !!python/name:pymdownx.superfences.fence_code_format + - "pymdownx.tabbed": + "alternate_style": true + - "pymdownx.tilde" + plugins: - "search" - - "mkdocs-version-annotations" + - "gen-files": + scripts: + - "docs/generate_code_reference_pages.py" + - "glightbox": + manual: true # See https://blueswen.github.io/mkdocs-glightbox/flexibility/enable-by-image-or-page/ + - "section-index" - "mkdocstrings": default_handler: "python" handlers: python: paths: ["."] options: + heading_level: 1 show_root_heading: true -watch: - - "README.md" + show_root_members_full_path: true + show_source: false + +validation: + absolute_links: "warn" + anchors: "warn" + omitted_files: "warn" + unrecognized_links: "warn" nav: - Overview: "index.md" @@ -104,17 +132,45 @@ nav: - Uninstall: "admin/uninstall.md" - Release Notes: - "admin/release_notes/index.md" - - v0.0: "admin/release_notes/version_0_0.md" - - v0.14: "admin/release_notes/version_0_14.md" - - v0.15: "admin/release_notes/version_0_15.md" - - v0.16: "admin/release_notes/version_0_16.md" - - v0.17: "admin/release_notes/version_0_17.md" - - v0.18: "admin/release_notes/version_0_18.md" - - v0.19: "admin/release_notes/version_0_19.md" - - v0.20: "admin/release_notes/version_0_20.md" - - v1.0: "admin/release_notes/version_1_0.md" - - v2.0: "admin/release_notes/version_2_0.md" + - v0.0: "admin/release_notes/version_0.0.md" + - v0.14: "admin/release_notes/version_0.14.md" + - v0.15: "admin/release_notes/version_0.15.md" + - v0.16: "admin/release_notes/version_0.16.md" + - v0.17: "admin/release_notes/version_0.17.md" + - v0.18: "admin/release_notes/version_0.18.md" + - v0.19: "admin/release_notes/version_0.19.md" + - v0.20: "admin/release_notes/version_0.20.md" + - v1.0: "admin/release_notes/version_1.0.md" + - v2.0: "admin/release_notes/version_2.0.md" + - v2.1: "admin/release_notes/version_2.1.md" - Developer Guide: - Extending the Library: "dev/extending.md" - Contributing to the Library: "dev/contributing.md" - Development Environment: "dev/dev_environment.md" + - Code Reference: + - pyntc: "code-reference/pyntc/__init__.md" + - pyntc.errors: "code-reference/pyntc/errors.md" + - pyntc.log: "code-reference/pyntc/log.md" + - pyntc.devices: "code-reference/pyntc/devices/__init__.md" + - pyntc.devices.aireos_device: "code-reference/pyntc/devices/aireos_device.md" + - pyntc.devices.asa_device: "code-reference/pyntc/devices/asa_device.md" + - pyntc.devices.base_device: "code-reference/pyntc/devices/base_device.md" + - pyntc.devices.eos_device: "code-reference/pyntc/devices/eos_device.md" + - pyntc.devices.f5_device: "code-reference/pyntc/devices/f5_device.md" + - pyntc.devices.ios_device: "code-reference/pyntc/devices/ios_device.md" + - pyntc.devices.iosxewlc_device: "code-reference/pyntc/devices/iosxewlc_device.md" + - pyntc.devices.jnpr_device: "code-reference/pyntc/devices/jnpr_device.md" + - pyntc.devices.nxos_device: "code-reference/pyntc/devices/nxos_device.md" + - pyntc.devices.system_features: "code-reference/pyntc/devices/system_features/__init__.md" + - pyntc.devices.system_features.base_feature: "code-reference/pyntc/devices/system_features/base_feature.md" + - pyntc.devices.system_features.vlans: "code-reference/pyntc/devices/system_features/vlans/__init__.md" + - pyntc.devices.system_features.vlans.base_vlans: "code-reference/pyntc/devices/system_features/vlans/base_vlans.md" + - pyntc.devices.system_features.vlans.eos_vlans: "code-reference/pyntc/devices/system_features/vlans/eos_vlans.md" + - pyntc.devices.tables: "code-reference/pyntc/devices/tables/__init__.md" + - pyntc.devices.juniper: "code-reference/pyntc/devices/tables/jnpr/__init__.md" + - pyntc.devices.juniper.loopback: "code-reference/pyntc/devices/tables/jnpr/loopback.md" + - pyntc.utils: "code-reference/pyntc/utils/__init__.md" + - pyntc.utils.converters: "code-reference/pyntc/utils/converters.md" + - pyntc.utils.templates: "code-reference/pyntc/utils/templates/__init__.md" + - pyntc.utils.models: "code-reference/pyntc/utils/models.md" + - pyntc.arch_decision: "dev/arch_decision.md" diff --git a/poetry.lock b/poetry.lock index f78d391b..75300ca9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,151 +1,150 @@ -# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "astroid" -version = "3.2.4" +version = "3.3.11" description = "An abstract syntax tree for Python with inference support." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["dev"] files = [ - {file = "astroid-3.2.4-py3-none-any.whl", hash = "sha256:413658a61eeca6202a59231abb473f932038fbcbf1666587f66d482083413a25"}, - {file = "astroid-3.2.4.tar.gz", hash = "sha256:0e14202810b30da1b735827f78f5157be2bbd4a7a59b7707ca0bfc2fb4c0063a"}, + {file = "astroid-3.3.11-py3-none-any.whl", hash = "sha256:54c760ae8322ece1abd213057c4b5bba7c49818853fc901ef09719a60dbf9dec"}, + {file = "astroid-3.3.11.tar.gz", hash = "sha256:1e5a5011af2920c7c67a53f65d536d65bfa7116feeaf2354d8b94f29573bb0ce"}, ] [package.dependencies] -typing-extensions = {version = ">=4.0.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4", markers = "python_version < \"3.11\""} [[package]] -name = "astunparse" -version = "1.6.3" -description = "An AST unparser for Python" +name = "attrs" +version = "23.2.0" +description = "Classes Without Boilerplate" optional = false -python-versions = "*" +python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "astunparse-1.6.3-py2.py3-none-any.whl", hash = "sha256:c2652417f2c8b5bb325c885ae329bdf3f86424075c4fd1a128674bc6fba4b8e8"}, - {file = "astunparse-1.6.3.tar.gz", hash = "sha256:5ad93a8456f0d084c3456d059fd9a92cce667963232cbf763eac3bc5b7940872"}, + {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"}, + {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"}, ] -[package.dependencies] -six = ">=1.6.1,<2.0" -wheel = ">=0.23.0,<1.0" +[package.extras] +cov = ["attrs[tests]", "coverage[toml] (>=5.3)"] +dev = ["attrs[tests]", "pre-commit"] +docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"] +tests = ["attrs[tests-no-zope]", "zope-interface"] +tests-mypy = ["mypy (>=1.6) ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.8\""] +tests-no-zope = ["attrs[tests-mypy]", "cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"] [[package]] -name = "bandit" -version = "1.7.10" -description = "Security oriented static analyser for python code." +name = "babel" +version = "2.17.0" +description = "Internationalization utilities" optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ - {file = "bandit-1.7.10-py3-none-any.whl", hash = "sha256:665721d7bebbb4485a339c55161ac0eedde27d51e638000d91c8c2d68343ad02"}, - {file = "bandit-1.7.10.tar.gz", hash = "sha256:59ed5caf5d92b6ada4bf65bc6437feea4a9da1093384445fed4d472acc6cff7b"}, + {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, + {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] -[package.dependencies] -colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""} -PyYAML = ">=5.3.1" -rich = "*" -stevedore = ">=1.20.0" - [package.extras] -baseline = ["GitPython (>=3.1.30)"] -sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"] -test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"] -toml = ["tomli (>=1.1.0)"] -yaml = ["PyYAML"] +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 = "bcrypt" -version = "4.2.1" -description = "Modern password hashing for your software and your servers" +name = "backrefs" +version = "5.9" +description = "A wrapper around re and regex that adds additional back references." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "bcrypt-4.2.1-cp37-abi3-macosx_10_12_universal2.whl", hash = "sha256:1340411a0894b7d3ef562fb233e4b6ed58add185228650942bdc885362f32c17"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b1ee315739bc8387aa36ff127afc99120ee452924e0df517a8f3e4c0187a0f5f"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8dbd0747208912b1e4ce730c6725cb56c07ac734b3629b60d4398f082ea718ad"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:aaa2e285be097050dba798d537b6efd9b698aa88eef52ec98d23dcd6d7cf6fea"}, - {file = "bcrypt-4.2.1-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:76d3e352b32f4eeb34703370e370997065d28a561e4a18afe4fef07249cb4396"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:b7703ede632dc945ed1172d6f24e9f30f27b1b1a067f32f68bf169c5f08d0425"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:89df2aea2c43be1e1fa066df5f86c8ce822ab70a30e4c210968669565c0f4685"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:04e56e3fe8308a88b77e0afd20bec516f74aecf391cdd6e374f15cbed32783d6"}, - {file = "bcrypt-4.2.1-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:cfdf3d7530c790432046c40cda41dfee8c83e29482e6a604f8930b9930e94139"}, - {file = "bcrypt-4.2.1-cp37-abi3-win32.whl", hash = "sha256:adadd36274510a01f33e6dc08f5824b97c9580583bd4487c564fc4617b328005"}, - {file = "bcrypt-4.2.1-cp37-abi3-win_amd64.whl", hash = "sha256:8c458cd103e6c5d1d85cf600e546a639f234964d0228909d8f8dbeebff82d526"}, - {file = "bcrypt-4.2.1-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:8ad2f4528cbf0febe80e5a3a57d7a74e6635e41af1ea5675282a33d769fba413"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:909faa1027900f2252a9ca5dfebd25fc0ef1417943824783d1c8418dd7d6df4a"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cde78d385d5e93ece5479a0a87f73cd6fa26b171c786a884f955e165032b262c"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:533e7f3bcf2f07caee7ad98124fab7499cb3333ba2274f7a36cf1daee7409d99"}, - {file = "bcrypt-4.2.1-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:687cf30e6681eeda39548a93ce9bfbb300e48b4d445a43db4298d2474d2a1e54"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:041fa0155c9004eb98a232d54da05c0b41d4b8e66b6fc3cb71b4b3f6144ba837"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f85b1ffa09240c89aa2e1ae9f3b1c687104f7b2b9d2098da4e923f1b7082d331"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c6f5fa3775966cca251848d4d5393ab016b3afed251163c1436fefdec3b02c84"}, - {file = "bcrypt-4.2.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:807261df60a8b1ccd13e6599c779014a362ae4e795f5c59747f60208daddd96d"}, - {file = "bcrypt-4.2.1-cp39-abi3-win32.whl", hash = "sha256:b588af02b89d9fad33e5f98f7838bf590d6d692df7153647724a7f20c186f6bf"}, - {file = "bcrypt-4.2.1-cp39-abi3-win_amd64.whl", hash = "sha256:e84e0e6f8e40a242b11bce56c313edc2be121cec3e0ec2d76fce01f6af33c07c"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:76132c176a6d9953cdc83c296aeaed65e1a708485fd55abf163e0d9f8f16ce0e"}, - {file = "bcrypt-4.2.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:e158009a54c4c8bc91d5e0da80920d048f918c61a581f0a63e4e93bb556d362f"}, - {file = "bcrypt-4.2.1.tar.gz", hash = "sha256:6765386e3ab87f569b276988742039baab087b2cdb01e809d74e74503c2faafe"}, + {file = "backrefs-5.9-py310-none-any.whl", hash = "sha256:db8e8ba0e9de81fcd635f440deab5ae5f2591b54ac1ebe0550a2ca063488cd9f"}, + {file = "backrefs-5.9-py311-none-any.whl", hash = "sha256:6907635edebbe9b2dc3de3a2befff44d74f30a4562adbb8b36f21252ea19c5cf"}, + {file = "backrefs-5.9-py312-none-any.whl", hash = "sha256:7fdf9771f63e6028d7fee7e0c497c81abda597ea45d6b8f89e8ad76994f5befa"}, + {file = "backrefs-5.9-py313-none-any.whl", hash = "sha256:cc37b19fa219e93ff825ed1fed8879e47b4d89aa7a1884860e2db64ccd7c676b"}, + {file = "backrefs-5.9-py314-none-any.whl", hash = "sha256:df5e169836cc8acb5e440ebae9aad4bf9d15e226d3bad049cf3f6a5c20cc8dc9"}, + {file = "backrefs-5.9-py39-none-any.whl", hash = "sha256:f48ee18f6252b8f5777a22a00a09a85de0ca931658f1dd96d4406a34f3748c60"}, + {file = "backrefs-5.9.tar.gz", hash = "sha256:808548cb708d66b82ee231f962cb36faaf4f2baab032f2fbb783e9c2fdddaa59"}, ] [package.extras] -tests = ["pytest (>=3.2.1,!=3.3.0)"] -typecheck = ["mypy"] +extras = ["regex"] [[package]] -name = "black" -version = "24.8.0" -description = "The uncompromising code formatter." +name = "bcrypt" +version = "4.3.0" +description = "Modern password hashing for your software and your servers" optional = false python-versions = ">=3.8" -files = [ - {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, - {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, - {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, - {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, - {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, - {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, - {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, - {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, - {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, - {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, - {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, - {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, - {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, - {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, - {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, - {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, - {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, - {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, - {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, - {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, - {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, - {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +groups = ["main"] +files = [ + {file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:59e1aa0e2cd871b08ca146ed08445038f42ff75968c7ae50d2fdd7860ade2180"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:0042b2e342e9ae3d2ed22727c1262f76cc4f345683b5c1715f0250cf4277294f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74a8d21a09f5e025a9a23e7c0fd2c7fe8e7503e4d356c0a2c1486ba010619f09"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:0142b2cb84a009f8452c8c5a33ace5e3dfec4159e7735f5afe9a4d50a8ea722d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_aarch64.whl", hash = "sha256:12fa6ce40cde3f0b899729dbd7d5e8811cb892d31b6f7d0334a1f37748b789fd"}, + {file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_34_x86_64.whl", hash = "sha256:5bd3cca1f2aa5dbcf39e2aa13dd094ea181f48959e1071265de49cc2b82525af"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:335a420cfd63fc5bc27308e929bee231c15c85cc4c496610ffb17923abf7f231"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:0e30e5e67aed0187a1764911af023043b4542e70a7461ad20e837e94d23e1d6c"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:3b8d62290ebefd49ee0b3ce7500f5dbdcf13b81402c05f6dafab9a1e1b27212f"}, + {file = "bcrypt-4.3.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2ef6630e0ec01376f59a006dc72918b1bf436c3b571b80fa1968d775fa02fe7d"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win32.whl", hash = "sha256:7a4be4cbf241afee43f1c3969b9103a41b40bcb3a3f467ab19f891d9bc4642e4"}, + {file = "bcrypt-4.3.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5c1949bf259a388863ced887c7861da1df681cb2388645766c89fdfd9004c669"}, + {file = "bcrypt-4.3.0-cp38-abi3-macosx_10_12_universal2.whl", hash = "sha256:f81b0ed2639568bf14749112298f9e4e2b28853dab50a8b357e31798686a036d"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:864f8f19adbe13b7de11ba15d85d4a428c7e2f344bac110f667676a0ff84924b"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3e36506d001e93bffe59754397572f21bb5dc7c83f54454c990c74a468cd589e"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:842d08d75d9fe9fb94b18b071090220697f9f184d4547179b60734846461ed59"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7c03296b85cb87db865d91da79bf63d5609284fc0cab9472fdd8367bbd830753"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:62f26585e8b219cdc909b6a0069efc5e4267e25d4a3770a364ac58024f62a761"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:beeefe437218a65322fbd0069eb437e7c98137e08f22c4660ac2dc795c31f8bb"}, + {file = "bcrypt-4.3.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:97eea7408db3a5bcce4a55d13245ab3fa566e23b4c67cd227062bb49e26c585d"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:191354ebfe305e84f344c5964c7cd5f924a3bfc5d405c75ad07f232b6dffb49f"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:41261d64150858eeb5ff43c753c4b216991e0ae16614a308a15d909503617732"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:33752b1ba962ee793fa2b6321404bf20011fe45b9afd2a842139de3011898fef"}, + {file = "bcrypt-4.3.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:50e6e80a4bfd23a25f5c05b90167c19030cf9f87930f7cb2eacb99f45d1c3304"}, + {file = "bcrypt-4.3.0-cp38-abi3-win32.whl", hash = "sha256:67a561c4d9fb9465ec866177e7aebcad08fe23aaf6fbd692a6fab69088abfc51"}, + {file = "bcrypt-4.3.0-cp38-abi3-win_amd64.whl", hash = "sha256:584027857bc2843772114717a7490a37f68da563b3620f78a849bcb54dc11e62"}, + {file = "bcrypt-4.3.0-cp39-abi3-macosx_10_12_universal2.whl", hash = "sha256:0d3efb1157edebfd9128e4e46e2ac1a64e0c1fe46fb023158a407c7892b0f8c3"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:08bacc884fd302b611226c01014eca277d48f0a05187666bca23aac0dad6fe24"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6746e6fec103fcd509b96bacdfdaa2fbde9a553245dbada284435173a6f1aef"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:afe327968aaf13fc143a56a3360cb27d4ad0345e34da12c7290f1b00b8fe9a8b"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d9af79d322e735b1fc33404b5765108ae0ff232d4b54666d46730f8ac1a43676"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f1e3ffa1365e8702dc48c8b360fef8d7afeca482809c5e45e653af82ccd088c1"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:3004df1b323d10021fda07a813fd33e0fd57bef0e9a480bb143877f6cba996fe"}, + {file = "bcrypt-4.3.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:531457e5c839d8caea9b589a1bcfe3756b0547d7814e9ce3d437f17da75c32b0"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:17a854d9a7a476a89dcef6c8bd119ad23e0f82557afbd2c442777a16408e614f"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:6fb1fd3ab08c0cbc6826a2e0447610c6f09e983a281b919ed721ad32236b8b23"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:e965a9c1e9a393b8005031ff52583cedc15b7884fce7deb8b0346388837d6cfe"}, + {file = "bcrypt-4.3.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:79e70b8342a33b52b55d93b3a59223a844962bef479f6a0ea318ebbcadf71505"}, + {file = "bcrypt-4.3.0-cp39-abi3-win32.whl", hash = "sha256:b4d4e57f0a63fd0b358eb765063ff661328f69a04494427265950c71b992a39a"}, + {file = "bcrypt-4.3.0-cp39-abi3-win_amd64.whl", hash = "sha256:e53e074b120f2877a35cc6c736b8eb161377caae8925c17688bd46ba56daaa5b"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:c950d682f0952bafcceaf709761da0a32a942272fad381081b51096ffa46cea1"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:107d53b5c67e0bbc3f03ebf5b030e0403d24dda980f8e244795335ba7b4a027d"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:b693dbb82b3c27a1604a3dff5bfc5418a7e6a781bb795288141e5f80cf3a3492"}, + {file = "bcrypt-4.3.0-pp310-pypy310_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:b6354d3760fcd31994a14c89659dee887f1351a06e5dac3c1142307172a79f90"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:a839320bf27d474e52ef8cb16449bb2ce0ba03ca9f44daba6d93fa1d8828e48a"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:bdc6a24e754a555d7316fa4774e64c6c3997d27ed2d1964d55920c7c227bc4ce"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:55a935b8e9a1d2def0626c4269db3fcd26728cbff1e84f0341465c31c4ee56d8"}, + {file = "bcrypt-4.3.0-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:57967b7a28d855313a963aaea51bf6df89f833db4320da458e5b3c5ab6d4c938"}, + {file = "bcrypt-4.3.0.tar.gz", hash = "sha256:3a3fd2204178b6d2adcf09cb4f6426ffef54762577a7c9b54c159008cb288c18"}, ] -[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.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] -jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] -uvloop = ["uvloop (>=0.15.2)"] +tests = ["pytest (>=3.2.1,!=3.3.0)"] +typecheck = ["mypy"] [[package]] name = "certifi" -version = "2024.12.14" +version = "2025.7.14" description = "Python package for providing Mozilla's CA Bundle." optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +groups = ["main", "dev", "docs"] files = [ - {file = "certifi-2024.12.14-py3-none-any.whl", hash = "sha256:1275f7a45be9464efc1173084eaa30f866fe2e47d389406136d332ed4967ec56"}, - {file = "certifi-2024.12.14.tar.gz", hash = "sha256:b650d30f370c2b724812bee08008be0c4163b163ddaec3f2546c1caf65f191db"}, + {file = "certifi-2025.7.14-py3-none-any.whl", hash = "sha256:6b31f564a415d79ee77df69d757bb49a5bb53bd9f756cbbe24394ffd6fc1f4b2"}, + {file = "certifi-2025.7.14.tar.gz", hash = "sha256:8ea99dbdfaaf2ba2f9bac77b9249ef62ec5218e7c2b2e903378ed5fccf765995"}, ] [[package]] @@ -154,6 +153,7 @@ version = "1.17.1" description = "Foreign Function Interface for Python calling C code." optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14"}, {file = "cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67"}, @@ -229,103 +229,104 @@ pycparser = "*" [[package]] name = "charset-normalizer" -version = "3.4.1" +version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" -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"}, +groups = ["main", "dev", "docs"] +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, ] [[package]] @@ -334,6 +335,7 @@ version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" +groups = ["dev", "docs"] files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, @@ -348,17 +350,124 @@ 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 = ["dev", "docs"] files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.13.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.10" +groups = ["dev"] +files = [ + {file = "coverage-7.13.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1fa280b3ad78eea5be86f94f461c04943d942697e0dac889fa18fff8f5f9147"}, + {file = "coverage-7.13.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c3d8c679607220979434f494b139dfb00131ebf70bb406553d69c1ff01a5c33d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:339dc63b3eba969067b00f41f15ad161bf2946613156fb131266d8debc8e44d0"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:db622b999ffe49cb891f2fff3b340cdc2f9797d01a0a202a0973ba2562501d90"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1443ba9acbb593fa7c1c29e011d7c9761545fe35e7652e85ce7f51a16f7e08d"}, + {file = "coverage-7.13.1-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c832ec92c4499ac463186af72f9ed4d8daec15499b16f0a879b0d1c8e5cf4a3b"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:562ec27dfa3f311e0db1ba243ec6e5f6ab96b1edfcfc6cf86f28038bc4961ce6"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:4de84e71173d4dada2897e5a0e1b7877e5eefbfe0d6a44edee6ce31d9b8ec09e"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:a5a68357f686f8c4d527a2dc04f52e669c2fc1cbde38f6f7eb6a0e58cbd17cae"}, + {file = "coverage-7.13.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:77cc258aeb29a3417062758975521eae60af6f79e930d6993555eeac6a8eac29"}, + {file = "coverage-7.13.1-cp310-cp310-win32.whl", hash = "sha256:bb4f8c3c9a9f34423dba193f241f617b08ffc63e27f67159f60ae6baf2dcfe0f"}, + {file = "coverage-7.13.1-cp310-cp310-win_amd64.whl", hash = "sha256:c8e2706ceb622bc63bac98ebb10ef5da80ed70fbd8a7999a5076de3afaef0fb1"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1a55d509a1dc5a5b708b5dad3b5334e07a16ad4c2185e27b40e4dba796ab7f88"}, + {file = "coverage-7.13.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4d010d080c4888371033baab27e47c9df7d6fb28d0b7b7adf85a4a49be9298b3"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d938b4a840fb1523b9dfbbb454f652967f18e197569c32266d4d13f37244c3d9"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bf100a3288f9bb7f919b87eb84f87101e197535b9bd0e2c2b5b3179633324fee"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef6688db9bf91ba111ae734ba6ef1a063304a881749726e0d3575f5c10a9facf"}, + {file = "coverage-7.13.1-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0b609fc9cdbd1f02e51f67f51e5aee60a841ef58a68d00d5ee2c0faf357481a3"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c43257717611ff5e9a1d79dce8e47566235ebda63328718d9b65dd640bc832ef"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e09fbecc007f7b6afdfb3b07ce5bd9f8494b6856dd4f577d26c66c391b829851"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:a03a4f3a19a189919c7055098790285cc5c5b0b3976f8d227aea39dbf9f8bfdb"}, + {file = "coverage-7.13.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3820778ea1387c2b6a818caec01c63adc5b3750211af6447e8dcfb9b6f08dbba"}, + {file = "coverage-7.13.1-cp311-cp311-win32.whl", hash = "sha256:ff10896fa55167371960c5908150b434b71c876dfab97b69478f22c8b445ea19"}, + {file = "coverage-7.13.1-cp311-cp311-win_amd64.whl", hash = "sha256:a998cc0aeeea4c6d5622a3754da5a493055d2d95186bad877b0a34ea6e6dbe0a"}, + {file = "coverage-7.13.1-cp311-cp311-win_arm64.whl", hash = "sha256:fea07c1a39a22614acb762e3fbbb4011f65eedafcb2948feeef641ac78b4ee5c"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f34591000f06e62085b1865c9bc5f7858df748834662a51edadfd2c3bfe0dd3"}, + {file = "coverage-7.13.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b67e47c5595b9224599016e333f5ec25392597a89d5744658f837d204e16c63e"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3e7b8bd70c48ffb28461ebe092c2345536fb18bbbf19d287c8913699735f505c"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:c223d078112e90dc0e5c4e35b98b9584164bea9fbbd221c0b21c5241f6d51b62"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:794f7c05af0763b1bbd1b9e6eff0e52ad068be3b12cd96c87de037b01390c968"}, + {file = "coverage-7.13.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0642eae483cc8c2902e4af7298bf886d605e80f26382124cddc3967c2a3df09e"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f5e772ed5fef25b3de9f2008fe67b92d46831bd2bc5bdc5dd6bfd06b83b316f"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:45980ea19277dc0a579e432aef6a504fe098ef3a9032ead15e446eb0f1191aee"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:e4f18eca6028ffa62adbd185a8f1e1dd242f2e68164dba5c2b74a5204850b4cf"}, + {file = "coverage-7.13.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f8dca5590fec7a89ed6826fce625595279e586ead52e9e958d3237821fbc750c"}, + {file = "coverage-7.13.1-cp312-cp312-win32.whl", hash = "sha256:ff86d4e85188bba72cfb876df3e11fa243439882c55957184af44a35bd5880b7"}, + {file = "coverage-7.13.1-cp312-cp312-win_amd64.whl", hash = "sha256:16cc1da46c04fb0fb128b4dc430b78fa2aba8a6c0c9f8eb391fd5103409a6ac6"}, + {file = "coverage-7.13.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d9bc218650022a768f3775dd7fdac1886437325d8d295d923ebcfef4892ad5c"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:cb237bfd0ef4d5eb6a19e29f9e528ac67ac3be932ea6b44fb6cc09b9f3ecff78"}, + {file = "coverage-7.13.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1dcb645d7e34dcbcc96cd7c132b1fc55c39263ca62eb961c064eb3928997363b"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3d42df8201e00384736f0df9be2ced39324c3907607d17d50d50116c989d84cd"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fa3edde1aa8807de1d05934982416cb3ec46d1d4d91e280bcce7cca01c507992"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9edd0e01a343766add6817bc448408858ba6b489039eaaa2018474e4001651a4"}, + {file = "coverage-7.13.1-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:985b7836931d033570b94c94713c6dba5f9d3ff26045f72c3e5dbc5fe3361e5a"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ffed1e4980889765c84a5d1a566159e363b71d6b6fbaf0bebc9d3c30bc016766"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:8842af7f175078456b8b17f1b73a0d16a65dcbdc653ecefeb00a56b3c8c298c4"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:ccd7a6fca48ca9c131d9b0a2972a581e28b13416fc313fb98b6d24a03ce9a398"}, + {file = "coverage-7.13.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0403f647055de2609be776965108447deb8e384fe4a553c119e3ff6bfbab4784"}, + {file = "coverage-7.13.1-cp313-cp313-win32.whl", hash = "sha256:549d195116a1ba1e1ae2f5ca143f9777800f6636eab917d4f02b5310d6d73461"}, + {file = "coverage-7.13.1-cp313-cp313-win_amd64.whl", hash = "sha256:5899d28b5276f536fcf840b18b61a9fce23cc3aec1d114c44c07fe94ebeaa500"}, + {file = "coverage-7.13.1-cp313-cp313-win_arm64.whl", hash = "sha256:868a2fae76dfb06e87291bcbd4dcbcc778a8500510b618d50496e520bd94d9b9"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:67170979de0dacac3f3097d02b0ad188d8edcea44ccc44aaa0550af49150c7dc"}, + {file = "coverage-7.13.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f80e2bb21bfab56ed7405c2d79d34b5dc0bc96c2c1d2a067b643a09fb756c43a"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:f83351e0f7dcdb14d7326c3d8d8c4e915fa685cbfdc6281f9470d97a04e9dfe4"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:bb3f6562e89bad0110afbe64e485aac2462efdce6232cdec7862a095dc3412f6"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:77545b5dcda13b70f872c3b5974ac64c21d05e65b1590b441c8560115dc3a0d1"}, + {file = "coverage-7.13.1-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a4d240d260a1aed814790bbe1f10a5ff31ce6c21bc78f0da4a1e8268d6c80dbd"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d2287ac9360dec3837bfdad969963a5d073a09a85d898bd86bea82aa8876ef3c"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:0d2c11f3ea4db66b5cbded23b20185c35066892c67d80ec4be4bab257b9ad1e0"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:3fc6a169517ca0d7ca6846c3c5392ef2b9e38896f61d615cb75b9e7134d4ee1e"}, + {file = "coverage-7.13.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d10a2ed46386e850bb3de503a54f9fe8192e5917fcbb143bfef653a9355e9a53"}, + {file = "coverage-7.13.1-cp313-cp313t-win32.whl", hash = "sha256:75a6f4aa904301dab8022397a22c0039edc1f51e90b83dbd4464b8a38dc87842"}, + {file = "coverage-7.13.1-cp313-cp313t-win_amd64.whl", hash = "sha256:309ef5706e95e62578cda256b97f5e097916a2c26247c287bbe74794e7150df2"}, + {file = "coverage-7.13.1-cp313-cp313t-win_arm64.whl", hash = "sha256:92f980729e79b5d16d221038dbf2e8f9a9136afa072f9d5d6ed4cb984b126a09"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:97ab3647280d458a1f9adb85244e81587505a43c0c7cff851f5116cd2814b894"}, + {file = "coverage-7.13.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8f572d989142e0908e6acf57ad1b9b86989ff057c006d13b76c146ec6a20216a"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:d72140ccf8a147e94274024ff6fd8fb7811354cf7ef88b1f0a988ebaa5bc774f"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d3c9f051b028810f5a87c88e5d6e9af3c0ff32ef62763bf15d29f740453ca909"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f398ba4df52d30b1763f62eed9de5620dcde96e6f491f4c62686736b155aa6e4"}, + {file = "coverage-7.13.1-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:132718176cc723026d201e347f800cd1a9e4b62ccd3f82476950834dad501c75"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e549d642426e3579b3f4b92d0431543b012dcb6e825c91619d4e93b7363c3f9"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:90480b2134999301eea795b3a9dbf606c6fbab1b489150c501da84a959442465"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e825dbb7f84dfa24663dd75835e7257f8882629fc11f03ecf77d84a75134b864"}, + {file = "coverage-7.13.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:623dcc6d7a7ba450bbdbeedbaa0c42b329bdae16491af2282f12a7e809be7eb9"}, + {file = "coverage-7.13.1-cp314-cp314-win32.whl", hash = "sha256:6e73ebb44dca5f708dc871fe0b90cf4cff1a13f9956f747cc87b535a840386f5"}, + {file = "coverage-7.13.1-cp314-cp314-win_amd64.whl", hash = "sha256:be753b225d159feb397bd0bf91ae86f689bad0da09d3b301478cd39b878ab31a"}, + {file = "coverage-7.13.1-cp314-cp314-win_arm64.whl", hash = "sha256:228b90f613b25ba0019361e4ab81520b343b622fc657daf7e501c4ed6a2366c0"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:60cfb538fe9ef86e5b2ab0ca8fc8d62524777f6c611dcaf76dc16fbe9b8e698a"}, + {file = "coverage-7.13.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:57dfc8048c72ba48a8c45e188d811e5efd7e49b387effc8fb17e97936dde5bf6"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3f2f725aa3e909b3c5fdb8192490bdd8e1495e85906af74fe6e34a2a77ba0673"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9ee68b21909686eeb21dfcba2c3b81fee70dcf38b140dcd5aa70680995fa3aa5"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:724b1b270cb13ea2e6503476e34541a0b1f62280bc997eab443f87790202033d"}, + {file = "coverage-7.13.1-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:916abf1ac5cf7eb16bc540a5bf75c71c43a676f5c52fcb9fe75a2bd75fb944e8"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:776483fd35b58d8afe3acbd9988d5de592ab6da2d2a865edfdbc9fdb43e7c486"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:b6f3b96617e9852703f5b633ea01315ca45c77e879584f283c44127f0f1ec564"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:bd63e7b74661fed317212fab774e2a648bc4bb09b35f25474f8e3325d2945cd7"}, + {file = "coverage-7.13.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:933082f161bbb3e9f90d00990dc956120f608cdbcaeea15c4d897f56ef4fe416"}, + {file = "coverage-7.13.1-cp314-cp314t-win32.whl", hash = "sha256:18be793c4c87de2965e1c0f060f03d9e5aff66cfeae8e1dbe6e5b88056ec153f"}, + {file = "coverage-7.13.1-cp314-cp314t-win_amd64.whl", hash = "sha256:0e42e0ec0cd3e0d851cb3c91f770c9301f48647cb2877cb78f74bdaa07639a79"}, + {file = "coverage-7.13.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eaecf47ef10c72ece9a2a92118257da87e460e113b83cc0d2905cbbe931792b4"}, + {file = "coverage-7.13.1-py3-none-any.whl", hash = "sha256:2016745cb3ba554469d02819d78958b571792bb68e31302610e898f80dd3a573"}, + {file = "coverage-7.13.1.tar.gz", hash = "sha256:b7593fe7eb5feaa3fbb461ac79aac9f9fc0387a5ca8080b0c6fe2ca27b091afd"}, +] + +[package.extras] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] + [[package]] name = "cryptography" version = "43.0.3" description = "cryptography is a package which provides cryptographic recipes and primitives to Python developers." optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "cryptography-43.0.3-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bf7a1932ac4176486eab36a19ed4c0492da5d97123f1406cf15e41b05e787d2e"}, {file = "cryptography-43.0.3-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63efa177ff54aec6e1c0aefaa1a241232dcd37413835a9b674b6e3f0ae2bfd3e"}, @@ -404,13 +513,14 @@ test-randomorder = ["pytest-randomly"] [[package]] name = "dill" -version = "0.3.9" +version = "0.4.0" description = "serialize all of Python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a"}, - {file = "dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c"}, + {file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"}, + {file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"}, ] [package.extras] @@ -419,15 +529,20 @@ profile = ["gprof2dot (>=2022.7.29)"] [[package]] name = "exceptiongroup" -version = "1.2.2" +version = "1.3.0" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ - {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, - {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, + {file = "exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10"}, + {file = "exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88"}, ] +[package.dependencies] +typing-extensions = {version = ">=4.6.0", markers = "python_version < \"3.13\""} + [package.extras] test = ["pytest (>=6)"] @@ -437,6 +552,7 @@ version = "1.3.13" description = "F5 BIG-IP iControl REST API client" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "f5-icontrol-rest-1.3.13.tar.gz", hash = "sha256:49fffd999fb4971d6754beb0e066051175db9d9baeb8a76fca6c801dacc89359"}, ] @@ -450,6 +566,7 @@ version = "3.0.21" description = "F5 Networks Python SDK" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "f5-sdk-3.0.21.tar.gz", hash = "sha256:22890c8fd98232c14c54560ee02a59501da2df670e6a6852e6eda6364363963a"}, ] @@ -458,28 +575,13 @@ files = [ f5-icontrol-rest = ">=1.3.13,<2.0.0" six = ">=1.9.0,<2.0.0" -[[package]] -name = "flake8" -version = "3.9.2" -description = "the modular source code checker: pep8 pyflakes and co" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" -files = [ - {file = "flake8-3.9.2-py2.py3-none-any.whl", hash = "sha256:bf8fd333346d844f616e8d47905ef3a3384edae6b4e9beb0c5101e25e3110907"}, - {file = "flake8-3.9.2.tar.gz", hash = "sha256:07528381786f2a6237b061f6e96610a4167b226cb926e2aa2b6b1d78057c576b"}, -] - -[package.dependencies] -mccabe = ">=0.6.0,<0.7.0" -pycodestyle = ">=2.7.0,<2.8.0" -pyflakes = ">=2.3.0,<2.4.0" - [[package]] name = "future" version = "1.0.0" description = "Clean single-source support for Python 3 and 2" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["main"] files = [ {file = "future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216"}, {file = "future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05"}, @@ -491,6 +593,7 @@ version = "2.1.0" description = "Copy your docs directly to the gh-pages branch." optional = false python-versions = "*" +groups = ["docs"] files = [ {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, @@ -504,25 +607,38 @@ dev = ["flake8", "markdown", "twine", "wheel"] [[package]] name = "griffe" -version = "1.4.0" +version = "1.1.1" description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] files = [ - {file = "griffe-1.4.0-py3-none-any.whl", hash = "sha256:e589de8b8c137e99a46ec45f9598fc0ac5b6868ce824b24db09c02d117b89bc5"}, - {file = "griffe-1.4.0.tar.gz", hash = "sha256:8fccc585896d13f1221035d32c50dec65830c87d23f9adb9b1e6f3d63574f7f5"}, + {file = "griffe-1.1.1-py3-none-any.whl", hash = "sha256:0c469411e8d671a545725f5c0851a746da8bd99d354a79fdc4abd45219252efb"}, + {file = "griffe-1.1.1.tar.gz", hash = "sha256:faeb78764c0b2bd010719d6e015d07709b0f260258b5d4dd6c88343d9702aa30"}, ] [package.dependencies] -astunparse = {version = ">=1.6", markers = "python_version < \"3.9\""} colorama = ">=0.4" +[[package]] +name = "hjson" +version = "3.1.0" +description = "Hjson, a user interface for JSON." +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "hjson-3.1.0-py3-none-any.whl", hash = "sha256:65713cdcf13214fb554eb8b4ef803419733f4f5e551047c9b711098ab7186b89"}, + {file = "hjson-3.1.0.tar.gz", hash = "sha256:55af475a27cf83a7969c808399d7bccdec8fb836a07ddbd574587593b9cdcf75"}, +] + [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" +groups = ["main", "dev", "docs"] files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, @@ -531,38 +647,16 @@ files = [ [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] -[[package]] -name = "importlib-metadata" -version = "8.5.0" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.8" -files = [ - {file = "importlib_metadata-8.5.0-py3-none-any.whl", hash = "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b"}, - {file = "importlib_metadata-8.5.0.tar.gz", hash = "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -perf = ["ipython"] -test = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["pytest-mypy"] - [[package]] name = "iniconfig" -version = "2.0.0" +version = "2.1.0" description = "brain-dead simple config-ini parsing" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, - {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, + {file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"}, + {file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"}, ] [[package]] @@ -571,6 +665,7 @@ version = "2.2.0" description = "Pythonic task execution" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ {file = "invoke-2.2.0-py3-none-any.whl", hash = "sha256:6ea924cc53d4f78e3d98bc436b08069a03077e6f85ad1ddaa8a116d7dad15820"}, {file = "invoke-2.2.0.tar.gz", hash = "sha256:ee6cbb101af1a859c7fe84f2a264c059020b0cb7fe3535f9424300ab568f6bd5"}, @@ -578,27 +673,30 @@ files = [ [[package]] name = "isort" -version = "5.13.2" +version = "6.0.1" description = "A Python utility / library to sort Python imports." optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["dev"] files = [ - {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, - {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, + {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 (>=0.4.6)"] +colors = ["colorama"] +plugins = ["setuptools"] [[package]] name = "jinja2" -version = "3.1.5" +version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" +groups = ["main", "dev", "docs"] files = [ - {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, - {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, ] [package.dependencies] @@ -609,13 +707,14 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "junos-eznc" -version = "2.7.2" +version = "2.7.4" description = "Junos 'EZ' automation for non-programmers" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ - {file = "junos-eznc-2.7.2.tar.gz", hash = "sha256:7c7e6f8e9bb9d0d034ffaeb592e400dd114f03db44c3bb608c951e88483c825d"}, - {file = "junos_eznc-2.7.2-py2.py3-none-any.whl", hash = "sha256:72ace7fe635efbb0480126b54f8ce79e9fb519f2c4aa53233318f1b2cb32b4b0"}, + {file = "junos-eznc-2.7.4.tar.gz", hash = "sha256:c9d90f8ecea908b0b8bf3f47ccc4c7afc212dddcd275a426e5c0104f40418d59"}, + {file = "junos_eznc-2.7.4-py2.py3-none-any.whl", hash = "sha256:9067f4868c6a0f884288a3f1d23e0aea50c7292760a0f8455a1809658c763dc3"}, ] [package.dependencies] @@ -633,181 +732,154 @@ yamlordereddictloader = "*" [[package]] name = "lxml" -version = "5.3.0" +version = "6.0.0" description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." optional = false -python-versions = ">=3.6" -files = [ - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:dd36439be765e2dde7660212b5275641edbc813e7b24668831a5c8ac91180656"}, - {file = "lxml-5.3.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ae5fe5c4b525aa82b8076c1a59d642c17b6e8739ecf852522c6321852178119d"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:501d0d7e26b4d261fca8132854d845e4988097611ba2531408ec91cf3fd9d20a"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb66442c2546446944437df74379e9cf9e9db353e61301d1a0e26482f43f0dd8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9e41506fec7a7f9405b14aa2d5c8abbb4dbbd09d88f9496958b6d00cb4d45330"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f7d4a670107d75dfe5ad080bed6c341d18c4442f9378c9f58e5851e86eb79965"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:41ce1f1e2c7755abfc7e759dc34d7d05fd221723ff822947132dc934d122fe22"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:44264ecae91b30e5633013fb66f6ddd05c006d3e0e884f75ce0b4755b3e3847b"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:3c174dc350d3ec52deb77f2faf05c439331d6ed5e702fc247ccb4e6b62d884b7"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:2dfab5fa6a28a0b60a20638dc48e6343c02ea9933e3279ccb132f555a62323d8"}, - {file = "lxml-5.3.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:b1c8c20847b9f34e98080da785bb2336ea982e7f913eed5809e5a3c872900f32"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2c86bf781b12ba417f64f3422cfc302523ac9cd1d8ae8c0f92a1c66e56ef2e86"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:c162b216070f280fa7da844531169be0baf9ccb17263cf5a8bf876fcd3117fa5"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:36aef61a1678cb778097b4a6eeae96a69875d51d1e8f4d4b491ab3cfb54b5a03"}, - {file = "lxml-5.3.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f65e5120863c2b266dbcc927b306c5b78e502c71edf3295dfcb9501ec96e5fc7"}, - {file = "lxml-5.3.0-cp310-cp310-win32.whl", hash = "sha256:ef0c1fe22171dd7c7c27147f2e9c3e86f8bdf473fed75f16b0c2e84a5030ce80"}, - {file = "lxml-5.3.0-cp310-cp310-win_amd64.whl", hash = "sha256:052d99051e77a4f3e8482c65014cf6372e61b0a6f4fe9edb98503bb5364cfee3"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74bcb423462233bc5d6066e4e98b0264e7c1bed7541fff2f4e34fe6b21563c8b"}, - {file = "lxml-5.3.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a3d819eb6f9b8677f57f9664265d0a10dd6551d227afb4af2b9cd7bdc2ccbf18"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5b8f5db71b28b8c404956ddf79575ea77aa8b1538e8b2ef9ec877945b3f46442"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2c3406b63232fc7e9b8783ab0b765d7c59e7c59ff96759d8ef9632fca27c7ee4"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ecdd78ab768f844c7a1d4a03595038c166b609f6395e25af9b0f3f26ae1230f"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:168f2dfcfdedf611eb285efac1516c8454c8c99caf271dccda8943576b67552e"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa617107a410245b8660028a7483b68e7914304a6d4882b5ff3d2d3eb5948d8c"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:69959bd3167b993e6e710b99051265654133a98f20cec1d9b493b931942e9c16"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:bd96517ef76c8654446fc3db9242d019a1bb5fe8b751ba414765d59f99210b79"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ab6dd83b970dc97c2d10bc71aa925b84788c7c05de30241b9e96f9b6d9ea3080"}, - {file = "lxml-5.3.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:eec1bb8cdbba2925bedc887bc0609a80e599c75b12d87ae42ac23fd199445654"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6a7095eeec6f89111d03dabfe5883a1fd54da319c94e0fb104ee8f23616b572d"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:6f651ebd0b21ec65dfca93aa629610a0dbc13dbc13554f19b0113da2e61a4763"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:f422a209d2455c56849442ae42f25dbaaba1c6c3f501d58761c619c7836642ec"}, - {file = "lxml-5.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:62f7fdb0d1ed2065451f086519865b4c90aa19aed51081979ecd05a21eb4d1be"}, - {file = "lxml-5.3.0-cp311-cp311-win32.whl", hash = "sha256:c6379f35350b655fd817cd0d6cbeef7f265f3ae5fedb1caae2eb442bbeae9ab9"}, - {file = "lxml-5.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:9c52100e2c2dbb0649b90467935c4b0de5528833c76a35ea1a2691ec9f1ee7a1"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:e99f5507401436fdcc85036a2e7dc2e28d962550afe1cbfc07c40e454256a859"}, - {file = "lxml-5.3.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:384aacddf2e5813a36495233b64cb96b1949da72bef933918ba5c84e06af8f0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:874a216bf6afaf97c263b56371434e47e2c652d215788396f60477540298218f"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:65ab5685d56914b9a2a34d67dd5488b83213d680b0c5d10b47f81da5a16b0b0e"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aac0bbd3e8dd2d9c45ceb82249e8bdd3ac99131a32b4d35c8af3cc9db1657179"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b369d3db3c22ed14c75ccd5af429086f166a19627e84a8fdade3f8f31426e52a"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c24037349665434f375645fa9d1f5304800cec574d0310f618490c871fd902b3"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:62d172f358f33a26d6b41b28c170c63886742f5b6772a42b59b4f0fa10526cb1"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:c1f794c02903c2824fccce5b20c339a1a14b114e83b306ff11b597c5f71a1c8d"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:5d6a6972b93c426ace71e0be9a6f4b2cfae9b1baed2eed2006076a746692288c"}, - {file = "lxml-5.3.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:3879cc6ce938ff4eb4900d901ed63555c778731a96365e53fadb36437a131a99"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:74068c601baff6ff021c70f0935b0c7bc528baa8ea210c202e03757c68c5a4ff"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ecd4ad8453ac17bc7ba3868371bffb46f628161ad0eefbd0a855d2c8c32dd81a"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:7e2f58095acc211eb9d8b5771bf04df9ff37d6b87618d1cbf85f92399c98dae8"}, - {file = "lxml-5.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e63601ad5cd8f860aa99d109889b5ac34de571c7ee902d6812d5d9ddcc77fa7d"}, - {file = "lxml-5.3.0-cp312-cp312-win32.whl", hash = "sha256:17e8d968d04a37c50ad9c456a286b525d78c4a1c15dd53aa46c1d8e06bf6fa30"}, - {file = "lxml-5.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:c1a69e58a6bb2de65902051d57fde951febad631a20a64572677a1052690482f"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c72e9563347c7395910de6a3100a4840a75a6f60e05af5e58566868d5eb2d6a"}, - {file = "lxml-5.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e92ce66cd919d18d14b3856906a61d3f6b6a8500e0794142338da644260595cd"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1d04f064bebdfef9240478f7a779e8c5dc32b8b7b0b2fc6a62e39b928d428e51"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c2fb570d7823c2bbaf8b419ba6e5662137f8166e364a8b2b91051a1fb40ab8b"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c120f43553ec759f8de1fee2f4794452b0946773299d44c36bfe18e83caf002"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:562e7494778a69086f0312ec9689f6b6ac1c6b65670ed7d0267e49f57ffa08c4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:423b121f7e6fa514ba0c7918e56955a1d4470ed35faa03e3d9f0e3baa4c7e492"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:c00f323cc00576df6165cc9d21a4c21285fa6b9989c5c39830c3903dc4303ef3"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_ppc64le.whl", hash = "sha256:1fdc9fae8dd4c763e8a31e7630afef517eab9f5d5d31a278df087f307bf601f4"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_s390x.whl", hash = "sha256:658f2aa69d31e09699705949b5fc4719cbecbd4a97f9656a232e7d6c7be1a367"}, - {file = "lxml-5.3.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1473427aff3d66a3fa2199004c3e601e6c4500ab86696edffdbc84954c72d832"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a87de7dd873bf9a792bf1e58b1c3887b9264036629a5bf2d2e6579fe8e73edff"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:0d7b36afa46c97875303a94e8f3ad932bf78bace9e18e603f2085b652422edcd"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cf120cce539453ae086eacc0130a324e7026113510efa83ab42ef3fcfccac7fb"}, - {file = "lxml-5.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:df5c7333167b9674aa8ae1d4008fa4bc17a313cc490b2cca27838bbdcc6bb15b"}, - {file = "lxml-5.3.0-cp313-cp313-win32.whl", hash = "sha256:c802e1c2ed9f0c06a65bc4ed0189d000ada8049312cfeab6ca635e39c9608957"}, - {file = "lxml-5.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:406246b96d552e0503e17a1006fd27edac678b3fcc9f1be71a2f94b4ff61528d"}, - {file = "lxml-5.3.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:8f0de2d390af441fe8b2c12626d103540b5d850d585b18fcada58d972b74a74e"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1afe0a8c353746e610bd9031a630a95bcfb1a720684c3f2b36c4710a0a96528f"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56b9861a71575f5795bde89256e7467ece3d339c9b43141dbdd54544566b3b94"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_28_x86_64.whl", hash = "sha256:9fb81d2824dff4f2e297a276297e9031f46d2682cafc484f49de182aa5e5df99"}, - {file = "lxml-5.3.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:2c226a06ecb8cdef28845ae976da407917542c5e6e75dcac7cc33eb04aaeb237"}, - {file = "lxml-5.3.0-cp36-cp36m-musllinux_1_2_x86_64.whl", hash = "sha256:7d3d1ca42870cdb6d0d29939630dbe48fa511c203724820fc0fd507b2fb46577"}, - {file = "lxml-5.3.0-cp36-cp36m-win32.whl", hash = "sha256:094cb601ba9f55296774c2d57ad68730daa0b13dc260e1f941b4d13678239e70"}, - {file = "lxml-5.3.0-cp36-cp36m-win_amd64.whl", hash = "sha256:eafa2c8658f4e560b098fe9fc54539f86528651f61849b22111a9b107d18910c"}, - {file = "lxml-5.3.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:cb83f8a875b3d9b458cada4f880fa498646874ba4011dc974e071a0a84a1b033"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:25f1b69d41656b05885aa185f5fdf822cb01a586d1b32739633679699f220391"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23e0553b8055600b3bf4a00b255ec5c92e1e4aebf8c2c09334f8368e8bd174d6"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9ada35dd21dc6c039259596b358caab6b13f4db4d4a7f8665764d616daf9cc1d"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:81b4e48da4c69313192d8c8d4311e5d818b8be1afe68ee20f6385d0e96fc9512"}, - {file = "lxml-5.3.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:2bc9fd5ca4729af796f9f59cd8ff160fe06a474da40aca03fcc79655ddee1a8b"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:07da23d7ee08577760f0a71d67a861019103e4812c87e2fab26b039054594cc5"}, - {file = "lxml-5.3.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:ea2e2f6f801696ad7de8aec061044d6c8c0dd4037608c7cab38a9a4d316bfb11"}, - {file = "lxml-5.3.0-cp37-cp37m-win32.whl", hash = "sha256:5c54afdcbb0182d06836cc3d1be921e540be3ebdf8b8a51ee3ef987537455f84"}, - {file = "lxml-5.3.0-cp37-cp37m-win_amd64.whl", hash = "sha256:f2901429da1e645ce548bf9171784c0f74f0718c3f6150ce166be39e4dd66c3e"}, - {file = "lxml-5.3.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:c56a1d43b2f9ee4786e4658c7903f05da35b923fb53c11025712562d5cc02753"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ee8c39582d2652dcd516d1b879451500f8db3fe3607ce45d7c5957ab2596040"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fdf3a3059611f7585a78ee10399a15566356116a4288380921a4b598d807a22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:146173654d79eb1fc97498b4280c1d3e1e5d58c398fa530905c9ea50ea849b22"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_aarch64.whl", hash = "sha256:0a7056921edbdd7560746f4221dca89bb7a3fe457d3d74267995253f46343f15"}, - {file = "lxml-5.3.0-cp38-cp38-manylinux_2_28_x86_64.whl", hash = "sha256:9e4b47ac0f5e749cfc618efdf4726269441014ae1d5583e047b452a32e221920"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:f914c03e6a31deb632e2daa881fe198461f4d06e57ac3d0e05bbcab8eae01945"}, - {file = "lxml-5.3.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:213261f168c5e1d9b7535a67e68b1f59f92398dd17a56d934550837143f79c42"}, - {file = "lxml-5.3.0-cp38-cp38-win32.whl", hash = "sha256:218c1b2e17a710e363855594230f44060e2025b05c80d1f0661258142b2add2e"}, - {file = "lxml-5.3.0-cp38-cp38-win_amd64.whl", hash = "sha256:315f9542011b2c4e1d280e4a20ddcca1761993dda3afc7a73b01235f8641e903"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:1ffc23010330c2ab67fac02781df60998ca8fe759e8efde6f8b756a20599c5de"}, - {file = "lxml-5.3.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2b3778cb38212f52fac9fe913017deea2fdf4eb1a4f8e4cfc6b009a13a6d3fcc"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b0c7a688944891086ba192e21c5229dea54382f4836a209ff8d0a660fac06be"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:747a3d3e98e24597981ca0be0fd922aebd471fa99d0043a3842d00cdcad7ad6a"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86a6b24b19eaebc448dc56b87c4865527855145d851f9fc3891673ff97950540"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b11a5d918a6216e521c715b02749240fb07ae5a1fefd4b7bf12f833bc8b4fe70"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68b87753c784d6acb8a25b05cb526c3406913c9d988d51f80adecc2b0775d6aa"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_aarch64.whl", hash = "sha256:109fa6fede314cc50eed29e6e56c540075e63d922455346f11e4d7a036d2b8cf"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_ppc64le.whl", hash = "sha256:02ced472497b8362c8e902ade23e3300479f4f43e45f4105c85ef43b8db85229"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_s390x.whl", hash = "sha256:6b038cc86b285e4f9fea2ba5ee76e89f21ed1ea898e287dc277a25884f3a7dfe"}, - {file = "lxml-5.3.0-cp39-cp39-manylinux_2_28_x86_64.whl", hash = "sha256:7437237c6a66b7ca341e868cda48be24b8701862757426852c9b3186de1da8a2"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7f41026c1d64043a36fda21d64c5026762d53a77043e73e94b71f0521939cc71"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:482c2f67761868f0108b1743098640fbb2a28a8e15bf3f47ada9fa59d9fe08c3"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:1483fd3358963cc5c1c9b122c80606a3a79ee0875bcac0204149fa09d6ff2727"}, - {file = "lxml-5.3.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:2dec2d1130a9cda5b904696cec33b2cfb451304ba9081eeda7f90f724097300a"}, - {file = "lxml-5.3.0-cp39-cp39-win32.whl", hash = "sha256:a0eabd0a81625049c5df745209dc7fcef6e2aea7793e5f003ba363610aa0a3ff"}, - {file = "lxml-5.3.0-cp39-cp39-win_amd64.whl", hash = "sha256:89e043f1d9d341c52bf2af6d02e6adde62e0a46e6755d5eb60dc6e4f0b8aeca2"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7b1cd427cb0d5f7393c31b7496419da594fe600e6fdc4b105a54f82405e6626c"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:51806cfe0279e06ed8500ce19479d757db42a30fd509940b1701be9c86a5ff9a"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee70d08fd60c9565ba8190f41a46a54096afa0eeb8f76bd66f2c25d3b1b83005"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:8dc2c0395bea8254d8daebc76dcf8eb3a95ec2a46fa6fae5eaccee366bfe02ce"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:6ba0d3dcac281aad8a0e5b14c7ed6f9fa89c8612b47939fc94f80b16e2e9bc83"}, - {file = "lxml-5.3.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:6e91cf736959057f7aac7adfc83481e03615a8e8dd5758aa1d95ea69e8931dba"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:94d6c3782907b5e40e21cadf94b13b0842ac421192f26b84c45f13f3c9d5dc27"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c300306673aa0f3ed5ed9372b21867690a17dba38c68c44b287437c362ce486b"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78d9b952e07aed35fe2e1a7ad26e929595412db48535921c5013edc8aa4a35ce"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:01220dca0d066d1349bd6a1726856a78f7929f3878f7e2ee83c296c69495309e"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2d9b8d9177afaef80c53c0a9e30fa252ff3036fb1c6494d427c066a4ce6a282f"}, - {file = "lxml-5.3.0-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:20094fc3f21ea0a8669dc4c61ed7fa8263bd37d97d93b90f28fc613371e7a875"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:ace2c2326a319a0bb8a8b0e5b570c764962e95818de9f259ce814ee666603f19"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:92e67a0be1639c251d21e35fe74df6bcc40cba445c2cda7c4a967656733249e2"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd5350b55f9fecddc51385463a4f67a5da829bc741e38cf689f38ec9023f54ab"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:4c1fefd7e3d00921c44dc9ca80a775af49698bbfd92ea84498e56acffd4c5469"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:71a8dd38fbd2f2319136d4ae855a7078c69c9a38ae06e0c17c73fd70fc6caad8"}, - {file = "lxml-5.3.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:97acf1e1fd66ab53dacd2c35b319d7e548380c2e9e8c54525c6e76d21b1ae3b1"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:68934b242c51eb02907c5b81d138cb977b2129a0a75a8f8b60b01cb8586c7b21"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b710bc2b8292966b23a6a0121f7a6c51d45d2347edcc75f016ac123b8054d3f2"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:18feb4b93302091b1541221196a2155aa296c363fd233814fa11e181adebc52f"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:3eb44520c4724c2e1a57c0af33a379eee41792595023f367ba3952a2d96c2aab"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:609251a0ca4770e5a8768ff902aa02bf636339c5a93f9349b48eb1f606f7f3e9"}, - {file = "lxml-5.3.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:516f491c834eb320d6c843156440fe7fc0d50b33e44387fcec5b02f0bc118a4c"}, - {file = "lxml-5.3.0.tar.gz", hash = "sha256:4e109ca30d1edec1ac60cdbe341905dc3b8f55b16855e03a54aaf59e51ec8c6f"}, +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:35bc626eec405f745199200ccb5c6b36f202675d204aa29bb52e27ba2b71dea8"}, + {file = "lxml-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:246b40f8a4aec341cbbf52617cad8ab7c888d944bfe12a6abd2b1f6cfb6f6082"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2793a627e95d119e9f1e19720730472f5543a6d84c50ea33313ce328d870f2dd"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:46b9ed911f36bfeb6338e0b482e7fe7c27d362c52fde29f221fddbc9ee2227e7"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b4790b558bee331a933e08883c423f65bbcd07e278f91b2272489e31ab1e2b4"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2030956cf4886b10be9a0285c6802e078ec2391e1dd7ff3eb509c2c95a69b76"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d23854ecf381ab1facc8f353dcd9adeddef3652268ee75297c1164c987c11dc"}, + {file = "lxml-6.0.0-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:43fe5af2d590bf4691531b1d9a2495d7aab2090547eaacd224a3afec95706d76"}, + {file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74e748012f8c19b47f7d6321ac929a9a94ee92ef12bc4298c47e8b7219b26541"}, + {file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:43cfbb7db02b30ad3926e8fceaef260ba2fb7df787e38fa2df890c1ca7966c3b"}, + {file = "lxml-6.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:34190a1ec4f1e84af256495436b2d196529c3f2094f0af80202947567fdbf2e7"}, + {file = "lxml-6.0.0-cp310-cp310-win32.whl", hash = "sha256:5967fe415b1920a3877a4195e9a2b779249630ee49ece22021c690320ff07452"}, + {file = "lxml-6.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:f3389924581d9a770c6caa4df4e74b606180869043b9073e2cec324bad6e306e"}, + {file = "lxml-6.0.0-cp310-cp310-win_arm64.whl", hash = "sha256:522fe7abb41309e9543b0d9b8b434f2b630c5fdaf6482bee642b34c8c70079c8"}, + {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36"}, + {file = "lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e"}, + {file = "lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58"}, + {file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2"}, + {file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851"}, + {file = "lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f"}, + {file = "lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c"}, + {file = "lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816"}, + {file = "lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab"}, + {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108"}, + {file = "lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da"}, + {file = "lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e"}, + {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741"}, + {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3"}, + {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16"}, + {file = "lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0"}, + {file = "lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a"}, + {file = "lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3"}, + {file = "lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb"}, + {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:6da7cd4f405fd7db56e51e96bff0865b9853ae70df0e6720624049da76bde2da"}, + {file = "lxml-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b34339898bb556a2351a1830f88f751679f343eabf9cf05841c95b165152c9e7"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:51a5e4c61a4541bd1cd3ba74766d0c9b6c12d6a1a4964ef60026832aac8e79b3"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d18a25b19ca7307045581b18b3ec9ead2b1db5ccd8719c291f0cd0a5cec6cb81"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d4f0c66df4386b75d2ab1e20a489f30dc7fd9a06a896d64980541506086be1f1"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f4b481b6cc3a897adb4279216695150bbe7a44c03daba3c894f49d2037e0a24"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8a78d6c9168f5bcb20971bf3329c2b83078611fbe1f807baadc64afc70523b3a"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ae06fbab4f1bb7db4f7c8ca9897dc8db4447d1a2b9bee78474ad403437bcc29"}, + {file = "lxml-6.0.0-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:1fa377b827ca2023244a06554c6e7dc6828a10aaf74ca41965c5d8a4925aebb4"}, + {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1676b56d48048a62ef77a250428d1f31f610763636e0784ba67a9740823988ca"}, + {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:0e32698462aacc5c1cf6bdfebc9c781821b7e74c79f13e5ffc8bfe27c42b1abf"}, + {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4d6036c3a296707357efb375cfc24bb64cd955b9ec731abf11ebb1e40063949f"}, + {file = "lxml-6.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7488a43033c958637b1a08cddc9188eb06d3ad36582cebc7d4815980b47e27ef"}, + {file = "lxml-6.0.0-cp313-cp313-win32.whl", hash = "sha256:5fcd7d3b1d8ecb91445bd71b9c88bdbeae528fefee4f379895becfc72298d181"}, + {file = "lxml-6.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:2f34687222b78fff795feeb799a7d44eca2477c3d9d3a46ce17d51a4f383e32e"}, + {file = "lxml-6.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:21db1ec5525780fd07251636eb5f7acb84003e9382c72c18c542a87c416ade03"}, + {file = "lxml-6.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:4eb114a0754fd00075c12648d991ec7a4357f9cb873042cc9a77bf3a7e30c9db"}, + {file = "lxml-6.0.0-cp38-cp38-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:7da298e1659e45d151b4028ad5c7974917e108afb48731f4ed785d02b6818994"}, + {file = "lxml-6.0.0-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7bf61bc4345c1895221357af8f3e89f8c103d93156ef326532d35c707e2fb19d"}, + {file = "lxml-6.0.0-cp38-cp38-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63b634facdfbad421d4b61c90735688465d4ab3a8853ac22c76ccac2baf98d97"}, + {file = "lxml-6.0.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:e380e85b93f148ad28ac15f8117e2fd8e5437aa7732d65e260134f83ce67911b"}, + {file = "lxml-6.0.0-cp38-cp38-win32.whl", hash = "sha256:185efc2fed89cdd97552585c624d3c908f0464090f4b91f7d92f8ed2f3b18f54"}, + {file = "lxml-6.0.0-cp38-cp38-win_amd64.whl", hash = "sha256:f97487996a39cb18278ca33f7be98198f278d0bc3c5d0fd4d7b3d63646ca3c8a"}, + {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:85b14a4689d5cff426c12eefe750738648706ea2753b20c2f973b2a000d3d261"}, + {file = "lxml-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:f64ccf593916e93b8d36ed55401bb7fe9c7d5de3180ce2e10b08f82a8f397316"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:b372d10d17a701b0945f67be58fae4664fd056b85e0ff0fbc1e6c951cdbc0512"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:a674c0948789e9136d69065cc28009c1b1874c6ea340253db58be7622ce6398f"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:edf6e4c8fe14dfe316939711e3ece3f9a20760aabf686051b537a7562f4da91a"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:048a930eb4572829604982e39a0c7289ab5dc8abc7fc9f5aabd6fbc08c154e93"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c0b5fa5eda84057a4f1bbb4bb77a8c28ff20ae7ce211588d698ae453e13c6281"}, + {file = "lxml-6.0.0-cp39-cp39-manylinux_2_31_armv7l.whl", hash = "sha256:c352fc8f36f7e9727db17adbf93f82499457b3d7e5511368569b4c5bd155a922"}, + {file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:8db5dc617cb937ae17ff3403c3a70a7de9df4852a046f93e71edaec678f721d0"}, + {file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:2181e4b1d07dde53986023482673c0f1fba5178ef800f9ab95ad791e8bdded6a"}, + {file = "lxml-6.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:b3c98d5b24c6095e89e03d65d5c574705be3d49c0d8ca10c17a8a4b5201b72f5"}, + {file = "lxml-6.0.0-cp39-cp39-win32.whl", hash = "sha256:04d67ceee6db4bcb92987ccb16e53bef6b42ced872509f333c04fb58a3315256"}, + {file = "lxml-6.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:e0b1520ef900e9ef62e392dd3d7ae4f5fa224d1dd62897a792cf353eb20b6cae"}, + {file = "lxml-6.0.0-cp39-cp39-win_arm64.whl", hash = "sha256:e35e8aaaf3981489f42884b59726693de32dabfc438ac10ef4eb3409961fd402"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:dbdd7679a6f4f08152818043dbb39491d1af3332128b3752c3ec5cebc0011a72"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:40442e2a4456e9910875ac12951476d36c0870dcb38a68719f8c4686609897c4"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:db0efd6bae1c4730b9c863fc4f5f3c0fa3e8f05cae2c44ae141cb9dfc7d091dc"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ab542c91f5a47aaa58abdd8ea84b498e8e49fe4b883d67800017757a3eb78e8"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:013090383863b72c62a702d07678b658fa2567aa58d373d963cca245b017e065"}, + {file = "lxml-6.0.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:c86df1c9af35d903d2b52d22ea3e66db8058d21dc0f59842ca5deb0595921141"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:4337e4aec93b7c011f7ee2e357b0d30562edd1955620fdd4aeab6aacd90d43c5"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ae74f7c762270196d2dda56f8dd7309411f08a4084ff2dfcc0b095a218df2e06"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:059c4cbf3973a621b62ea3132934ae737da2c132a788e6cfb9b08d63a0ef73f9"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:17f090a9bc0ce8da51a5632092f98a7e7f84bca26f33d161a98b57f7fb0004ca"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9da022c14baeec36edfcc8daf0e281e2f55b950249a455776f0d1adeeada4734"}, + {file = "lxml-6.0.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a55da151d0b0c6ab176b4e761670ac0e2667817a1e0dadd04a01d0561a219349"}, + {file = "lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72"}, ] [package.extras] cssselect = ["cssselect (>=0.7)"] -html-clean = ["lxml-html-clean"] +html-clean = ["lxml_html_clean"] html5 = ["html5lib"] htmlsoup = ["BeautifulSoup4"] -source = ["Cython (>=3.0.11)"] [[package]] name = "markdown" -version = "3.3.7" -description = "Python implementation of Markdown." +version = "3.8.2" +description = "Python implementation of John Gruber's Markdown." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["dev", "docs"] files = [ - {file = "Markdown-3.3.7-py3-none-any.whl", hash = "sha256:f5da449a6e1c989a4cea2631aa8ee67caa5a2ef855d551c88f9e309f4634c621"}, - {file = "Markdown-3.3.7.tar.gz", hash = "sha256:cbb516f16218e643d8e0a95b309f77eb118cb138d39a4f27851e6a63581db874"}, + {file = "markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24"}, + {file = "markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45"}, ] -[package.dependencies] -importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} - [package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] testing = ["coverage", "pyyaml"] +[[package]] +name = "markdown-data-tables" +version = "1.0.0" +description = "Embed data files such as YAML as tables in a Markdown document" +optional = false +python-versions = ">=3.8,<4.0" +groups = ["docs"] +files = [ + {file = "markdown_data_tables-1.0.0-py3-none-any.whl", hash = "sha256:a59c6743685691ced4341bdb01024b7a863a1adaa3a2ef92fa068a7e90227d9a"}, + {file = "markdown_data_tables-1.0.0.tar.gz", hash = "sha256:ac1b07c58bb66e9f060ba81cdd63070ec94deb21f0147e519c77c8475ba696ea"}, +] + +[package.dependencies] +markdown = ">=3.3.7,<4.0.0" +pyyaml = ">=6.0,<7.0" +tabulate = ">=0.9.0,<0.10.0" + [[package]] name = "markdown-it-py" version = "3.0.0" description = "Python port of markdown-it. Markdown parsing, done right!" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, @@ -826,84 +898,102 @@ profiling = ["gprof2dot"] rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] +[[package]] +name = "markdown-version-annotations" +version = "1.0.1" +description = "Markdown plugin to add custom admonitions for documenting version differences" +optional = false +python-versions = "<4.0,>=3.7" +groups = ["docs"] +files = [ + {file = "markdown_version_annotations-1.0.1-py3-none-any.whl", hash = "sha256:6df0b2ac08bab906c8baa425f59fc0fe342fbe8b3917c144fb75914266b33200"}, + {file = "markdown_version_annotations-1.0.1.tar.gz", hash = "sha256:620aade507ef175ccfb2059db152a34c6a1d2add28c2be16ea4de38d742e6132"}, +] + +[package.dependencies] +markdown = ">=3.3.7,<4.0.0" + [[package]] name = "markupsafe" -version = "2.1.5" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false -python-versions = ">=3.7" -files = [ - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, - {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, - {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, - {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, - {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, - {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, - {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, - {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +python-versions = ">=3.9" +groups = ["main", "dev", "docs"] +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 = "mccabe" -version = "0.6.1" +version = "0.7.0" description = "McCabe checker, plugin for flake8" optional = false -python-versions = "*" +python-versions = ">=3.6" +groups = ["dev"] files = [ - {file = "mccabe-0.6.1-py2.py3-none-any.whl", hash = "sha256:ab8a6258860da4b6677da4bd2fe5dc2c659cff31b3ee4f7f5d64e79735b80d42"}, - {file = "mccabe-0.6.1.tar.gz", hash = "sha256:dd8d182285a0fe56bace7f45b5e7d1a6ebcbf524e8f3bd87eb0f125271b8831f"}, + {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, + {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, ] [[package]] @@ -912,6 +1002,7 @@ version = "0.1.2" description = "Markdown URL utilities" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, @@ -923,6 +1014,7 @@ version = "1.3.4" description = "A deep merge function for 🐍." optional = false python-versions = ">=3.6" +groups = ["docs"] files = [ {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, @@ -930,39 +1022,45 @@ files = [ [[package]] name = "mkdocs" -version = "1.3.1" +version = "1.6.1" description = "Project documentation with Markdown." optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +groups = ["docs"] files = [ - {file = "mkdocs-1.3.1-py3-none-any.whl", hash = "sha256:fda92466393127d2da830bc6edc3a625a14b436316d1caf347690648e774c4f0"}, - {file = "mkdocs-1.3.1.tar.gz", hash = "sha256:a41a2ff25ce3bbacc953f9844ba07d106233cd76c88bac1f59cb1564ac0d87ed"}, + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, ] [package.dependencies] -click = ">=3.3" +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} ghp-import = ">=1.0" -importlib-metadata = ">=4.3" -Jinja2 = ">=2.10.2" -Markdown = ">=3.2.1,<3.4" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" packaging = ">=20.5" -PyYAML = ">=3.10" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" pyyaml-env-tag = ">=0.1" watchdog = ">=2.0" [package.extras] i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] [[package]] name = "mkdocs-autorefs" -version = "1.2.0" +version = "1.4.2" description = "Automatically link across pages in MkDocs." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "mkdocs_autorefs-1.2.0-py3-none-any.whl", hash = "sha256:d588754ae89bd0ced0c70c06f58566a4ee43471eeeee5202427da7de9ef85a2f"}, - {file = "mkdocs_autorefs-1.2.0.tar.gz", hash = "sha256:a86b93abff653521bda71cf3fc5596342b7a23982093915cb74273f67522190f"}, + {file = "mkdocs_autorefs-1.4.2-py3-none-any.whl", hash = "sha256:83d6d777b66ec3c372a1aad4ae0cf77c243ba5bcda5bf0c6b8a2c5e7a3d89f13"}, + {file = "mkdocs_autorefs-1.4.2.tar.gz", hash = "sha256:e2ebe1abd2b67d597ed19378c0fff84d73d1dbce411fce7a7cc6f161888b6749"}, ] [package.dependencies] @@ -970,24 +1068,105 @@ Markdown = ">=3.3" markupsafe = ">=2.0.1" mkdocs = ">=1.1" +[[package]] +name = "mkdocs-gen-files" +version = "0.5.0" +description = "MkDocs plugin to programmatically generate documentation pages during the build" +optional = false +python-versions = ">=3.7" +groups = ["docs"] +files = [ + {file = "mkdocs_gen_files-0.5.0-py3-none-any.whl", hash = "sha256:7ac060096f3f40bd19039e7277dd3050be9a453c8ac578645844d4d91d7978ea"}, + {file = "mkdocs_gen_files-0.5.0.tar.gz", hash = "sha256:4c7cf256b5d67062a788f6b1d035e157fc1a9498c2399be9af5257d4ff4d19bc"}, +] + +[package.dependencies] +mkdocs = ">=1.0.3" + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + +[[package]] +name = "mkdocs-glightbox" +version = "0.4.0" +description = "MkDocs plugin supports image lightbox with GLightbox." +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "mkdocs-glightbox-0.4.0.tar.gz", hash = "sha256:392b34207bf95991071a16d5f8916d1d2f2cd5d5bb59ae2997485ccd778c70d9"}, + {file = "mkdocs_glightbox-0.4.0-py3-none-any.whl", hash = "sha256:e0107beee75d3eb7380ac06ea2d6eac94c999eaa49f8c3cbab0e7be2ac006ccf"}, +] + +[[package]] +name = "mkdocs-macros-plugin" +version = "1.3.7" +description = "Unleash the power of MkDocs with macros and variables" +optional = false +python-versions = ">=3.8" +groups = ["docs"] +files = [ + {file = "mkdocs_macros_plugin-1.3.7-py3-none-any.whl", hash = "sha256:02432033a5b77fb247d6ec7924e72fc4ceec264165b1644ab8d0dc159c22ce59"}, + {file = "mkdocs_macros_plugin-1.3.7.tar.gz", hash = "sha256:17c7fd1a49b94defcdb502fd453d17a1e730f8836523379d21292eb2be4cb523"}, +] + +[package.dependencies] +hjson = "*" +jinja2 = "*" +mkdocs = ">=0.17" +packaging = "*" +pathspec = "*" +python-dateutil = "*" +pyyaml = "*" +super-collections = "*" +termcolor = "*" + +[package.extras] +test = ["mkdocs-d2-plugin", "mkdocs-include-markdown-plugin", "mkdocs-macros-test", "mkdocs-material (>=6.2)", "mkdocs-test"] + [[package]] name = "mkdocs-material" -version = "8.3.9" +version = "9.6.15" description = "Documentation that simply works" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["docs"] files = [ - {file = "mkdocs-material-8.3.9.tar.gz", hash = "sha256:dc82b667d2a83f0de581b46a6d0949732ab77e7638b87ea35b770b33bc02e75a"}, - {file = "mkdocs_material-8.3.9-py2.py3-none-any.whl", hash = "sha256:263f2721f3abe533b61f7c8bed435a0462620912742c919821ac2d698b4bfe67"}, + {file = "mkdocs_material-9.6.15-py3-none-any.whl", hash = "sha256:ac969c94d4fe5eb7c924b6d2f43d7db41159ea91553d18a9afc4780c34f2717a"}, + {file = "mkdocs_material-9.6.15.tar.gz", hash = "sha256:64adf8fa8dba1a17905b6aee1894a5aafd966d4aeb44a11088519b0f5ca4f1b5"}, ] [package.dependencies] -jinja2 = ">=3.0.2" -markdown = ">=3.2" -mkdocs = ">=1.3.0" -mkdocs-material-extensions = ">=1.0.3" -pygments = ">=2.12" -pymdown-extensions = ">=9.4" +babel = ">=2.10,<3.0" +backrefs = ">=5.7.post1,<6.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.1,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.6,<2.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<3)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] [[package]] name = "mkdocs-material-extensions" @@ -995,39 +1174,62 @@ version = "1.3.1" description = "Extension pack for Python Markdown and MkDocs Material." optional = false python-versions = ">=3.8" +groups = ["docs"] files = [ {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, ] [[package]] -name = "mkdocs-version-annotations" -version = "1.0.0" -description = "MkDocs plugin to add custom admonitions for documenting version differences" +name = "mkdocs-redirects" +version = "1.2.2" +description = "A MkDocs plugin for dynamic page redirects to prevent broken links" optional = false -python-versions = ">=3.7,<4.0" +python-versions = ">=3.8" +groups = ["docs"] files = [ - {file = "mkdocs-version-annotations-1.0.0.tar.gz", hash = "sha256:6786024b37d27b330fda240b76ebec8e7ce48bd5a9d7a66e99804559d088dffa"}, - {file = "mkdocs_version_annotations-1.0.0-py3-none-any.whl", hash = "sha256:385004eb4a7530dd87a227e08cd907ce7a8fe21fdf297720a4149c511bcf05f5"}, + {file = "mkdocs_redirects-1.2.2-py3-none-any.whl", hash = "sha256:7dbfa5647b79a3589da4401403d69494bd1f4ad03b9c15136720367e1f340ed5"}, + {file = "mkdocs_redirects-1.2.2.tar.gz", hash = "sha256:3094981b42ffab29313c2c1b8ac3969861109f58b2dd58c45fc81cd44bfa0095"}, ] +[package.dependencies] +mkdocs = ">=1.1.1" + +[[package]] +name = "mkdocs-section-index" +version = "0.3.10" +description = "MkDocs plugin to allow clickable sections that lead to an index page" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "mkdocs_section_index-0.3.10-py3-none-any.whl", hash = "sha256:bc27c0d0dc497c0ebaee1fc72839362aed77be7318b5ec0c30628f65918e4776"}, + {file = "mkdocs_section_index-0.3.10.tar.gz", hash = "sha256:a82afbda633c82c5568f0e3b008176b9b365bf4bd8b6f919d6eff09ee146b9f8"}, +] + +[package.dependencies] +mkdocs = ">=1.2" + [[package]] name = "mkdocstrings" -version = "0.19.0" +version = "0.27.0" description = "Automatic documentation from sources, for MkDocs." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "mkdocstrings-0.19.0-py3-none-any.whl", hash = "sha256:3217d510d385c961f69385a670b2677e68e07b5fea4a504d86bf54c006c87c7d"}, - {file = "mkdocstrings-0.19.0.tar.gz", hash = "sha256:efa34a67bad11229d532d89f6836a8a215937548623b64f3698a1df62e01cc3e"}, + {file = "mkdocstrings-0.27.0-py3-none-any.whl", hash = "sha256:6ceaa7ea830770959b55a16203ac63da24badd71325b96af950e59fd37366332"}, + {file = "mkdocstrings-0.27.0.tar.gz", hash = "sha256:16adca6d6b0a1f9e0c07ff0b02ced8e16f228a9d65a37c063ec4c14d7b76a657"}, ] [package.dependencies] +click = ">=7.0" Jinja2 = ">=2.11.1" -Markdown = ">=3.3" +Markdown = ">=3.6" MarkupSafe = ">=1.1" -mkdocs = ">=1.2" -mkdocs-autorefs = ">=0.3.1" +mkdocs = ">=1.4" +mkdocs-autorefs = ">=1.2" +platformdirs = ">=2.2" pymdown-extensions = ">=6.3" [package.extras] @@ -1037,28 +1239,31 @@ python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] [[package]] name = "mkdocstrings-python" -version = "0.7.1" +version = "1.13.0" description = "A Python handler for mkdocstrings." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "mkdocstrings-python-0.7.1.tar.gz", hash = "sha256:c334b382dca202dfa37071c182418a6df5818356a95d54362a2b24822ca3af71"}, - {file = "mkdocstrings_python-0.7.1-py3-none-any.whl", hash = "sha256:a22060bfa374697678e9af4e62b020d990dad2711c98f7a9fac5c0345bef93c7"}, + {file = "mkdocstrings_python-1.13.0-py3-none-any.whl", hash = "sha256:b88bbb207bab4086434743849f8e796788b373bd32e7bfefbf8560ac45d88f97"}, + {file = "mkdocstrings_python-1.13.0.tar.gz", hash = "sha256:2dbd5757e8375b9720e81db16f52f1856bf59905428fd7ef88005d1370e2f64c"}, ] [package.dependencies] -griffe = ">=0.11.1" -mkdocstrings = ">=0.19" +griffe = ">=0.49" +mkdocs-autorefs = ">=1.2" +mkdocstrings = ">=0.26" [[package]] name = "mock" -version = "5.1.0" +version = "5.2.0" description = "Rolling backport of unittest.mock for all Pythons" optional = false python-versions = ">=3.6" +groups = ["dev"] files = [ - {file = "mock-5.1.0-py3-none-any.whl", hash = "sha256:18c694e5ae8a208cdb3d2c20a993ca1a7b0efa258c247a1e565150f477f83744"}, - {file = "mock-5.1.0.tar.gz", hash = "sha256:5e96aad5ccda4718e0a229ed94b2024df75cc2d55575ba5762d31f5767b8767d"}, + {file = "mock-5.2.0-py3-none-any.whl", hash = "sha256:7ba87f72ca0e915175596069dbbcc7c75af7b5e9b9bc107ad6349ede0819982f"}, + {file = "mock-5.2.0.tar.gz", hash = "sha256:4e460e818629b4b173f32d08bf30d3af8123afbb8e04bb5707a1fd4799e503f0"}, ] [package.extras] @@ -1066,23 +1271,13 @@ build = ["blurb", "twine", "wheel"] docs = ["sphinx"] test = ["pytest", "pytest-cov"] -[[package]] -name = "mypy-extensions" -version = "1.0.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.5" -files = [ - {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, - {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, -] - [[package]] name = "ncclient" version = "0.6.15" description = "Python library for NETCONF clients" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" +groups = ["main"] files = [ {file = "ncclient-0.6.15.tar.gz", hash = "sha256:6757cb41bc9160dfe47f22f5de8cf2f1adf22f27463fb50453cc415ab96773d8"}, ] @@ -1099,6 +1294,7 @@ version = "1.3.0" description = "A network address manipulation library for Python" optional = false python-versions = ">=3.7" +groups = ["main"] files = [ {file = "netaddr-1.3.0-py3-none-any.whl", hash = "sha256:c2c6a8ebe5554ce33b7d5b3a306b71bbb373e000bbbf2350dd5213cc56e3dbbe"}, {file = "netaddr-1.3.0.tar.gz", hash = "sha256:5c3c3d9895b551b763779ba7db7a03487dc1f8e3b385af819af341ae9ef6e48a"}, @@ -1109,34 +1305,36 @@ nicer-shell = ["ipython"] [[package]] name = "netmiko" -version = "4.4.0" +version = "4.6.0" description = "Multi-vendor library to simplify legacy CLI connections to network devices" optional = false -python-versions = "<4.0,>=3.8" +python-versions = "<4.0,>=3.9" +groups = ["main"] files = [ - {file = "netmiko-4.4.0-py3-none-any.whl", hash = "sha256:2ff4683f013fac0f80715286c7d3250e89166aefc4421cb75d3ff483f2ebbbc0"}, - {file = "netmiko-4.4.0.tar.gz", hash = "sha256:25ff1237976aa3ff2cacf04949314638c899220a1675bd029e31b07ce20ce3b6"}, + {file = "netmiko-4.6.0-py3-none-any.whl", hash = "sha256:0c9b7309005d2c8a010b275f3494628cadb1658a8841632131c848074b7cdadb"}, + {file = "netmiko-4.6.0.tar.gz", hash = "sha256:9701bb2c1a15eb2e8074cb2e28ca007c69b9fa52961b83b98c757ead6b80deef"}, ] [package.dependencies] -cffi = ">=1.17.0rc1" ntc-templates = ">=3.1.0" paramiko = ">=2.9.5" pyserial = ">=3.3" -pyyaml = ">=5.3" +pyyaml = ">=6.0.2" +rich = ">=13.8" +"ruamel.yaml" = ">=0.17" scp = ">=0.13.6" -setuptools = ">=65.0.0" textfsm = ">=1.1.3" [[package]] name = "ntc-templates" -version = "7.5.0" +version = "7.9.0" description = "TextFSM Templates for Network Devices, and Python wrapper for TextFSM's CliTable." optional = false python-versions = "<4.0,>=3.8" +groups = ["main"] files = [ - {file = "ntc_templates-7.5.0-py3-none-any.whl", hash = "sha256:9d7fb6467ccaaedf8e93e12106e4c46b1610e88d1bcae396b8c2f6a786d9db1c"}, - {file = "ntc_templates-7.5.0.tar.gz", hash = "sha256:b4b1693cd79ef0da5be0c66d58e3c6285d8d264d46832545765c0d394afed0aa"}, + {file = "ntc_templates-7.9.0-py3-none-any.whl", hash = "sha256:44ae2651719592bb70e98886f363b15bab12892b37f8338f0a2255aa5c7b6ee3"}, + {file = "ntc_templates-7.9.0.tar.gz", hash = "sha256:df4b4520c3dd41b33ad0746ea7742c63b13eb2860579c7648c348167af5432e2"}, ] [package.dependencies] @@ -1144,24 +1342,42 @@ textfsm = ">=1.1.0,<2.0.0" [[package]] name = "packaging" -version = "24.2" +version = "25.0" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] files = [ - {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, - {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, ] +[[package]] +name = "paginate" +version = "0.5.7" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +groups = ["docs"] +files = [ + {file = "paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591"}, + {file = "paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945"}, +] + +[package.extras] +dev = ["pytest", "tox"] +lint = ["black"] + [[package]] name = "paramiko" -version = "3.5.0" +version = "3.5.1" description = "SSH2 protocol library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ - {file = "paramiko-3.5.0-py3-none-any.whl", hash = "sha256:1fedf06b085359051cd7d0d270cebe19e755a8a921cc2ddbfa647fb0cd7d68f9"}, - {file = "paramiko-3.5.0.tar.gz", hash = "sha256:ad11e540da4f55cedda52931f1a3f812a8238a7af7f62a60de538cd80bb28124"}, + {file = "paramiko-3.5.1-py3-none-any.whl", hash = "sha256:43b9a0501fc2b5e70680388d9346cf252cfb7d00b0667c39e80eb43a408b8f61"}, + {file = "paramiko-3.5.1.tar.gz", hash = "sha256:b2c665bc45b2b215bd7d7f039901b14b067da00f3a11e6640995fd58f2664822"}, ] [package.dependencies] @@ -1170,8 +1386,8 @@ cryptography = ">=3.3" pynacl = ">=1.5" [package.extras] -all = ["gssapi (>=1.4.1)", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] -gssapi = ["gssapi (>=1.4.1)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8)"] +all = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "invoke (>=2.0)", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] +gssapi = ["gssapi (>=1.4.1) ; platform_system != \"Windows\"", "pyasn1 (>=0.1.7)", "pywin32 (>=2.1.8) ; platform_system == \"Windows\""] invoke = ["invoke (>=2.0)"] [[package]] @@ -1180,63 +1396,44 @@ version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" +groups = ["dev", "docs"] 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 = "pbr" -version = "6.1.0" -description = "Python Build Reasonableness" -optional = false -python-versions = ">=2.6" -files = [ - {file = "pbr-6.1.0-py2.py3-none-any.whl", hash = "sha256:a776ae228892d8013649c0aeccbb3d5f99ee15e005a4cbb7e61d55a067b28a2a"}, - {file = "pbr-6.1.0.tar.gz", hash = "sha256:788183e382e3d1d7707db08978239965e8b9e4e5ed42669bf4758186734d5f24"}, -] - [[package]] name = "platformdirs" -version = "4.3.6" +version = "4.3.8" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev", "docs"] files = [ - {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, - {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, ] [package.extras] -docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] -test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] -type = ["mypy (>=1.11.2)"] +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" +version = "1.6.0" description = "plugin and hook calling mechanisms for python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +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"}, + {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, + {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, ] [package.extras] dev = ["pre-commit", "tox"] -testing = ["pytest", "pytest-benchmark"] - -[[package]] -name = "pycodestyle" -version = "2.7.0" -description = "Python style guide checker" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pycodestyle-2.7.0-py2.py3-none-any.whl", hash = "sha256:514f76d918fcc0b55c6680472f0a37970994e07bbb80725808c17089be302068"}, - {file = "pycodestyle-2.7.0.tar.gz", hash = "sha256:c389c1d06bf7904078ca03399a4816f974a1d590090fecea0c63ec26ebaf1cef"}, -] +testing = ["coverage", "pytest", "pytest-benchmark"] [[package]] name = "pycparser" @@ -1244,34 +1441,19 @@ version = "2.22" description = "C parser in Python" optional = false python-versions = ">=3.8" +groups = ["main"] files = [ {file = "pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc"}, {file = "pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6"}, ] -[[package]] -name = "pydocstyle" -version = "6.3.0" -description = "Python docstring style checker" -optional = false -python-versions = ">=3.6" -files = [ - {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"}, - {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"}, -] - -[package.dependencies] -snowballstemmer = ">=2.2.0" - -[package.extras] -toml = ["tomli (>=1.2.3)"] - [[package]] name = "pyeapi" version = "1.0.4" description = "Python Client for eAPI" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyeapi-1.0.4.tar.gz", hash = "sha256:05920677246823cd3dddf7d4d0f831fbc86fd416f356706a03bc56a291d78f3d"}, ] @@ -1283,26 +1465,16 @@ netaddr = "*" dev = ["check-manifest", "pep8", "pyflakes", "twine"] test = ["coverage"] -[[package]] -name = "pyflakes" -version = "2.3.1" -description = "passive checker of Python programs" -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -files = [ - {file = "pyflakes-2.3.1-py2.py3-none-any.whl", hash = "sha256:7893783d01b8a89811dd72d7dfd4d84ff098e5eed95cfa8905b22bbffe52efc3"}, - {file = "pyflakes-2.3.1.tar.gz", hash = "sha256:f5bc8ecabc05bb9d291eb5203d6810b49040f6ff446a756326104746cc00c1db"}, -] - [[package]] name = "pygments" -version = "2.19.1" +version = "2.19.2" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ - {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, - {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, ] [package.extras] @@ -1310,29 +1482,29 @@ windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pylint" -version = "3.2.7" +version = "3.3.7" description = "python code static checker" optional = false -python-versions = ">=3.8.0" +python-versions = ">=3.9.0" +groups = ["dev"] files = [ - {file = "pylint-3.2.7-py3-none-any.whl", hash = "sha256:02f4aedeac91be69fb3b4bea997ce580a4ac68ce58b89eaefeaf06749df73f4b"}, - {file = "pylint-3.2.7.tar.gz", hash = "sha256:1b7a721b575eaeaa7d39db076b6e7743c993ea44f57979127c517c6c572c803e"}, + {file = "pylint-3.3.7-py3-none-any.whl", hash = "sha256:43860aafefce92fca4cf6b61fe199cdc5ae54ea28f9bf4cd49de267b5195803d"}, + {file = "pylint-3.3.7.tar.gz", hash = "sha256:2b11de8bde49f9c5059452e0c310c079c746a0a8eeaa789e5aa966ecc23e4559"}, ] [package.dependencies] -astroid = ">=3.2.4,<=3.3.0-dev0" +astroid = ">=3.3.8,<=3.4.0.dev0" colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""} dill = [ {version = ">=0.2", markers = "python_version < \"3.11\""}, {version = ">=0.3.7", markers = "python_version >= \"3.12\""}, - {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""}, + {version = ">=0.3.6", markers = "python_version == \"3.11\""}, ] -isort = ">=4.2.5,<5.13.0 || >5.13.0,<6" +isort = ">=4.2.5,<5.13 || >5.13,<7" mccabe = ">=0.6,<0.8" -platformdirs = ">=2.2.0" -tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +platformdirs = ">=2.2" +tomli = {version = ">=1.1", markers = "python_version < \"3.11\""} tomlkit = ">=0.10.1" -typing-extensions = {version = ">=3.10.0", markers = "python_version < \"3.10\""} [package.extras] spelling = ["pyenchant (>=3.2,<4.0)"] @@ -1340,21 +1512,22 @@ testutils = ["gitpython (>3)"] [[package]] name = "pymdown-extensions" -version = "10.4" +version = "10.16.1" description = "Extension pack for Python Markdown." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "pymdown_extensions-10.4-py3-none-any.whl", hash = "sha256:cfc28d6a09d19448bcbf8eee3ce098c7d17ff99f7bd3069db4819af181212037"}, - {file = "pymdown_extensions-10.4.tar.gz", hash = "sha256:bc46f11749ecd4d6b71cf62396104b4a200bad3498cb0f5dad1b8502fe461a35"}, + {file = "pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d"}, + {file = "pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91"}, ] [package.dependencies] -markdown = ">=3.2" +markdown = ">=3.6" pyyaml = "*" [package.extras] -extra = ["pygments (>=2.12)"] +extra = ["pygments (>=2.19.1)"] [[package]] name = "pynacl" @@ -1362,6 +1535,7 @@ version = "1.5.0" description = "Python binding to the Networking and Cryptography (NaCl) library" optional = false python-versions = ">=3.6" +groups = ["main"] files = [ {file = "PyNaCl-1.5.0-cp36-abi3-macosx_10_10_universal2.whl", hash = "sha256:401002a4aaa07c9414132aaed7f6836ff98f59277a234704ff66878c2ee4a0d1"}, {file = "PyNaCl-1.5.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:52cb72a79269189d4e0dc537556f4740f7f0a9ec41c1322598799b0bdad4ef92"}, @@ -1388,6 +1562,7 @@ version = "0.0.5" description = "A library for managing Cisco NX-OS devices through NX-API." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pynxos-0.0.5.tar.gz", hash = "sha256:ad148aec468b715b4a4c86802997cbf2b54e67e3709b043cf79f65b420143168"}, ] @@ -1399,13 +1574,14 @@ scp = "*" [[package]] name = "pyparsing" -version = "3.1.4" +version = "3.2.3" description = "pyparsing module - Classes and methods to define and execute parsing grammars" optional = false -python-versions = ">=3.6.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "pyparsing-3.1.4-py3-none-any.whl", hash = "sha256:a6a7ee4235a3f944aa1fa2249307708f893fe5717dc603503c6c7969c070fb7c"}, - {file = "pyparsing-3.1.4.tar.gz", hash = "sha256:f86ec8d1a83f11977c9a6ea7598e8c27fc5cddfa5b07ea2241edbbde1d7bc032"}, + {file = "pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf"}, + {file = "pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be"}, ] [package.extras] @@ -1417,6 +1593,7 @@ version = "3.5" description = "Python Serial Port Extension" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0"}, {file = "pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb"}, @@ -1427,25 +1604,27 @@ cp2110 = ["hidapi"] [[package]] name = "pytest" -version = "8.3.4" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, - {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.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"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] [[package]] name = "python-dateutil" @@ -1453,6 +1632,7 @@ version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["docs"] files = [ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, @@ -1467,6 +1647,7 @@ version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] 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"}, @@ -1525,13 +1706,14 @@ files = [ [[package]] name = "pyyaml-env-tag" -version = "0.1" -description = "A custom YAML tag for referencing environment variables in YAML files. " +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." optional = false -python-versions = ">=3.6" +python-versions = ">=3.9" +groups = ["docs"] files = [ - {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, - {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, ] [package.dependencies] @@ -1539,18 +1721,19 @@ pyyaml = "*" [[package]] name = "requests" -version = "2.32.3" +version = "2.32.4" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" +groups = ["main", "dev", "docs"] files = [ - {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, - {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, ] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<4" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" @@ -1564,6 +1747,7 @@ version = "1.12.1" description = "Mock out responses from the requests package" optional = false python-versions = ">=3.5" +groups = ["dev"] files = [ {file = "requests-mock-1.12.1.tar.gz", hash = "sha256:e9e12e333b525156e82a3c852f22016b9158220d2f47454de9cae8a77d371401"}, {file = "requests_mock-1.12.1-py2.py3-none-any.whl", hash = "sha256:b1e37054004cdd5e56c84454cc7df12b25f90f382159087f4b6915aaeef39563"}, @@ -1577,29 +1761,129 @@ fixture = ["fixtures"] [[package]] name = "rich" -version = "13.9.4" +version = "14.1.0" description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" optional = false python-versions = ">=3.8.0" +groups = ["main"] files = [ - {file = "rich-13.9.4-py3-none-any.whl", hash = "sha256:6049d5e6ec054bf2779ab3358186963bac2ea89175919d699e378b99738c2a90"}, - {file = "rich-13.9.4.tar.gz", hash = "sha256:439594978a49a09530cff7ebc4b5c7103ef57baf48d5ea3184f21d9a2befa098"}, + {file = "rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f"}, + {file = "rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8"}, ] [package.dependencies] markdown-it-py = ">=2.2.0" pygments = ">=2.13.0,<3.0.0" -typing-extensions = {version = ">=4.0.0,<5.0", markers = "python_version < \"3.11\""} [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] +[[package]] +name = "ruamel-yaml" +version = "0.18.14" +description = "ruamel.yaml is a YAML parser/emitter that supports roundtrip preservation of comments, seq/map flow style, and map key order" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2"}, + {file = "ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7"}, +] + +[package.dependencies] +"ruamel.yaml.clib" = {version = ">=0.2.7", markers = "platform_python_implementation == \"CPython\" and python_version < \"3.14\""} + +[package.extras] +docs = ["mercurial (>5.7)", "ryd"] +jinja2 = ["ruamel.yaml.jinja2 (>=0.2)"] + +[[package]] +name = "ruamel-yaml-clib" +version = "0.2.12" +description = "C version of reader, parser and emitter for ruamel.yaml derived from libyaml" +optional = false +python-versions = ">=3.9" +groups = ["main"] +markers = "platform_python_implementation == \"CPython\"" +files = [ + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-macosx_13_0_arm64.whl", hash = "sha256:11f891336688faf5156a36293a9c362bdc7c88f03a8a027c2c1d8e0bcde998e5"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux2014_aarch64.whl", hash = "sha256:a606ef75a60ecf3d924613892cc603b154178ee25abb3055db5062da811fd969"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5415dded15c3822597455bc02bcd66e81ef8b7a48cb71a33628fc9fdde39df"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f66efbc1caa63c088dead1c4170d148eabc9b80d95fb75b6c92ac0aad2437d76"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:22353049ba4181685023b25b5b51a574bce33e7f51c759371a7422dcae5402a6"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:932205970b9f9991b34f55136be327501903f7c66830e9760a8ffb15b07f05cd"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win32.whl", hash = "sha256:3eac5a91891ceb88138c113f9db04f3cebdae277f5d44eaa3651a4f573e6a5da"}, + {file = "ruamel.yaml.clib-0.2.12-cp310-cp310-win_amd64.whl", hash = "sha256:ab007f2f5a87bd08ab1499bdf96f3d5c6ad4dcfa364884cb4549aa0154b13a28"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:4a6679521a58256a90b0d89e03992c15144c5f3858f40d7c18886023d7943db6"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:d84318609196d6bd6da0edfa25cedfbabd8dbde5140a0a23af29ad4b8f91fb1e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bb43a269eb827806502c7c8efb7ae7e9e9d0573257a46e8e952f4d4caba4f31e"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:811ea1594b8a0fb466172c384267a4e5e367298af6b228931f273b111f17ef52"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:cf12567a7b565cbf65d438dec6cfbe2917d3c1bdddfce84a9930b7d35ea59642"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:7dd5adc8b930b12c8fc5b99e2d535a09889941aa0d0bd06f4749e9a9397c71d2"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win32.whl", hash = "sha256:bd0a08f0bab19093c54e18a14a10b4322e1eacc5217056f3c063bd2f59853ce4"}, + {file = "ruamel.yaml.clib-0.2.12-cp311-cp311-win_amd64.whl", hash = "sha256:a274fb2cb086c7a3dea4322ec27f4cb5cc4b6298adb583ab0e211a4682f241eb"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:20b0f8dc160ba83b6dcc0e256846e1a02d044e13f7ea74a3d1d56ede4e48c632"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:943f32bc9dedb3abff9879edc134901df92cfce2c3d5c9348f172f62eb2d771d"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95c3829bb364fdb8e0332c9931ecf57d9be3519241323c5274bd82f709cebc0c"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:749c16fcc4a2b09f28843cda5a193e0283e47454b63ec4b81eaa2242f50e4ccd"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bf165fef1f223beae7333275156ab2022cffe255dcc51c27f066b4370da81e31"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:32621c177bbf782ca5a18ba4d7af0f1082a3f6e517ac2a18b3974d4edf349680"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win32.whl", hash = "sha256:e8c4ebfcfd57177b572e2040777b8abc537cdef58a2120e830124946aa9b42c5"}, + {file = "ruamel.yaml.clib-0.2.12-cp312-cp312-win_amd64.whl", hash = "sha256:0467c5965282c62203273b838ae77c0d29d7638c8a4e3a1c8bdd3602c10904e4"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4c8c5d82f50bb53986a5e02d1b3092b03622c02c2eb78e29bec33fd9593bae1a"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux2014_aarch64.whl", hash = "sha256:e7e3736715fbf53e9be2a79eb4db68e4ed857017344d697e8b9749444ae57475"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0b7e75b4965e1d4690e93021adfcecccbca7d61c7bddd8e22406ef2ff20d74ef"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96777d473c05ee3e5e3c3e999f5d23c6f4ec5b0c38c098b3a5229085f74236c6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:3bc2a80e6420ca8b7d3590791e2dfc709c88ab9152c00eeb511c9875ce5778bf"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:e188d2699864c11c36cdfdada94d781fd5d6b0071cd9c427bceb08ad3d7c70e1"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win32.whl", hash = "sha256:6442cb36270b3afb1b4951f060eccca1ce49f3d087ca1ca4563a6eb479cb3de6"}, + {file = "ruamel.yaml.clib-0.2.12-cp313-cp313-win_amd64.whl", hash = "sha256:e5b8daf27af0b90da7bb903a876477a9e6d7270be6146906b276605997c7e9a3"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:fc4b630cd3fa2cf7fce38afa91d7cfe844a9f75d7f0f36393fa98815e911d987"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux2014_aarch64.whl", hash = "sha256:bc5f1e1c28e966d61d2519f2a3d451ba989f9ea0f2307de7bc45baa526de9e45"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a0e060aace4c24dcaf71023bbd7d42674e3b230f7e7b97317baf1e953e5b519"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e2f1c3765db32be59d18ab3953f43ab62a761327aafc1594a2a1fbe038b8b8a7"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d85252669dc32f98ebcd5d36768f5d4faeaeaa2d655ac0473be490ecdae3c285"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e143ada795c341b56de9418c58d028989093ee611aa27ffb9b7f609c00d813ed"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win32.whl", hash = "sha256:beffaed67936fbbeffd10966a4eb53c402fafd3d6833770516bf7314bc6ffa12"}, + {file = "ruamel.yaml.clib-0.2.12-cp39-cp39-win_amd64.whl", hash = "sha256:040ae85536960525ea62868b642bdb0c2cc6021c9f9d507810c0c604e66f5a7b"}, + {file = "ruamel.yaml.clib-0.2.12.tar.gz", hash = "sha256:6c8fbb13ec503f99a91901ab46e0b07ae7941cd527393187039aec586fdfd36f"}, +] + +[[package]] +name = "ruff" +version = "0.12.7" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "ruff-0.12.7-py3-none-linux_armv6l.whl", hash = "sha256:76e4f31529899b8c434c3c1dede98c4483b89590e15fb49f2d46183801565303"}, + {file = "ruff-0.12.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:789b7a03e72507c54fb3ba6209e4bb36517b90f1a3569ea17084e3fd295500fb"}, + {file = "ruff-0.12.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:2e1c2a3b8626339bb6369116e7030a4cf194ea48f49b64bb505732a7fce4f4e3"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32dec41817623d388e645612ec70d5757a6d9c035f3744a52c7b195a57e03860"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:47ef751f722053a5df5fa48d412dbb54d41ab9b17875c6840a58ec63ff0c247c"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a828a5fc25a3efd3e1ff7b241fd392686c9386f20e5ac90aa9234a5faa12c423"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:5726f59b171111fa6a69d82aef48f00b56598b03a22f0f4170664ff4d8298efb"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74e6f5c04c4dd4aba223f4fe6e7104f79e0eebf7d307e4f9b18c18362124bccd"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d0bfe4e77fba61bf2ccadf8cf005d6133e3ce08793bbe870dd1c734f2699a3e"}, + {file = "ruff-0.12.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:06bfb01e1623bf7f59ea749a841da56f8f653d641bfd046edee32ede7ff6c606"}, + {file = "ruff-0.12.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:e41df94a957d50083fd09b916d6e89e497246698c3f3d5c681c8b3e7b9bb4ac8"}, + {file = "ruff-0.12.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:4000623300563c709458d0ce170c3d0d788c23a058912f28bbadc6f905d67afa"}, + {file = "ruff-0.12.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:69ffe0e5f9b2cf2b8e289a3f8945b402a1b19eff24ec389f45f23c42a3dd6fb5"}, + {file = "ruff-0.12.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a07a5c8ffa2611a52732bdc67bf88e243abd84fe2d7f6daef3826b59abbfeda4"}, + {file = "ruff-0.12.7-py3-none-win32.whl", hash = "sha256:c928f1b2ec59fb77dfdf70e0419408898b63998789cc98197e15f560b9e77f77"}, + {file = "ruff-0.12.7-py3-none-win_amd64.whl", hash = "sha256:9c18f3d707ee9edf89da76131956aba1270c6348bfee8f6c647de841eac7194f"}, + {file = "ruff-0.12.7-py3-none-win_arm64.whl", hash = "sha256:dfce05101dbd11833a0776716d5d1578641b7fddb537fe7fa956ab85d1769b69"}, + {file = "ruff-0.12.7.tar.gz", hash = "sha256:1fc3193f238bc2d7968772c82831a4ff69252f673be371fb49663f0068b7ec71"}, +] + [[package]] name = "scp" version = "0.14.5" description = "scp module for paramiko" optional = false python-versions = "*" +groups = ["main"] files = [ {file = "scp-0.14.5-py2.py3-none-any.whl", hash = "sha256:d224535dd8ed00294f52b0e0e18fde7a6fb7a3d06b97ede9e3f750fa7bf75c09"}, {file = "scp-0.14.5.tar.gz", hash = "sha256:64f0015899b3d212cb8088e7d40ebaf0686889ff0e243d5c1242efe8b50f053e"}, @@ -1610,23 +1894,24 @@ paramiko = "*" [[package]] name = "setuptools" -version = "75.3.0" +version = "80.9.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main"] files = [ - {file = "setuptools-75.3.0-py3-none-any.whl", hash = "sha256:f2504966861356aa38616760c0f66568e535562374995367b4e69c7143cf6bcd"}, - {file = "setuptools-75.3.0.tar.gz", hash = "sha256:fba5dd4d766e97be1b1681d98712680ae8f2f26d7881245f2ce9e40714f1a686"}, + {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, + {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] [package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.5.2)"] -core = ["importlib-metadata (>=6)", "importlib-resources (>=5.10.2)", "jaraco.collections", "jaraco.functools", "jaraco.text (>=3.7)", "more-itertools", "more-itertools (>=8.8)", "packaging", "packaging (>=24)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"] +check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] +core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] cover = ["pytest-cov"] doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "jaraco.test (>=5.5)", "packaging (>=23.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib-metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.12.*)", "pytest-mypy"] +test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] +type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"] [[package]] name = "six" @@ -1634,35 +1919,59 @@ version = "1.17.0" description = "Python 2 and 3 compatibility utilities" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["main", "docs"] files = [ {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, ] [[package]] -name = "snowballstemmer" -version = "2.2.0" -description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." +name = "super-collections" +version = "0.5.3" +description = "file: README.md" optional = false -python-versions = "*" +python-versions = ">=3.8" +groups = ["docs"] files = [ - {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, - {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, + {file = "super_collections-0.5.3-py3-none-any.whl", hash = "sha256:907d35b25dc4070910e8254bf2f5c928348af1cf8a1f1e8259e06c666e902cff"}, + {file = "super_collections-0.5.3.tar.gz", hash = "sha256:94c1ec96c0a0d5e8e7d389ed8cde6882ac246940507c5e6b86e91945c2968d46"}, ] +[package.dependencies] +hjson = "*" + +[package.extras] +test = ["pytest (>=7.0)"] + [[package]] -name = "stevedore" -version = "5.3.0" -description = "Manage dynamic plugins for Python applications" +name = "tabulate" +version = "0.9.0" +description = "Pretty-print tabular data" optional = false -python-versions = ">=3.8" +python-versions = ">=3.7" +groups = ["docs"] files = [ - {file = "stevedore-5.3.0-py3-none-any.whl", hash = "sha256:1efd34ca08f474dad08d9b19e934a22c68bb6fe416926479ba29e5013bcc8f78"}, - {file = "stevedore-5.3.0.tar.gz", hash = "sha256:9a64265f4060312828151c204efbe9b7a9852a0d9228756344dbc7e4023e375a"}, + {file = "tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f"}, + {file = "tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c"}, ] -[package.dependencies] -pbr = ">=2.0.0" +[package.extras] +widechars = ["wcwidth"] + +[[package]] +name = "termcolor" +version = "3.1.0" +description = "ANSI color formatting for output in terminal" +optional = false +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "termcolor-3.1.0-py3-none-any.whl", hash = "sha256:591dd26b5c2ce03b9e43f391264626557873ce1d379019786f99b0c2bee140aa"}, + {file = "termcolor-3.1.0.tar.gz", hash = "sha256:6a6dd7fbee581909eeec6a756cff1d7f7c376063b14e4a298dc4980309e55970"}, +] + +[package.extras] +tests = ["pytest", "pytest-cov"] [[package]] name = "textfsm" @@ -1670,6 +1979,7 @@ version = "1.1.3" description = "Python module for parsing semi-structured text into python tables." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "textfsm-1.1.3-py2.py3-none-any.whl", hash = "sha256:dcbeebc6a6137bed561c71a56344d752e6dbc04ae5ea309252cb70fb97ccc9cd"}, {file = "textfsm-1.1.3.tar.gz", hash = "sha256:577ef278a9237f5341ae9b682947cefa4a2c1b24dbe486f94f2c95addc6504b5"}, @@ -1685,6 +1995,7 @@ version = "0.10.2" description = "Python Library for Tom's Obvious, Minimal Language" optional = false python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" +groups = ["dev"] files = [ {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, @@ -1696,6 +2007,8 @@ version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" +groups = ["dev"] +markers = "python_version == \"3.10\"" 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"}, @@ -1733,24 +2046,46 @@ files = [ [[package]] name = "tomlkit" -version = "0.13.2" +version = "0.13.3" description = "Style preserving TOML library" optional = false python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0"}, + {file = "tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1"}, +] + +[[package]] +name = "towncrier" +version = "24.8.0" +description = "Building newsfiles for your project." +optional = false +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"}, - {file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"}, + {file = "towncrier-24.8.0-py3-none-any.whl", hash = "sha256:9343209592b839209cdf28c339ba45792fbfe9775b5f9c177462fd693e127d8d"}, + {file = "towncrier-24.8.0.tar.gz", hash = "sha256:013423ee7eed102b2f393c287d22d95f66f1a3ea10a4baa82d298001a7f18af3"}, ] +[package.dependencies] +click = "*" +jinja2 = "*" +tomli = {version = "*", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["furo (>=2024.05.06)", "nox", "packaging", "sphinx (>=5)", "twisted"] + [[package]] name = "transitions" -version = "0.9.2" +version = "0.9.3" description = "A lightweight, object-oriented Python state machine implementation with many extensions." optional = false python-versions = "*" +groups = ["main"] files = [ - {file = "transitions-0.9.2-py2.py3-none-any.whl", hash = "sha256:f7b40c9b4a93869f36c4d1c33809aeb18cdeeb065fd1adba018ee39c3db216f3"}, - {file = "transitions-0.9.2.tar.gz", hash = "sha256:2f8490dbdbd419366cef1516032ab06d07ccb5839ef54905e842a472692d4204"}, + {file = "transitions-0.9.3-py2.py3-none-any.whl", hash = "sha256:02463248f2b668d86f66636b1e3c9e8de84d93e22915247f4e1aa9ee1cae28aa"}, + {file = "transitions-0.9.3.tar.gz", hash = "sha256:881fb75bb1654ed55d86060bb067f2c716f8e155f57bb73fd444e53713aafec8"}, ] [package.dependencies] @@ -1762,102 +2097,88 @@ test = ["pytest"] [[package]] name = "typing-extensions" -version = "4.12.2" -description = "Backported and Experimental Type Hints for Python 3.8+" +version = "4.14.1" +description = "Backported and Experimental Type Hints for Python 3.9+" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ - {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, - {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, + {file = "typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76"}, + {file = "typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36"}, ] [[package]] name = "urllib3" -version = "2.2.3" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["main", "dev", "docs"] files = [ - {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, - {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, ] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +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 = "watchdog" -version = "4.0.2" +version = "6.0.0" description = "Filesystem events monitoring" optional = false -python-versions = ">=3.8" -files = [ - {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ede7f010f2239b97cc79e6cb3c249e72962404ae3865860855d5cbe708b0fd22"}, - {file = "watchdog-4.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a2cffa171445b0efa0726c561eca9a27d00a1f2b83846dbd5a4f639c4f8ca8e1"}, - {file = "watchdog-4.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c50f148b31b03fbadd6d0b5980e38b558046b127dc483e5e4505fcef250f9503"}, - {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c7d4bf585ad501c5f6c980e7be9c4f15604c7cc150e942d82083b31a7548930"}, - {file = "watchdog-4.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:914285126ad0b6eb2258bbbcb7b288d9dfd655ae88fa28945be05a7b475a800b"}, - {file = "watchdog-4.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:984306dc4720da5498b16fc037b36ac443816125a3705dfde4fd90652d8028ef"}, - {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:1cdcfd8142f604630deef34722d695fb455d04ab7cfe9963055df1fc69e6727a"}, - {file = "watchdog-4.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d7ab624ff2f663f98cd03c8b7eedc09375a911794dfea6bf2a359fcc266bff29"}, - {file = "watchdog-4.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:132937547a716027bd5714383dfc40dc66c26769f1ce8a72a859d6a48f371f3a"}, - {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd67c7df93eb58f360c43802acc945fa8da70c675b6fa37a241e17ca698ca49b"}, - {file = "watchdog-4.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bcfd02377be80ef3b6bc4ce481ef3959640458d6feaae0bd43dd90a43da90a7d"}, - {file = "watchdog-4.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:980b71510f59c884d684b3663d46e7a14b457c9611c481e5cef08f4dd022eed7"}, - {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:aa160781cafff2719b663c8a506156e9289d111d80f3387cf3af49cedee1f040"}, - {file = "watchdog-4.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:f6ee8dedd255087bc7fe82adf046f0b75479b989185fb0bdf9a98b612170eac7"}, - {file = "watchdog-4.0.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:0b4359067d30d5b864e09c8597b112fe0a0a59321a0f331498b013fb097406b4"}, - {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:770eef5372f146997638d737c9a3c597a3b41037cfbc5c41538fc27c09c3a3f9"}, - {file = "watchdog-4.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eeea812f38536a0aa859972d50c76e37f4456474b02bd93674d1947cf1e39578"}, - {file = "watchdog-4.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b2c45f6e1e57ebb4687690c05bc3a2c1fb6ab260550c4290b8abb1335e0fd08b"}, - {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:10b6683df70d340ac3279eff0b2766813f00f35a1d37515d2c99959ada8f05fa"}, - {file = "watchdog-4.0.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:f7c739888c20f99824f7aa9d31ac8a97353e22d0c0e54703a547a218f6637eb3"}, - {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:c100d09ac72a8a08ddbf0629ddfa0b8ee41740f9051429baa8e31bb903ad7508"}, - {file = "watchdog-4.0.2-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:f5315a8c8dd6dd9425b974515081fc0aadca1d1d61e078d2246509fd756141ee"}, - {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:2d468028a77b42cc685ed694a7a550a8d1771bb05193ba7b24006b8241a571a1"}, - {file = "watchdog-4.0.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f15edcae3830ff20e55d1f4e743e92970c847bcddc8b7509bcd172aa04de506e"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_aarch64.whl", hash = "sha256:936acba76d636f70db8f3c66e76aa6cb5136a936fc2a5088b9ce1c7a3508fc83"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_armv7l.whl", hash = "sha256:e252f8ca942a870f38cf785aef420285431311652d871409a64e2a0a52a2174c"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_i686.whl", hash = "sha256:0e83619a2d5d436a7e58a1aea957a3c1ccbf9782c43c0b4fed80580e5e4acd1a"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64.whl", hash = "sha256:88456d65f207b39f1981bf772e473799fcdc10801062c36fd5ad9f9d1d463a73"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:32be97f3b75693a93c683787a87a0dc8db98bb84701539954eef991fb35f5fbc"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_s390x.whl", hash = "sha256:c82253cfc9be68e3e49282831afad2c1f6593af80c0daf1287f6a92657986757"}, - {file = "watchdog-4.0.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:c0b14488bd336c5b1845cee83d3e631a1f8b4e9c5091ec539406e4a324f882d8"}, - {file = "watchdog-4.0.2-py3-none-win32.whl", hash = "sha256:0d8a7e523ef03757a5aa29f591437d64d0d894635f8a50f370fe37f913ce4e19"}, - {file = "watchdog-4.0.2-py3-none-win_amd64.whl", hash = "sha256:c344453ef3bf875a535b0488e3ad28e341adbd5a9ffb0f7d62cefacc8824ef2b"}, - {file = "watchdog-4.0.2-py3-none-win_ia64.whl", hash = "sha256:baececaa8edff42cd16558a639a9b0ddf425f93d892e8392a56bf904f5eff22c"}, - {file = "watchdog-4.0.2.tar.gz", hash = "sha256:b4dfbb6c49221be4535623ea4474a4d6ee0a9cef4a80b20c28db4d858b64e270"}, +python-versions = ">=3.9" +groups = ["docs"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, ] [package.extras] watchmedo = ["PyYAML (>=3.10)"] -[[package]] -name = "wheel" -version = "0.45.1" -description = "A built-package format for Python" -optional = false -python-versions = ">=3.8" -files = [ - {file = "wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248"}, - {file = "wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729"}, -] - -[package.extras] -test = ["pytest (>=6.0.0)", "setuptools (>=65)"] - [[package]] name = "yamllint" -version = "1.35.1" +version = "1.37.1" description = "A linter for YAML files." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "yamllint-1.35.1-py3-none-any.whl", hash = "sha256:2e16e504bb129ff515b37823b472750b36b6de07963bd74b307341ef5ad8bdc3"}, - {file = "yamllint-1.35.1.tar.gz", hash = "sha256:7a003809f88324fd2c877734f2d575ee7881dd9043360657cc8049c809eba6cd"}, + {file = "yamllint-1.37.1-py3-none-any.whl", hash = "sha256:364f0d79e81409f591e323725e6a9f4504c8699ddf2d7263d8d2b539cd66a583"}, + {file = "yamllint-1.37.1.tar.gz", hash = "sha256:81f7c0c5559becc8049470d86046b36e96113637bcbe4753ecef06977c00245d"}, ] [package.dependencies] @@ -1873,6 +2194,7 @@ version = "0.4.2" description = "YAML loader and dumper for PyYAML allowing to keep keys order." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "yamlordereddictloader-0.4.2-py3-none-any.whl", hash = "sha256:dc048adb67026786cd24119bd71241f35bc8b0fd37d24b415c37bbc8049f9cd7"}, {file = "yamlordereddictloader-0.4.2.tar.gz", hash = "sha256:36af2f6210fcff5da4fc4c12e1d815f973dceb41044e795e1f06115d634bca13"}, @@ -1881,26 +2203,7 @@ files = [ [package.dependencies] pyyaml = "*" -[[package]] -name = "zipp" -version = "3.20.2" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.8" -files = [ - {file = "zipp-3.20.2-py3-none-any.whl", hash = "sha256:a817ac80d6cf4b23bf7f2828b7cabf326f15a001bea8b1f9b49631780ba28350"}, - {file = "zipp-3.20.2.tar.gz", hash = "sha256:bc9eb26f4506fda01b81bcde0ca78103b6e62f991b381fec825435c836edbc29"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more-itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - [metadata] -lock-version = "2.0" -python-versions = "^3.8" -content-hash = "d47b4578c91f6f2f8c6b0dddacaaab6fbf5efc7aa678bd3a6fe133b0d2c1a65e" +lock-version = "2.1" +python-versions = ">=3.10,<3.14" +content-hash = "76102fe7332fc0f84d722b536f6e71f9ae3390ab027da60cee11595467abe1e6" diff --git a/pyntc/__init__.py b/pyntc/__init__.py index 36b855ab..6bc3e2ac 100644 --- a/pyntc/__init__.py +++ b/pyntc/__init__.py @@ -5,7 +5,11 @@ from importlib import metadata from .devices import supported_devices -from .errors import ConfFileNotFoundError, DeviceNameNotFoundError, UnsupportedDeviceError +from .errors import ( + ConfFileNotFoundError, + DeviceNameNotFoundError, + UnsupportedDeviceError, +) try: from configparser import ConfigParser as SafeConfigParser @@ -26,14 +30,16 @@ def ntc_device(device_type, *args, **kwargs): """ Instantiate an instance of a ``pyntc.devices.BaseDevice`` by ``device_type``. - The ``*args`` and ``*kwargs`` are passed directly to the device initializer. + The ``*args`` and ``**kwargs`` are passed directly to the device initializer. Arguments: - device_type (string): A valid device_type - listed in ``pyntc.devices.supported_devices`` + device_type (str): A valid device_type + listed in `pyntc.devices.supported_devices` + args (tuple): Positional arguments to pass to the device initializer. + kwargs (dict): Keyword arguments to pass to the device initializer. Returns: - An instance of a subclass of ``pyntc.devices.BaseDevice``. + (pyntc.devices.BaseDevice): An instance of a subclass of ``pyntc.devices.BaseDevice``. Raises: UnsupportedDeviceError: if the device_type is unsupported. diff --git a/pyntc/devices/__init__.py b/pyntc/devices/__init__.py index d421c656..a9dcead4 100644 --- a/pyntc/devices/__init__.py +++ b/pyntc/devices/__init__.py @@ -1,14 +1,13 @@ """Device drivers.""" -from .eos_device import EOSDevice -from .nxos_device import NXOSDevice -from .ios_device import IOSDevice -from .jnpr_device import JunosDevice +from .aireos_device import AIREOSDevice from .asa_device import ASADevice +from .eos_device import EOSDevice from .f5_device import F5Device -from .aireos_device import AIREOSDevice +from .ios_device import IOSDevice from .iosxewlc_device import IOSXEWLCDevice - +from .jnpr_device import JunosDevice +from .nxos_device import NXOSDevice supported_devices = { "cisco_asa_ssh": ASADevice, diff --git a/pyntc/devices/aireos_device.py b/pyntc/devices/aireos_device.py index 129e1695..f77b6103 100644 --- a/pyntc/devices/aireos_device.py +++ b/pyntc/devices/aireos_device.py @@ -48,7 +48,7 @@ def convert_filename_to_version(filename): filename (str): The name of the file downloaded from Cisco. Returns: - str: The version number. + (str): The version number. Example: >>> version = convert_filename_to_version("AIR-CT5520-K9-8-8-125-0.aes") @@ -69,7 +69,8 @@ class AIREOSDevice(BaseDevice): vendor = "cisco" active_redundancy_states = {None, "active"} - def __init__( # nosec # pylint: disable=too-many-arguments + # pylint: disable=too-many-arguments, too-many-positional-arguments + def __init__( # nosec self, host, username, password, secret="", port=None, confirm_active=True, **kwargs ): # noqa: D403 """ @@ -82,6 +83,7 @@ def __init__( # nosec # pylint: disable=too-many-arguments secret (str): The password to escalate privilege on the device. port (int): The port to use to establish the connection. Defaults to 22. confirm_active (bool): Determines if device's high availability state should be validated before leaving connection open. + **kwargs (dict): Additional keyword arguments for device customization. """ super().__init__(host, username, password, device_type="cisco_aireos_ssh") self.native = None @@ -126,6 +128,7 @@ def _check_command_output_for_errors(self, command, command_response): Args: command (str): The command that was sent to the device. + command_response (str): The response received from the device. Raises: CommandError: When ``command_response`` reports an error in sending ``command``. @@ -137,7 +140,6 @@ def _check_command_output_for_errors(self, command, command_response): >>> device._check_command_output_for_errors(command, command_response) >>> command = "invalid command" >>> command_response = "Incorrect Usage: invalid command" - >>> device._check_command_output_for_errors(command, command_resposne) CommandError: ... >>> """ @@ -157,6 +159,7 @@ def _image_booted(self, image_name, **vendor_specifics): Args: image_name (str): The version to check if image is booted. + vendor_specifics (dict): Additional vendor-specific arguments. Returns: bool: True if ``image_name`` is the current boot version, else False. @@ -184,9 +187,7 @@ def _send_command(self, command, expect_string=None, **kwargs): Args: command (str): The command to send to the device. expect_string (str): The expected prompt after running the command. - - Kwargs: - Any argument supported by Netmiko's ``send_command_timing`` method. + kwargs (dict): Any argument supported by Netmiko's ``send_command_timing`` method. Returns: str: The response from the device after issuing the ``command``. @@ -276,7 +277,7 @@ def _wait_for_ap_image_download(self, timeout=3600): } >>> - TODO: + Todo: Change timeout to be a multiplier for number of APs attached to controller """ start = time.time() @@ -297,7 +298,7 @@ def _wait_for_ap_image_download(self, timeout=3600): failed, ) raise FileTransferError( - "Failed transferring image to AP\n" f"Unsupported: {unsupported}\n" f"Failed: {failed}\n" + f"Failed transferring image to AP\nUnsupported: {unsupported}\nFailed: {failed}\n" ) elapsed_time = time.time() - start if elapsed_time > timeout: @@ -313,9 +314,7 @@ def _wait_for_ap_image_download(self, timeout=3600): ) log.debug( - "Host %s:" - "End of waiting time for AP image to be transferred to all devices:\n" - "Total: %s\nDownloaded: %s", + "Host %s:End of waiting time for AP image to be transferred to all devices:\nTotal: %s\nDownloaded: %s", self.host, ap_count, downloaded, @@ -394,7 +393,7 @@ def ap_boot_options(self): Boot Options for all APs associated with the controller. Returns: - dict: The name of each AP are the keys, and the values are the primary and backup values. + (dict): The name of each AP are the keys, and the values are the primary and backup values. Example: >>> device = AIREOSDevice(**connection_args) @@ -423,7 +422,7 @@ def ap_boot_options(self): } for ap in ap_boot_options } - log.debug("Host %s: Boot options: {boot_options_by_ap}", self.host, boot_options_by_ap) + log.debug("Host %s: Boot options: %s", self.host, boot_options_by_ap) return boot_options_by_ap @property @@ -432,7 +431,7 @@ def ap_image_stats(self): Stats of downloading the the image to all APs. Returns: - dict: The AP count, and the downloaded, unsupported, and failed APs. + (dict): The AP count, and the downloaded, unsupported, and failed APs. Example: >>> device = AIREOSDevice(**connection_args) @@ -477,7 +476,7 @@ def boot_options(self): Images that are candidates for booting on reload. Returns: - dict: The boot options on the device. The "sys" key is the expected image on reload. + (dict): The boot options on the device. The "sys" key is the expected image on reload. Example: >>> device = AIREOSDevice(**connection_args) @@ -516,7 +515,7 @@ def checkpoint(self, filename): Create a checkpoint file of the current config. Args: - checkpoint_file (str): Saves a checkpoint file with the name provided to the function. + filename (str): Saves a checkpoint file with the name provided to the function. Raises: NotImplementedError: Function currently not implemented @@ -542,11 +541,11 @@ def config(self, command, **netmiko_args): Args: command (str|list): The command or commands to send to the device. - **netmiko_args: Any argument supported by ``netmiko.base_connection.BaseConnection.send_config_set``. + **netmiko_args (dict): Any argument supported by ``netmiko.base_connection.BaseConnection.send_config_set``. Returns: - str: When ``command`` is a str, the config session input and ouput from sending ``command``. - list: When ``command`` is a list, the config session input and ouput from sending ``command``. + (str): When ``command`` is a str, the config session input and ouput from sending ``command``. + (list): When ``command`` is a list, the config session input and ouput from sending ``command``. Raises: TypeError: When sending an argument in ``**netmiko_args`` that is not supported. @@ -620,7 +619,7 @@ def confirm_is_active(self): Confirm that the device is either standalone or the active device in a high availability cluster. Returns: - bool: True when the device is considered active. + (bool): True when the device is considered active. Rasies: DeviceNotActiveError: When the device is not considered the active device. @@ -662,7 +661,7 @@ def connected(self): Get connection status of the device. Returns: - bool: True if the device is connected, else False. + (bool): True if the device is connected, else False. """ log.debug("Host %s: Connection status %s.", self.host, self._connected) return self._connected @@ -729,7 +728,7 @@ def disabled_wlans(self): # noqa: D403 IDs for all disabled WLANs. Returns: - list: Disabled WLAN IDs. + (list): Disabled WLAN IDs. Example: >>> device = AIREOSDevice(**connection_args) @@ -755,7 +754,7 @@ def enable(self): Ensure device is in enable mode. Returns: - None: Device prompt is set to enable mode. + (None): Device prompt is set to enable mode. """ # Netmiko reports enable and config mode as being enabled if not self.native.check_enable_mode(): @@ -822,7 +821,7 @@ def enabled_wlans(self): # noqa: D403 IDs for all enabled WLANs. Returns: - list: Enabled WLAN IDs. + (list): Enabled WLAN IDs. Example: >>> device = AIREOSDevice(**connection_args) @@ -852,6 +851,7 @@ def facts(self): """ raise NotImplementedError + # pylint: disable=too-many-arguments, too-many-positional-arguments def file_copy( self, username, @@ -870,12 +870,12 @@ def file_copy( password (str): The password to authenticate with the ``server``. server (str): The address of the file server. filepath (str): The full path to the file on the ``server``. - protocol (str): The transfer protocol to use to transfer the file. - filetype (str): The type of file per aireos definitions. - read_timeout (int): The Netmiko read_timeout to wait for device to complete transfer. + protocol (str, optional): The transfer protocol to use to transfer the file. Defaults to "sftp". + filetype (str, optional): The type of file per aireos definitions. Defaults to "code". + read_timeout (int, optional): The Netmiko read_timeout to wait for device to complete transfer. Defaults to 1000. Returns: - bool: True when the file was transferred, False when the file is deemed to already be on the device. + (bool): True when the file was transferred, False when the file is deemed to already be on the device. Raises: FileTransferError: When an error is detected in transferring the file. @@ -955,6 +955,7 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): src (str): The path to the file to be copied to the device. dest (str, optional): The name to use for storing the file on the device. Defaults to use the name of the ``src`` file. + kwargs (dict): Any additional arguments supported by Netmiko's ``file_copy`` method. Raises: NotImplementedError: Function currently not implemented. @@ -980,9 +981,10 @@ def install_os(self, image_name, controller="both", save_config=True, disable_wl save_config (bool): Whether the config should be saved to the device before reboot. disable_wlans (str|list): Which WLANs to disable/enable before/after upgrade. Default is None. To disable all WLANs, pass `"all"`. To disable select WLANs, pass a list of WLAN IDs. + vendor_specifics (dict): Any vendor specific arguments to pass to the install method. Returns: - bool: True when the install is successful, False when the version is deemed to already be running. + (bool): True when the install is successful, False when the version is deemed to already be running. Raises: OSInstallError: When the device is not booted with the specified image after reload. @@ -1045,7 +1047,7 @@ def is_active(self): Determine if the current processor is the active processor. Returns: - bool: True if the processor is active or does not support HA, else False. + (bool): True if the processor is active or does not support HA, else False. Example: >>> device = AIREOSDevice(**connection_args) @@ -1113,8 +1115,8 @@ def peer_redundancy_state(self): Determine the redundancy state of the peer processor. Returns: - str: The redundancy state of the peer processor. - None: When the processor does not support redundancy. + (str): The redundancy state of the peer processor. + (None): When the processor does not support redundancy. Example: >>> device = AIREOSDevice(**connection_args) @@ -1140,9 +1142,10 @@ def reboot(self, wait_for_reload=False, controller="self", save_config=True, **k Reload the controller or controller pair. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. controller (str): Which controller(s) to reboot (only applies to HA pairs). save_config (bool): Whether the configuration should be saved before reload. + kwargs (dict): Additional arguments that are not used, but are accepted for backwards compatibility. Raises: ReloadTimeoutError: When the device is still unreachable after the timeout period. @@ -1189,7 +1192,7 @@ def redundancy_mode(self): Get operating redundancy mode of the controller. Returns: - str: The redundancy mode the device is operating in. + (str): The redundancy mode the device is operating in. Example: >>> device = AIREOSDevice(**connection_args) @@ -1199,7 +1202,7 @@ def redundancy_mode(self): """ high_availability = self.show("show redundancy summary") ha_mode = re.search(r"^\s*Redundancy\s+Mode\s*=\s*(.+?)\s*$", high_availability, re.M) - log.debug("Host %s: Redundancy mode: {ha_mode.group(1).lower()}", self.host, ha_mode.group(1).lower()) + log.debug("Host %s: Redundancy mode: %s", self.host, ha_mode.group(1).lower()) return ha_mode.group(1).lower() @property @@ -1208,8 +1211,8 @@ def redundancy_state(self): Determine the redundancy state of the current processor. Returns: - str: The redundancy state of the current processor. - None: When the processor does not support redundancy. + (str): The redundancy state of the current processor. + (None): When the processor does not support redundancy. Example: >>> device = AIREOSDevice(**connection_args) @@ -1253,7 +1256,7 @@ def save(self): Save the configuration on the device. Returns: - bool: True if the save command did not fail. + (bool): True if the save command did not fail. Example: >>> device = AIREOSDevice(**connection_args) @@ -1270,6 +1273,7 @@ def set_boot_options(self, image_name, **vendor_specifics): Args: image_name (str): The version to boot into on next reload. + **vendor_specifics (dict): Additional vendor-specific arguments (unused). Raises: NTCFileNotFoundError: When the version is not listed in ``boot_options``. @@ -1278,16 +1282,16 @@ def set_boot_options(self, image_name, **vendor_specifics): >>> device = AIREOSDevice(**connection_args) >>> device.boot_options { - 'backup': '8.8.125.0', - 'primary': '8.9.100.0', - 'sys': '8.9.100.0' + 'backup': '8.8.125.0', + 'primary': '8.9.100.0', + 'sys': '8.9.100.0' } >>> device.set_boot_options("8.8.125.0") >>> device.boot_options { - 'backup': '8.8.125.0', - 'primary': '8.9.100.0', - 'sys': '8.8.125.0' + 'backup': '8.8.125.0', + 'primary': '8.9.100.0', + 'sys': '8.8.125.0' } """ if self.boot_options["primary"] == image_name: @@ -1313,11 +1317,11 @@ def show(self, command, expect_string=None, **netmiko_args): Args: command (str|list): The commands to send to the device. expect_string (str): The expected prompt after running the command. - **netmiko_args: Any argument supported by ``netmiko.ConnectHandler.send_command``. + **netmiko_args (dict): Any argument supported by ``netmiko.ConnectHandler.send_command``. Returns: - str: When ``command`` is str, the data returned from the device. - list: When ``command`` is list, the data returned from the device for each command. + (str): When ``command`` is str, the data returned from the device. + (list): When ``command`` is list, the data returned from the device for each command. Raises: TypeError: When sending an argument in ``**netmiko_args`` that is not supported. @@ -1405,10 +1409,9 @@ def transfer_image_to_ap(self, image): Args: image (str): The image that should be sent to the APs. - timeout (int): Removed, The max time to wait for all APs to download the image. Returns: - bool: True if AP images are transferred or swapped, False otherwise. + (bool): True if AP images are transferred or swapped, False otherwise. Example: >>> device = AIREOSDevice(**connection_args) @@ -1482,7 +1485,7 @@ def uptime(self): Get uptime of the device in seconds. Returns: - int: The number of seconds the device has been up. + (int): The number of seconds the device has been up. Example: >>> device = AIREOSDevice(**connection_args) @@ -1503,7 +1506,7 @@ def uptime_string(self): Get uptime of the device as a string in the format is dd::hh::mm. Returns: - str: The uptime of the device. + (str): The uptime of the device. Example: >>> device = AIREOSDevice(**connection_args) @@ -1520,7 +1523,7 @@ def wlans(self): All configured WLANs. Returns: - dict: WLAN IDs mapped to their operational data. + (dict): WLAN IDs mapped to their operational data. Example: >>> device = AIREOSDevice(**connection_args) diff --git a/pyntc/devices/asa_device.py b/pyntc/devices/asa_device.py index 398a000b..03ea207c 100644 --- a/pyntc/devices/asa_device.py +++ b/pyntc/devices/asa_device.py @@ -4,7 +4,7 @@ import re import time from collections import Counter -from ipaddress import ip_address, IPv4Address, IPv4Interface, IPv6Address, IPv6Interface +from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface, ip_address from typing import Dict, Iterable, List, Optional, Union from netmiko import ConnectHandler @@ -38,6 +38,7 @@ class ASADevice(BaseDevice): vendor = "cisco" active_redundancy_states = {None, "active"} + # pylint: disable=too-many-arguments, too-many-positional-arguments def __init__(self, host: str, username: str, password: str, secret="", port=None, **kwargs): # nosec """ Pyntc Device constructor for Cisco ASA. @@ -48,6 +49,7 @@ def __init__(self, host: str, username: str, password: str, secret="", port=None password (str): The password to authenticate to the device. secret (str, optional): The password to escalate privilege on the device. Defaults to 22. port (int, optional): Port used to establish connection. Defaults to 22. + kwargs (dict): Additional keyword arguments to pass to the Netmiko connection handler. """ super().__init__(host, username, password, device_type="cisco_asa_ssh") @@ -116,7 +118,7 @@ def _get_file_system(self): """Determine the default file system or directory for device. Returns: - str: The name of the default file system or directory for the device. + (str): The name of the default file system or directory for the device. Raises: FileSystemNotFound: When the module is unable to determine the default file system. @@ -141,7 +143,7 @@ def _get_ipv4_addresses(self, host: str) -> Dict[str, List[IPv4Address]]: host (str): Whether to get IP Addresses for `self` or `peer` device. Returns: - dict: The list of ``ip_interface`` objects mapped to their associated interface. + (dict): The list of ``ip_interface`` objects mapped to their associated interface. Example: >>> dev = ASADevice(**connection_args) @@ -162,7 +164,7 @@ def _get_ipv4_addresses(self, host: str) -> Dict[str, List[IPv4Address]]: results = { interface: [IPv4Interface(f"{address}/{netmask}")] for interface, address, netmask in re_ip_addresses } - log.debug("Host %s: ip interfaces %s", self.host) + log.debug("Host %s: ip interfaces %s", self.host, results) return results def _get_ipv6_addresses(self, host: str) -> Dict[str, List[IPv6Address]]: @@ -173,7 +175,7 @@ def _get_ipv6_addresses(self, host: str) -> Dict[str, List[IPv6Address]]: host (str): Whether to get IP Addresses for `self` or `peer` device. Returns: - dict: The list of ``ip_interface`` objects mapped to their associated interface. + (dict): The list of ``ip_interface`` objects mapped to their associated interface. Example: >>> dev = ASADevice(**connection_args) @@ -362,7 +364,7 @@ def boot_options(self): Determine boot image. Returns: - dict: Key: 'sys' Value: Current boot image. + (dict): Key: 'sys' Value: Current boot image. """ show_boot_out = self.show("show boot | i BOOT variable") # Improve regex to get only the first boot $var in the sequence! @@ -398,7 +400,7 @@ def config(self, command): """Send configuration commands to a device. Args: - commands (str, list): String with single command, or list with multiple commands. + command (str, list): String with single command, or list with multiple commands. Raises: CommandListError: Message stating which command failed and the response from the device. @@ -447,7 +449,7 @@ def enable(self): """Ensure device is in enable mode. Returns: - None: Device prompt is set to enable mode. + (None): Device prompt is set to enable mode. """ # Netmiko reports enable and config mode as being enabled if not self.native.check_enable_mode(): @@ -560,7 +562,7 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): Defaults to discover the default directory of the device. Returns: - bool: True if the file exists on the device and the md5 hashes match. Otherwise, false. + (bool): True if the file exists on the device and the md5 hashes match. Otherwise, false. Example: >>> status = file_copy_remote_exists("path/to/asa-image.bin") @@ -586,12 +588,13 @@ def install_os(self, image_name, **vendor_specifics): Args: image_name (str): Name of the image to be installed. + vendor_specifics (dict): Vendor specific arguments to pass to the install process. Raises: OSInstallError: Message stating the end device could not boot into the new image. Returns: - bool: True if new image is installed correctly. False if device is already running image_name. + (bool): True if new image is installed correctly. False if device is already running image_name. """ timeout = vendor_specifics.get("timeout", 3600) if not self._image_booted(image_name): @@ -693,7 +696,7 @@ def ip_protocol(self) -> str: """ protocol = f"ipv{self.ip_address.version}" - log.debug("Host %s: IP protocol for paramiko is %s.", self.host) + log.debug("Host %s: IP protocol for paramiko is %s.", self.host, protocol) return protocol def is_active(self): @@ -701,7 +704,7 @@ def is_active(self): Determine if the current processor is the active processor. Returns: - bool: True if the processor is active or does not support HA, else False. + (bool): True if the processor is active or does not support HA, else False. Example: >>> device = ASADevice(**connection_args) @@ -822,8 +825,8 @@ def peer_redundancy_state(self): common state will be returned. Returns: - str: The redundancy state of the peer processor. - None: When the processor does not support redundancy. + (str): The redundancy state of the peer processor. + (None): When the processor does not support redundancy. Example: >>> device = ASADevice(**connection_args) @@ -860,7 +863,8 @@ def reboot(self, wait_for_reload=False, **kwargs): Reload the controller or controller pair. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + kwargs (dict): Additional arguments to pass to the reboot method. Raises: RebootTimeoutError: When the device is still unreachable after the timeout period. @@ -932,7 +936,7 @@ def redundancy_mode(self): Operating redundancy mode of the device. Returns: - str: The redundancy mode the device is operating in. + (str): The redundancy mode the device is operating in. If the command is not supported, then "n/a" is returned. Example: @@ -961,8 +965,8 @@ def redundancy_state(self): common state will be returned. Returns: - str: The redundancy state of the processor. - None: When the processor does not support redundancy. + (str): The redundancy state of the processor. + (None): When the processor does not support redundancy. Example: >>> device = ASADevice(**connection_args) @@ -1012,7 +1016,7 @@ def running_config(self): Get current running config on device. Returns: - str: Running configuration on device. + (str): Running configuration on device. """ return self.show("show running-config") @@ -1024,7 +1028,7 @@ def save(self, filename="startup-config"): filename (str, optional): Name of startup configuration file. Defaults to "startup-config". Returns: - bool: True if configuration saved succesfully. + (bool): True if configuration saved succesfully. """ command = f"copy running-config {filename}" # Changed to send_command_timing to not require a direct prompt return. @@ -1044,6 +1048,7 @@ def set_boot_options(self, image_name, **vendor_specifics): Args: image_name (str): AName of image. + vendor_specifics (dict): Vendor specific arguments to pass to the set_boot_options process. Raises: NTCFileNotFoundError: File not found on device. @@ -1088,7 +1093,7 @@ def show(self, command, expect_string=None): expect_string (str, optional): Expected response from running command on device. Defaults to None. Returns: - str: Output from running command on device. + (str): Output from running command on device. """ self.enable() log.debug("Host %s: Successfully executed command 'show' with responses.", self.host) @@ -1118,7 +1123,7 @@ def uptime(self): """Get uptime from device. Returns: - int: Uptime in seconds. + (int): Uptime in seconds. """ if self._uptime is None: version_data = self._raw_version_data() @@ -1132,7 +1137,7 @@ def uptime_string(self): """Get uptime in format dd:hh:mm. Returns: - str: Uptime of device. + (str): Uptime of device. """ if self._uptime_string is None: version_data = self._raw_version_data() @@ -1146,7 +1151,7 @@ def hostname(self): """Get hostname of device. Returns: - str: Hostname of device. + (str): Hostname of device. """ version_data = self._raw_version_data() if self._hostname is None: @@ -1160,7 +1165,7 @@ def interfaces(self): Get list of interfaces on device. Returns: - list: List of interfaces on device. + (list): List of interfaces on device. """ if self._interfaces is None: self._interfaces = list(x["interface"] for x in self._interfaces_detailed_list()) @@ -1172,7 +1177,7 @@ def model(self): """Get the device model. Returns: - str: Device model. + (str): Device model. """ version_data = self._raw_version_data() if self._model is None: @@ -1185,7 +1190,7 @@ def os_version(self): """Get os version on device. Returns: - str: OS version on device. + (str): OS version on device. """ version_data = self._raw_version_data() if self._os_version is None: @@ -1198,7 +1203,7 @@ def serial_number(self): """Get serial number of device. Returns: - str: Serial number of device. + (str): Serial number of device. """ version_data = self._raw_version_data() if self._serial_number is None: @@ -1211,7 +1216,7 @@ def vlans(self): """Get vlan ids from device. Returns: - list: List of vlans + (list): List of vlans """ if self._vlans is None: self._vlans = self._show_vlan() diff --git a/pyntc/devices/base_device.py b/pyntc/devices/base_device.py index 6c496d6c..abe02237 100644 --- a/pyntc/devices/base_device.py +++ b/pyntc/devices/base_device.py @@ -1,16 +1,18 @@ """The module contains the base class that all device classes must inherit from.""" +import hashlib import importlib import warnings from pyntc.errors import FeatureNotFoundError, NTCError +from pyntc.utils.models import FileCopyModel def fix_docs(cls): """Create docstring at runtime. Returns: - class: Returns the class passed in. + (class): Returns the class passed in. """ for name, func in vars(cls).items(): if hasattr(func, "__call__") and not func.__doc__: @@ -26,9 +28,7 @@ def fix_docs(cls): class BaseDevice: # pylint: disable=too-many-instance-attributes,too-many-public-methods """Base Device ABC.""" - def __init__( - self, host, username, password, device_type=None, **kwargs - ): # noqa: D403 # pylint: disable=unused-argument + def __init__(self, host, username, password, device_type=None, **kwargs): # noqa: D403 # pylint: disable=unused-argument """PyNTC base device implementation. Args: @@ -36,6 +36,7 @@ def __init__( username (str): The username to authenticate with the device. password (str): The password to authenticate with the device. device_type (str, optional): Denotes which device type. Defaults to None. + kwargs (dict): Additional keyword arguments that may be used by subclasses. """ self.host = host self.username = username @@ -60,7 +61,7 @@ def _image_booted(self, image_name, **vendor_specifics): volume: Required by F5Device as F5 boots into a volume. Returns: - bool: True if image is currently being used by the device, else False. + (bool): True if image is currently being used by the device, else False. """ raise NotImplementedError @@ -79,7 +80,7 @@ def boot_options(self): like system image and kickstart image. Returns: - A dictionary, e.g. {'kick': router_kick.img, 'sys': 'router_sys.img'} + (dict): A dictionary, e.g. {'kick': router_kick.img, 'sys': 'router_sys.img'} """ raise NotImplementedError @@ -218,10 +219,11 @@ def file_copy(self, src, dest=None, **kwargs): dest (str): The destination file path to be saved on remote flash. If none is supplied, the implementing class should use the basename of the source path. + kwargs (dict): Additional keyword arguments that may be used by subclasses. Keyword Args: file_system (str): Supported only for IOS and NXOS. The file system for the - remote fle. If no file_system is provided, then the ``get_file_system`` + remote file. If no file_system is provided, then the ``get_file_system`` method is used to determine the correct file system to use. """ raise NotImplementedError @@ -237,21 +239,135 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): dest (str): The destination file path to be saved on remote the remote device. If none is supplied, the implementing class should use the basename of the source path. + kwargs (dict): Additional keyword arguments that may be used by subclasses. Keyword Args: file_system (str): Supported only for IOS and NXOS. The file system for the - remote fle. If no file_system is provided, then the ``get_file_system`` + remote file. If no file_system is provided, then the ``get_file_system`` method is used to determine the correct file system to use. Returns: - True if the remote file exists, False if it doesn't. + (bool): True if the remote file exists, False if it doesn't. """ + def check_file_exists(self, filename, **kwargs): + """Check if a remote file exists by filename. + + Args: + filename (str): The name of the file to check for on the remote device. + kwargs (dict): Additional keyword arguments that may be used by subclasses. + + Keyword Args: + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (bool): True if the remote file exists, False if it doesn't. + """ + raise NotImplementedError + + def get_remote_checksum(self, filename, hashing_algorithm="md5", **kwargs): + """Get the checksum of a remote file. + + Args: + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use (default: "md5"). + kwargs (dict): Additional keyword arguments that may be used by subclasses. + + Keyword Args: + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (str): The checksum of the remote file. + """ + raise NotImplementedError + + @staticmethod + def get_local_checksum(filepath, hashing_algorithm="md5", add_newline=False): + """Get the checksum of a local file using a specified algorithm. + + Args: + filepath (str): The path to the local file. + hashing_algorithm (str): The hashing algorithm to use (e.g., "md5", "sha256"). + add_newline (bool): Whether to append a newline before final hashing (Some devices may require this). + + Returns: + (str): The hex digest of the file. + """ + # Initialize the hash object dynamically + file_hash = hashlib.new(hashing_algorithm.lower()) + + with open(filepath, "rb") as f: + # Read in chunks to handle large firmware files without RAM spikes + for chunk in iter(lambda: f.read(4096), b""): + file_hash.update(chunk) + + if add_newline: + file_hash.update(b"\n") + + return file_hash.hexdigest() + + def compare_file_checksum(self, checksum, filename, hashing_algorithm="md5", **kwargs): + """Compare the checksum of a local file with a remote file. + + Args: + checksum (str): The checksum of the file. + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use (default: "md5"). + kwargs (dict): Additional keyword arguments that may be used by subclasses. + + Keyword Args: + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (bool): True if the checksums match, False otherwise. + """ + return checksum == self.get_remote_checksum(filename, hashing_algorithm, **kwargs) + + def remote_file_copy(self, src: FileCopyModel = None, dest=None, **kwargs): + """Copy a file to a remote device. + + Args: + src (FileCopyModel): The source file model. + dest (str): The destination file path on the remote device. + kwargs (dict): Additional keyword arguments that may be used by subclasses. + + Keyword Args: + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + """ + raise NotImplementedError + + def verify_file(self, checksum, filename, hashing_algorithm="md5", **kwargs): + """Verify a file on the remote device by confirming the file exists and validate the checksum. + + Args: + checksum (str): The checksum of the file. + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use (default: "md5"). + kwargs (dict): Additional keyword arguments that may be used by subclasses. + + Keyword Args: + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (bool): True if the file is verified successfully, False otherwise. + """ + raise NotImplementedError + def install_os(self, image_name, **vendor_specifics): """Install the OS from specified image_name. Args: - image_name(str): The name of the image on the device to install. + image_name (str): The name of the image on the device to install. Keyword Args: kickstart (str): Option for ``NXOSDevice`` for devices that require a kickstart image. @@ -261,9 +377,10 @@ def install_os(self, image_name, **vendor_specifics): the ``_get_file_system`` method. timeout (int): Option for ``IOSDevice`` and ``NXOSDevice`` to set the wait time for device installation to complete. + vendor_specifics (kwargs): Additional keyword arguments that may be used by subclasses. Returns: - True if system has been installed during function's call, False if OS has not been installed + (bool): True if system has been installed during function's call, False if OS has not been installed Raises: OSInstallError: When device finishes installation process, but the running image @@ -287,7 +404,7 @@ def reboot(self, wait_for_reload=False): """Reload a device. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. Raises: RebootTimeoutError: When the device is still unreachable after the timeout period. @@ -324,14 +441,15 @@ def set_boot_options(self, image_name, **vendor_specifics): """Set boot variables like system image and kickstart image. Args: - image_name: The main system image file name. + image_name (str): The main system image file name. Keyword Args: - kickstart: Option for ``NXOSDevice`` for devices that require a kickstart image. - volume: Option for ``F5Device`` to set which volume should have image installed. - file_system: Option for ``ASADevice`` and ``IOSDevice`` to set which directory + kickstart (str): Option for ``NXOSDevice`` for devices that require a kickstart image. + volume (str): Option for ``F5Device`` to set which volume should have image installed. + file_system (str): Option for ``ASADevice`` and ``IOSDevice`` to set which directory to use when setting the boot path. The default will use the directory returned by the ``_get_file_system()`` method. + vendor_specifics (kwargs): Additional keyword arguments that may be used by subclasses. Raises: ValueError: When the boot options returned by the ``boot_options`` @@ -350,7 +468,7 @@ def show(self, command, raw_text=False): raw_text (bool): Whether to return raw text or structured data. Returns: - The output of the show command, which could be raw text or structured data. + (NotImplementedError): The output of the show command, which could be raw text or structured data. """ raise NotImplementedError @@ -375,7 +493,7 @@ def get_boot_options(self): """Get current boot variables like system image and kickstart image. Returns: - A dictionary, e.g. {'kick': router_kick.img, 'sys': 'router_sys.img'} + (dict): A dictionary, e.g. {'kick': router_kick.img, 'sys': 'router_sys.img'} """ warnings.warn("get_boot_options() is deprecated; use boot_options property.", DeprecationWarning) return self.boot_options diff --git a/pyntc/devices/eos_device.py b/pyntc/devices/eos_device.py index 7ee87a35..d3c8345b 100644 --- a/pyntc/devices/eos_device.py +++ b/pyntc/devices/eos_device.py @@ -10,7 +10,7 @@ from pyeapi.eapilib import CommandError as EOSCommandError from pyntc import log -from pyntc.devices.base_device import BaseDevice, fix_docs, RollbackError +from pyntc.devices.base_device import BaseDevice, RollbackError, fix_docs from pyntc.devices.system_features.vlans.eos_vlans import EOSVlans from pyntc.errors import ( CommandError, @@ -24,7 +24,6 @@ ) from pyntc.utils import convert_list_by_key - BASIC_FACTS_KM = {"model": "modelName", "os_version": "internalVersion", "serial_number": "serialNumber"} INTERFACES_KM = { "speed": "bandwidth", @@ -41,6 +40,7 @@ class EOSDevice(BaseDevice): vendor = "arista" + # pylint: disable=too-many-arguments, too-many-positional-arguments def __init__(self, host, username, password, transport="http", port=None, timeout=None, **kwargs): # noqa: D403 """PyNTC Device implementation for Arista EOS. @@ -50,7 +50,8 @@ def __init__(self, host, username, password, transport="http", port=None, timeou password (str): The password to authenticate with the device. transport (str): The protocol to communicate with the device. Defaults to http. port (int): The port to use to establish the connection. Defaults to None. - timeout(int): Timeout value used for connection with the device. Defaults to None. + timeout (int): Timeout value used for connection with the device. Defaults to None. + kwargs (dict): Additional keyword arguments. """ super().__init__(host, username, password, device_type="arista_eos_eapi") self.transport = transport @@ -175,7 +176,7 @@ def boot_options(self): """Get current running software. Returns: - dict: Key is ``sys`` with value being the image on the device. + (dict): Key is ``sys`` with value being the image on the device. """ image = self.show("show boot-config")["softwareImage"] image = image.replace("flash:/", "") @@ -220,7 +221,7 @@ def enable(self): """Ensure device is in enable mode. Returns: - None: Device prompt is set to enable mode. + (None): Device prompt is set to enable mode. """ # Netmiko reports enable and config mode as being enabled if not self.native_ssh.check_enable_mode(): @@ -237,7 +238,7 @@ def uptime(self): Get uptime of the device in seconds. Returns: - int: Uptime of the device. + (int): Uptime of the device. """ if self._uptime is None: sh_version_output = self.show("show version") @@ -252,7 +253,7 @@ def uptime_string(self): Get uptime of the device in the format of dd::hh::mm. Returns: - str: Uptime in string format. + (str): Uptime in string format. """ if self._uptime_string is None: self._uptime_string = self._uptime_to_string(self.uptime) @@ -264,7 +265,7 @@ def hostname(self): """Get hostname from device. Returns: - str: Hostname of the device. + (str): Hostname of the device. """ if self._hostname is None: sh_hostname_output = self.show("show hostname") @@ -278,7 +279,7 @@ def interfaces(self): """Get list of interfaces on device. Returns: - list: List of interfaces + (list): List of interfaces """ if self._interfaces is None: iface_detailed_list = self._interfaces_status_list() @@ -292,7 +293,7 @@ def vlans(self): """Get list of VLANS on device. Returns: - list: List of VLANS on device. + (list): List of VLANS on device. """ if self._vlans is None: vlans = EOSVlans(self) @@ -306,7 +307,7 @@ def fqdn(self): """Get fully-qualified domain name of device. Returns: - str: Fully-qualified domain name of device. + (str): Fully-qualified domain name of device. """ if self._fqdn is None: sh_hostname_output = self.show("show hostname") @@ -320,7 +321,7 @@ def model(self): """Get model of device. Returns: - str: Model of device. + (str): Model of device. """ if self._model is None: sh_version_output = self.show("show version") @@ -334,7 +335,7 @@ def os_version(self): """Get OS version on device. Returns: - str: OS version of device. + (str): OS version of device. """ if self._os_version is None: sh_version_output = self.show("show version") @@ -348,7 +349,7 @@ def serial_number(self): """Get serial number of device. Returns: - str: Serial number of device. + (str): Serial number of device. """ if self._serial_number is None: sh_version_output = self.show("show version") @@ -401,12 +402,12 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): """Copy file to remote device if it exists. Args: - src (string): source file - dest (string, optional): Destintion file. Defaults to None. - file_system (string, optional): Describes device file system. Defaults to None. + src (str): source file + dest (str, optional): Destintion file. Defaults to None. + file_system (str, optional): Describes device file system. Defaults to None. Returns: - bool: True if remote file exists. + (bool): True if remote file exists. """ self.enable() if file_system is None: @@ -425,12 +426,13 @@ def install_os(self, image_name, **vendor_specifics): Args: image_name (str): Name of the image name to be installed. + vendor_specifics (dict): Vendor specific options for installing OS, such as timeout. Raises: OSInstallError: Error in installing new OS. Returns: - bool: True if device OS is succesfully installed. + (bool): True if device OS is succesfully installed. """ timeout = vendor_specifics.get("timeout", 3600) if not self._image_booted(image_name): @@ -475,7 +477,8 @@ def reboot(self, wait_for_reload=False, **kwargs): Reload the controller or controller pair. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + kwargs (dict): Additional keyword arguments, such as confirm. Raises: RebootTimeoutError: When the device is still unreachable after the timeout period. @@ -515,7 +518,7 @@ def running_config(self): """Return running config. Returns: - str: Running configuration. + (str): Running configuration. """ log.debug("Host %s: Show running config.", self.host) return self.show("show running-config", raw_text=True) @@ -524,7 +527,7 @@ def save(self, filename="startup-config"): """Show running configuration. Returns: - str: Running configuration. + (str): Running configuration. """ log.debug("Host %s: Copy running config with name %s.", self.host, filename) self.show(f"copy running-config {filename}") @@ -535,6 +538,7 @@ def set_boot_options(self, image_name, **vendor_specifics): Args: image_name (str): Name of the image file. + vendor_specifics (dict): Vendor specific options, such as file_system. Raises: NTCFileNotFoundError: File not found on device. @@ -597,7 +601,7 @@ def startup_config(self): """Get startup configuration. Returns: - str: Startup configuration. + (str): Startup configuration. """ log.debug("Host %s: show startup-config", self.host) return self.show("show startup-config", raw_text=True) diff --git a/pyntc/devices/f5_device.py b/pyntc/devices/f5_device.py index 06da359c..c59d8015 100644 --- a/pyntc/devices/f5_device.py +++ b/pyntc/devices/f5_device.py @@ -28,6 +28,7 @@ def __init__(self, host, username, password, **kwargs): # noqa: D403 host (str): The address of the network device. username (str): The username to authenticate with the device. password (str): The password to authenticate with the device. + kwargs (dict): Additional keyword arguments. """ super().__init__(host, username, password, device_type="f5_tmos_icontrol") @@ -63,7 +64,7 @@ def _check_md5sum(self, filename, checksum): checksum (str): checksum used against image. Returns: - bool: True if md5 matches. Otherwise, false. + (bool): True if md5 matches. Otherwise, false. """ md5sum = self._file_copy_remote_md5(filename) @@ -80,7 +81,7 @@ def _file_copy_local_file_exists(filepath): def _file_copy_local_md5(self, filepath, blocksize=2**20): if self._file_copy_local_file_exists(filepath): - md5_check = hashlib.md5() # nosec + md5_check = hashlib.md5() # noqa: S324 with open(filepath, "rb") as file_name: buf = file_name.read(blocksize) while buf: @@ -101,7 +102,7 @@ def _get_active_volume(self): """Get name of active volume on the device. Returns: - str: Name of active volume. + (str): Name of active volume. """ volumes = self._get_volumes() for _volume in volumes: @@ -118,7 +119,7 @@ def _get_free_space(self): >>> "vg-db-sda" 30.98 GB [23.89 GB used / 7.10 GB free] Returns: - int: Number of gigabytes of free space. + (int): Number of gigabytes of free space. """ free_space = None free_space_output = self.api_handler.tm.util.bash.exec_cmd("run", utilCmdArgs='-c "vgdisplay -s --units G"') @@ -184,9 +185,10 @@ def _image_booted(self, image_name, **vendor_specifics): Args: image_name (str): Name of image. + vendor_specifics (dict): Vendor specific arguments. Returns: - bool: True if booted volume is equal to active volume. Otherwise, false. + (bool): True if booted volume is equal to active volume. Otherwise, false. """ volume = vendor_specifics.get("volume") log.debug("Host %s: Checking if image %s has been booted.", self.host, image_name) @@ -199,7 +201,7 @@ def _image_exists(self, image_name): image_name (str): Name of image. Returns: - bool: True if image exists on device. Otherwise, false. + (bool): True if image exists on device. Otherwise, false. """ all_images_output = self.api_handler.tm.util.unix_ls.exec_cmd("run", utilCmdArgs="/shared/images") @@ -241,7 +243,7 @@ def _image_match(self, image_name, checksum): checksum (str): Expected checksum. Returns: - bool: True if expected checksum matches file checksum. Otherwise, false. + (bool): True if expected checksum matches file checksum. Otherwise, false. """ if self._image_exists(image_name): image = os.path.join("/shared/images", image_name) @@ -299,12 +301,12 @@ def _upload_image(self, image_filepath): headers["Content-Range"] = content_range # pylint: disable=missing-timeout # TODO Add timeout to requests.post, missing timeout can cause the method to hang indefinitely - requests.post( + requests.post( # noqa: S113 upload_uri, auth=(self.username, self.password), data=payload, headers=headers, - verify=False, # nosec + verify=False, # noqa: S501 ) start += len(payload) @@ -319,7 +321,7 @@ def _uptime_to_string(uptime): uptime (float): Uptime represented in a float. Returns: - str: Uptime in a string. + (str): Uptime in a string. """ days = uptime / (24 * 60 * 60) uptime = uptime % (24 * 60 * 60) @@ -338,7 +340,7 @@ def _volume_exists(self, volume_name): volume_name (str): Volume name. Returns: - bool: True if volume exists. Otherwise, false. + (bool): True if volume exists. Otherwise, false. """ result = self.api_handler.tm.sys.software.volumes.volume.exists(name=volume_name) @@ -353,7 +355,7 @@ def _wait_for_device_reboot(self, volume_name, timeout=600): timeout (int, optional): Timeout value. Defaults to 600. Returns: - bool: True if device boots into specified voluem successfully. Otherwise, false. + (bool): True if device boots into specified voluem successfully. Otherwise, false. """ end_time = time.time() + timeout time.sleep(60) @@ -415,7 +417,7 @@ def boot_options(self): """Get active volume. Returns: - dict: Key is ``active volume`` with value being the current active volume. + (dict): Key is ``active volume`` with value being the current active volume. """ active_volume = self._get_active_volume() @@ -453,7 +455,7 @@ def uptime(self): """Get uptime of device in seconds. Returns: - float: Uptime of device. + (float): Uptime of device. """ if self._uptime is None: self._uptime = self._get_uptime() @@ -467,7 +469,7 @@ def uptime_string(self): Get uptime of device in format dd:hh:mm:ss. Returns: - str: Uptime of device. + (str): Uptime of device. """ if self._uptime_string is None: self._uptime_string = self._uptime_to_string(self._get_uptime()) @@ -479,7 +481,7 @@ def hostname(self): """Get hostname of device. Returns: - str: Hostname. + (str): Hostname. """ if self._hostname is None: fqdn_split = self.fqdn.split(".") @@ -492,7 +494,7 @@ def interfaces(self): """Get list of images on the device. Returns: - list: List of images. + (list): List of images. """ if self._interfaces is None: self._interfaces = self._get_interfaces_list() @@ -504,7 +506,7 @@ def vlans(self): """Get list of vlans on device. Returns: - list: List of vlans. + (list): List of vlans. """ if self._vlans is None: self._vlans = self._get_vlans() @@ -516,7 +518,7 @@ def fqdn(self): """Get fully-qualified domain name. Returns: - str: Fully qualified domain name. + (str): Fully qualified domain name. """ if self._fqdn is None: settings = self.api_handler.tm.sys.global_settings.load() @@ -529,7 +531,7 @@ def model(self): """Get model of device. Returns: - str: Model of device. + (str): Model of device. """ if self._model is None: self._model = self._get_model() @@ -541,7 +543,7 @@ def os_version(self): """Get version of device. Returns: - str: Version on device. + (str): Version on device. """ if self._os_version is None: self._os_version = self._get_version() @@ -553,7 +555,7 @@ def serial_number(self): """Get serial number of device. Returns: - str: Serial number of device. + (str): Serial number of device. """ if self._serial_number is None: self._serial_number = self._get_serial_number() @@ -566,6 +568,7 @@ def file_copy(self, src, dest=None, **kwargs): Args: src (str): Source of file. dest (str, optional): Destination to save file. Defaults to None. + kwargs (dict): Additional keyword arguments. Raises: FileTransferError: Error in verifying if file existed before transfer. @@ -590,12 +593,13 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): Args: src (str): Source of file. dest (str, optional): Destination to save file. Defaults to None. + kwargs (dict): Additional keyword arguments. Raises: NotImplementedError: Destination must be ``/shared/images``. Returns: - bool: True if image specified exists on device. Otherwise, false. + (bool): True if image specified exists on device. Otherwise, false. """ if dest and not dest.startswith("/shared/images"): log.error("Host %s: Support only for images - destination is always /shared/images.", self.host) @@ -607,7 +611,7 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): if not self._image_match(image_name=file_basename, checksum=local_md5sum): log.debug("Host %s: File %s does not already exist on remote.", self.host, src) return False - log.debug("Host %s: File %s already exists on remote.", self.host) + log.debug("Host %s: File %s already exists on remote.", self.host, src) return True def image_installed(self, image_name, volume): @@ -621,7 +625,7 @@ def image_installed(self, image_name, volume): RuntimeError: Either image name or volume were not specified. Returns: - bool: True if file exists on volume. Otherwise, false. + (bool): True if file exists on volume. Otherwise, false. """ if not image_name or not volume: raise RuntimeError("image_name and volume must be specified") @@ -655,12 +659,13 @@ def install_os(self, image_name, **vendor_specifics): Args: image_name (str): Image name. + vendor_specifics (dict): Vendor specific arguments. Raises: NTCFileNotFoundError: Error is image is not found on device. Returns: - bool: True if image is installed successfully. Otherwise, false. + (bool): True if image is installed successfully. Otherwise, false. """ volume = vendor_specifics.get("volume") if not self.image_installed(image_name, volume): @@ -687,7 +692,8 @@ def reboot(self, wait_for_reload=False, volume=None, **kwargs): Args: volume (str, optional): Active volume to reboot. Defaults to None. - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + kwargs (dict): Additional keyword arguments. Raises: RuntimeError: If device is unreachable after timeout period, raise an error. @@ -747,6 +753,7 @@ def set_boot_options(self, image_name, **vendor_specifics): Args: image_name (str): Name of image. + vendor_specifics (dict): Vendor specific arguments. Raises: NTCFileNotFoundError: Error if file is not found on device. diff --git a/pyntc/devices/ios_device.py b/pyntc/devices/ios_device.py index d2f150fd..f529ff85 100644 --- a/pyntc/devices/ios_device.py +++ b/pyntc/devices/ios_device.py @@ -8,7 +8,7 @@ from netmiko.exceptions import ReadTimeout from pyntc import log -from pyntc.devices.base_device import BaseDevice, fix_docs, RollbackError +from pyntc.devices.base_device import BaseDevice, RollbackError, fix_docs from pyntc.errors import ( CommandError, CommandListError, @@ -22,6 +22,7 @@ SocketClosedError, ) from pyntc.utils import get_structured_data +from pyntc.utils.models import FileCopyModel BASIC_FACTS_KM = {"model": "hardware", "os_version": "version", "serial_number": "serial", "hostname": "hostname"} RE_SHOW_REDUNDANCY = re.compile( @@ -43,6 +44,7 @@ class IOSDevice(BaseDevice): vendor = "cisco" active_redundancy_states = {None, "active"} + # pylint: disable=too-many-arguments, too-many-positional-arguments def __init__( # nosec self, host, username, password, secret="", port=None, confirm_active=True, **kwargs ): # noqa: D403 @@ -56,6 +58,7 @@ def __init__( # nosec secret (str): The password to escalate privilege on the device. port (int): The port to use to establish the connection. Defaults to 22. confirm_active (bool): Determines if device's high availability state should be validated before leaving connection open. + kwargs (dict): Additional arguments to pass to the Netmiko ConnectHandler. """ super().__init__(host, username, password, device_type="cisco_ios_ssh") @@ -73,6 +76,7 @@ def _check_command_output_for_errors(self, command, command_response): Args: command (str): The command that was sent to the device. + command_response (str): The response from the device after sending ``command``. Raises: CommandError: When ``command_response`` reports an error in sending ``command``. @@ -102,6 +106,7 @@ def _enter_config(self): log.debug("Host %s: Device entered config mode.", self.host) def _file_copy_instance(self, src, dest=None, file_system="flash:"): + """Create a FileTransfer instance for copying a file to the device.""" if dest is None: dest = os.path.basename(src) @@ -113,7 +118,7 @@ def _get_file_system(self): """Determine the default file system or directory for device. Returns: - str: The name of the default file system or directory for the device. + (str): The name of the default file system or directory for the device. Raises: FileSystemNotFound: When the module is unable to determine the default file system. @@ -263,7 +268,7 @@ def boot_options(self): """Get current boot image. Returns: - dict: Key ``sys`` with value being the current boot image. + (dict): Key ``sys`` with value being the current boot image. """ boot_path_regex = r"(?:BOOT variable\s+=\s+(\S+)\s*$|BOOT path-list\s+:\s*(\S+)\s*$)" try: @@ -325,11 +330,11 @@ def config(self, command, **netmiko_args): Args: command (str|list): The command or commands to send to the device. - **netmiko_args: Any argument supported by ``netmiko.ConnectHandler.send_config_set``. + **netmiko_args (dict): Any argument supported by ``netmiko.ConnectHandler.send_config_set``. Returns: - str: When ``command`` is a str, the config session input and output from sending ``command``. - list: When ``command`` is a list, the config session input and output from sending ``command``. + (str): When ``command`` is a str, the config session input and output from sending ``command``. + (list): When ``command`` is a list, the config session input and output from sending ``command``. Raises: TypeError: When sending an argument in ``**netmiko_args`` that is not supported. @@ -399,7 +404,7 @@ def confirm_is_active(self): Confirm that the device is either standalone or the active device in a high availability cluster. Returns: - bool: True when the device is considered active. + (bool): True when the device is considered active. Rasies: DeviceNotActiveError: When the device is not considered the active device. @@ -442,7 +447,7 @@ def connected(self): # noqa: D401 Get connection status of the device. Returns: - bool: True if the device is connected, else False. + (bool): True if the device is connected, else False. """ return self._connected @@ -454,7 +459,7 @@ def enable(self): """Ensure device is in enable mode. Returns: - None: Device prompt is set to enable mode. + (None): Device prompt is set to enable mode. """ # Netmiko reports enable and config mode as being enabled if not self.native.check_enable_mode(): @@ -470,7 +475,7 @@ def uptime(self): """Get uptime from device. Returns: - int: Uptime in seconds. + (int): Uptime in seconds. """ if self._uptime is None: version_data = self._raw_version_data() @@ -485,7 +490,7 @@ def uptime_string(self): """Get uptime in format dd:hh:mm. Returns: - str: Uptime of device. + (str): Uptime of device. """ if self._uptime_string is None: version_data = self._raw_version_data() @@ -499,7 +504,7 @@ def hostname(self): """Get hostname of device. Returns: - str: Hostname of device. + (str): Hostname of device. """ version_data = self._raw_version_data() if self._hostname is None: @@ -514,7 +519,7 @@ def interfaces(self): Get list of interfaces on device. Returns: - list: List of interfaces on device. + (list): List of interfaces on device. """ if self._interfaces is None: self._interfaces = list(x["intf"] for x in self._interfaces_detailed_list()) @@ -528,7 +533,7 @@ def vlans(self): Get list of VLANs on device. Returns: - list: List of VLANs on device. + (list): List of VLANs on device. """ if self._vlans is None: if self.model.startswith("WS"): @@ -544,7 +549,7 @@ def fqdn(self): """Get fully qualified domain name. Returns: - str: Fully qualified domain name or ``N/A`` if not defined. + (str): Fully qualified domain name or ``N/A`` if not defined. """ if self._fqdn is None: self._fqdn = "N/A" @@ -557,7 +562,7 @@ def model(self): """Get the device model. Returns: - str: Device model. + (str): Device model. """ version_data = self._raw_version_data() if self._model is None: @@ -571,7 +576,7 @@ def os_version(self): """Get os version on device. Returns: - str: OS version on device. + (str): OS version on device. """ version_data = self._raw_version_data() if self._os_version is None: @@ -585,7 +590,7 @@ def serial_number(self): """Get serial number of device. Returns: - str: Serial number of device. + (str): Serial number of device. """ version_data = self._raw_version_data() if self._serial_number is None: @@ -599,7 +604,7 @@ def config_register(self): """Get config register of device. Returns: - str: Config register. + (str): Config register. """ # ios-specific facts version_data = self._raw_version_data() @@ -608,6 +613,101 @@ def config_register(self): log.debug("Host %s: Config register %s", self.host, self._config_register) return self._config_register + def get_remote_checksum(self, filename, hashing_algorithm="md5", file_system=None): + """Get the checksum of a remote file. + + Args: + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use. Valid choices are "md5" and "sha512" (default: "md5"). + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (str): The checksum of the remote file. + + Raises: + ValueError: If an unsupported hashing algorithm is provided. + CommandError: If there is an error in executing the command to get the remote checksum. + """ + if hashing_algorithm not in {"md5", "sha512"}: + raise ValueError("hashing_algorithm must be either 'md5' or 'sha512' for Cisco IOS devices.") + if file_system is None: + file_system = self._get_file_system() + cmd = f"verify /{hashing_algorithm} {file_system}/{filename}" + result = self.native.send_command_timing(cmd, read_timeout=300) + + patterns = [r"=\s+(\S+)", r"^([a-fA-F0-9]+)$"] + for pattern in patterns: + if match := re.search(pattern, result): + log.debug( + "Host %s: Remote checksum for file %s with hashing algorithm %s is %s.", + self.host, + filename, + hashing_algorithm, + match[1], + ) + return match[1] + log.error( + "Host %s: Unable to get remote checksum for file %s with hashing algorithm %s", + self.host, + filename, + hashing_algorithm, + ) + raise CommandError( + cmd, f"Unable to get remote checksum for file {filename} with hashing algorithm {hashing_algorithm}" + ) + + def check_file_exists(self, filename, file_system=None): + """Check if a remote file exists by filename. + + Args: + filename (str): The name of the file to check for on the remote device. + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (bool): True if the remote file exists, False if it doesn't. + + Raises: + CommandError: If there is an error in executing the command to check if the file exists. + """ + cmd = f"dir {file_system or self._get_file_system()}/{filename}" + result = self.native.send_command(cmd, read_timeout=30) + log.debug( + "Host %s: Checking if file %s exists on remote with command '%s' and result: %s", + self.host, + filename, + cmd, + result, + ) + if re.search(r"No such file|No files found|Path does not exist|Error opening", result): + log.debug("Host %s: File %s does not exist on remote.", self.host, filename) + return False + if re.search(rf"Directory of .*{filename}", result): + log.debug("Host %s: File %s exists on remote.", self.host, filename) + return True + raise CommandError(cmd, f"Unable to determine if file {filename} exists on remote: {result}") + + def verify_file(self, checksum, filename, hashing_algorithm="md5", file_system=None): + """Verify a file on the remote device by and validate the checksums. + + Args: + checksum (str): The checksum of the file. + filename (str): The name of the file to check for on the remote device. + hashing_algorithm (str): The hashing algorithm to use (default: "md5"). + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Returns: + (bool): True if the file is verified successfully, False otherwise. + """ + return self.check_file_exists(filename, file_system=file_system) and self.compare_file_checksum( + checksum, filename, hashing_algorithm, file_system=file_system + ) + def file_copy(self, src, dest=None, file_system=None): """Copy file to device. @@ -625,7 +725,11 @@ def file_copy(self, src, dest=None, file_system=None): if file_system is None: file_system = self._get_file_system() - if not self.file_copy_remote_exists(src, dest, file_system): + dest = dest or os.path.basename(src) + local_checksum = self.get_local_checksum(src) + log.debug("Host %s: Local checksum for file %s is %s.", self.host, src, local_checksum) + + if not self.verify_file(local_checksum, dest, file_system=file_system): file_copy = self._file_copy_instance(src, dest, file_system=file_system) # if not self.fc.verify_space_available(): # raise FileTransferError('Not enough space available.') @@ -639,7 +743,7 @@ def file_copy(self, src, dest=None, file_system=None): # compare hashes if not file_copy.compare_md5(): log.error("Host %s: Socket closed error %s", self.host, error) - raise SocketClosedError(message=error) + raise SocketClosedError(message=error) from error log.error("Host %s: OS error %s", self.host, error) except: # noqa E722 log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) @@ -650,7 +754,7 @@ def file_copy(self, src, dest=None, file_system=None): # Ensure connection to device is still open after long transfers self.open() - if not self.file_copy_remote_exists(src, dest, file_system): + if not self.verify_file(local_checksum, dest, file_system=file_system): log.error( "Host %s: Attempted file copy, but could not validate file existed after transfer %s", self.host, @@ -658,9 +762,82 @@ def file_copy(self, src, dest=None, file_system=None): ) raise FileTransferError + def remote_file_copy(self, src: FileCopyModel, dest=None, file_system=None, **kwargs): + """Copy a file to a remote device. + + Args: + src (FileCopyModel): The source file model. + dest (str): The destination file path on the remote device. + kwargs (dict): Additional keyword arguments that may be used by subclasses. + + Keyword Args: + file_system (str): Supported only for IOS and NXOS. The file system for the + remote file. If no file_system is provided, then the ``get_file_system`` + method is used to determine the correct file system to use. + + Raises: + TypeError: If src is not an instance of FileCopyModel. + FileTransferError: If there is an error during file transfer or if the file cannot be verified after transfer. + """ + if not isinstance(src, FileCopyModel): + raise TypeError("src must be an instance of FileCopyModel") + if file_system is None: + file_system = self._get_file_system() + if dest is None: + dest = src.file_name + if not self.verify_file(src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system): + current_prompt = self.native.find_prompt() + + # Define prompt mapping for expected prompts during file copy + prompt_answers = { + r"Password": src.token, + r"Source username": src.username, + r"yes/no|Are you sure you want to continue connecting": "yes", + r"(confirm|Address or name of remote host|Source filename|Destination filename)": "", # Press Enter + } + keys = list(prompt_answers.keys()) + [re.escape(current_prompt)] + expect_regex = f"({'|'.join(keys)})" + + command = f"copy {src.clean_url} {file_system}{dest}" + if src.vrf and src.scheme not in {"http", "https"}: + command = f"{command} vrf {src.vrf}" + + # _send_command currently checks for % and raises an error, but during the file copy + # there may be a % warning that does not indicate a failure so we will use send_command directly. + output = self.native.send_command(command, expect_string=expect_regex, read_timeout=src.timeout) + + while current_prompt not in output: + # Check for success message in output to break loop and avoid waiting for next prompt + if re.search(r"Copy complete|bytes copied in|File transfer successful", output, re.IGNORECASE): + log.info( + "Host %s: File %s transferred successfully with output: %s", self.host, src.file_name, output + ) + break + # Check for errors explicitly to avoid infinite loops on failure + if re.search(r"(Error|Invalid|Failed|Aborted|denied)", output, re.IGNORECASE): + log.error("Host %s: File transfer error %s", self.host, FileTransferError.default_message) + raise FileTransferError + for prompt, answer in prompt_answers.items(): + if re.search(prompt, output, re.IGNORECASE): + is_password = "Password" in prompt + output = self.native.send_command( + answer, expect_string=expect_regex, read_timeout=src.timeout, cmd_verify=not is_password + ) + break # Exit the for loop and check the new output for the next prompt + + if not self.verify_file( + src.checksum, dest, hashing_algorithm=src.hashing_algorithm, file_system=file_system + ): + log.error( + "Host %s: Attempted remote file copy, but could not validate file existed after transfer %s", + self.host, + FileTransferError.default_message, + ) + raise FileTransferError + # TODO: Make this an internal method since exposing file_copy should be sufficient def file_copy_remote_exists(self, src, dest=None, file_system=None): - """Copy file to device. + """Check if file exists on remote device. Args: src (str): Source of file. @@ -668,7 +845,7 @@ def file_copy_remote_exists(self, src, dest=None, file_system=None): file_system (str, optional): File system to copy file to. Defaults to None. Returns: - bool: True if file copied succesfully and md5 hashes match. Otherwise, false. + (bool): True if file copied succesfully and md5 hashes match. Otherwise, false. """ self.enable() if file_system is None: @@ -689,12 +866,13 @@ def install_os(self, image_name, install_mode=False, read_timeout=2000, **vendor image_name (str): Name of the IOS image to boot into install_mode (bool, optional): Uses newer install method on devices. Defaults to False. read_timeout (int, optional): Netmiko timeout when waiting for device prompt. Default 30. + vendor_specifics (dict, optional): Vendor specific arguments to pass to the install command. Raises: OSInstallError: Unable to install OS Error type Returns: - bool: False if no install is needed, true if the install completes successfully + (bool): False if no install is needed, true if the install completes successfully """ timeout = vendor_specifics.get("timeout", 3600) if not self._image_booted(image_name): @@ -718,7 +896,10 @@ def install_os(self, image_name, install_mode=False, read_timeout=2000, **vendor ) # Set a higher read_timeout and send it in try: - self.show(command, read_timeout=read_timeout) + install_message = self.show(command, read_timeout=read_timeout) + if install_message.startswith("FAILED:"): + log.error("Host %s: OS install error for image %s", self.host, image_name) + raise OSInstallError(hostname=self.hostname, desired_boot=image_name) except IOError: log.error("Host %s: IO error for image %s", self.host, image_name) except CommandError: @@ -752,7 +933,7 @@ def is_active(self): Determine if the current processor is the active processor. Returns: - bool: True if the processor is active or does not support HA, else False. + (bool): True if the processor is active or does not support HA, else False. Example: >>> device = IOSDevice(**connection_args) @@ -819,8 +1000,8 @@ def peer_redundancy_state(self): Determine the current redundancy state of the peer processor. Returns: - str: The redundancy state of the peer processor. - None: When the processor does not support redundancy. + (str): The redundancy state of the peer processor. + (None): When the processor does not support redundancy. Example: >>> device = IOSDevice(**connection_args) @@ -850,7 +1031,8 @@ def reboot(self, wait_for_reload=False, **kwargs): Reload the controller or controller pair. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + kwargs (dict): Additional arguments to pass to the Netmiko. Raises: ReloadTimeoutError: When the device is still unreachable after the timeout period. @@ -882,7 +1064,7 @@ def redundancy_mode(self): Get operating redundancy mode of the device. Returns: - str: The redundancy mode the device is operating in. + (str): The redundancy mode the device is operating in. If the command is not supported, then "n/a" is returned. Example: @@ -909,8 +1091,8 @@ def redundancy_state(self): Determine the current redundancy state of the processor. Returns: - str: The redundancy state of the current processor. - None: When the processor does not support redundancy. + (str): The redundancy state of the current processor. + (None): When the processor does not support redundancy. Example: >>> device = IOSDevice(**connection_args) @@ -952,7 +1134,7 @@ def running_config(self): """Get running configuration. Returns: - str: Output of ``show running-config``. + (str): Output of ``show running-config``. """ log.debug("Host %s: Show running config.", self.host) return self.show("show running-config") @@ -964,7 +1146,7 @@ def save(self, filename="startup-config"): filename (str, optional): Name of file to save running configuration. Defaults to "startup-config". Returns: - bool: True if save is succesfull. + (bool): True if save is succesfull. """ command = f"copy running-config {filename}" # Changed to send_command_timing to not require a direct prompt return. @@ -983,6 +1165,7 @@ def set_boot_options(self, image_name, **vendor_specifics): Args: image_name (str): Name of image to set as boot variable. + vendor_specifics (dict, optional): Vendor specific arguments to pass to the set_boot_options command. Raises: NTCFileNotFoundError: Error if file is not found on device. @@ -1055,9 +1238,10 @@ def show(self, command, expect_string=None, **netmiko_args): Args: command (str): Command to be ran. expect_string (str, optional): Expected string from command output. Defaults to None. + netmiko_args (dict): Additional arguments to pass to Netmiko's send_command method. Returns: - str: Output of command. + (str): Output of command. """ self.enable() if isinstance(command, list): @@ -1078,7 +1262,7 @@ def startup_config(self): """Get startup configuration. Returns: - str: Startup configuration from device. + (str): Startup configuration from device. """ log.debug("Host %s: Successfully executed command 'show startup-config'.", self.host) return self.show("show startup-config") diff --git a/pyntc/devices/iosxewlc_device.py b/pyntc/devices/iosxewlc_device.py index d33f45b4..8825f3e3 100644 --- a/pyntc/devices/iosxewlc_device.py +++ b/pyntc/devices/iosxewlc_device.py @@ -45,12 +45,14 @@ def install_os(self, image_name, read_timeout=2000, **vendor_specifics): Args: image_name (str): Name of the IOS image to boot into + read_timeout (int): Timeout for reading the output of the command. + vendor_specifics (dict): Vendor specific options. Raises: OSInstallError: Unable to install OS Error type Returns: - bool: False if no install is needed, true if the install completes successfully + (bool): False if no install is needed, true if the install completes successfully """ timeout = vendor_specifics.get("timeout", 5400) if not self._image_booted(image_name): @@ -90,9 +92,10 @@ def show(self, command, expect_string=None, **netmiko_args): Args: command (str): Command to be ran. expect_string (str, optional): Expected string from command output. Defaults to None. + netmiko_args (dict): Additional arguments to pass to Netmiko's send_command method. Returns: - str: Output of command. + (str): Output of command. """ self.enable() log.debug("Host %s: Successfully executed command 'show'.", self.host) diff --git a/pyntc/devices/jnpr_device.py b/pyntc/devices/jnpr_device.py index 2d87329d..660d1c39 100644 --- a/pyntc/devices/jnpr_device.py +++ b/pyntc/devices/jnpr_device.py @@ -33,6 +33,8 @@ def __init__(self, host, username, password, *args, **kwargs): # noqa: D403 host (str): The address of the network device. username (str): The username to authenticate with the device. password (str): The password to authenticate with the device. + args (tuple): Additional positional arguments to pass to the device. + kwargs (dict): Additional keyword arguments to pass to the device. """ super().__init__(host, username, password, *args, device_type="juniper_junos_netconf", **kwargs) @@ -47,7 +49,7 @@ def _file_copy_local_file_exists(self, filepath): def _file_copy_local_md5(self, filepath, blocksize=2**20): if self._file_copy_local_file_exists(filepath): - md5_hash = hashlib.md5() # nosec + md5_hash = hashlib.md5() # noqa: S324 with open(filepath, "rb") as file_name: buf = file_name.read(blocksize) while buf: @@ -124,7 +126,7 @@ def boot_options(self): """Get os version on device. Returns: - str: OS version on device. + (str): OS version on device. """ return self.os_version @@ -146,6 +148,7 @@ def config(self, commands, format_type="set"): Args: commands (str, list): String with single command, or list with multiple commands. + format_type (str, optional): Format type for the command. Defaults to "set". Raises: ConfigLoadError: Issue with loading the command. @@ -172,7 +175,7 @@ def connected(self): """Get connection status of device. Returns: - bool: True if connection is active. Otherwise, false. + (bool): True if connection is active. Otherwise, false. """ return self.native.connected @@ -181,7 +184,7 @@ def uptime(self): """Get device uptime in seconds. Returns: - int: Device uptime in seconds. + (int): Device uptime in seconds. """ try: native_uptime_string = self.native.facts["RE0"]["up_time"] @@ -200,7 +203,7 @@ def uptime_string(self): Get device uptime in format dd:hh:mm:ss. Returns: - str: Device uptime. + (str): Device uptime. """ try: native_uptime_string = self.native.facts["RE0"]["up_time"] @@ -217,7 +220,7 @@ def hostname(self): """Get device hostname. Returns: - str: Device hostname. + (str): Device hostname. """ if self._hostname is None: self._hostname = self.native.facts.get("hostname") @@ -229,7 +232,7 @@ def interfaces(self): """Get list of interfaces. Returns: - list: List of interfaces. + (list): List of interfaces. """ if self._interfaces is None: self._interfaces = self._get_interfaces() @@ -241,7 +244,7 @@ def fqdn(self): """Get fully qualified domain name. Returns: - str: Fully qualified domain name. + (str): Fully qualified domain name. """ if self._fqdn is None: self._fqdn = self.native.facts.get("fqdn") @@ -253,7 +256,7 @@ def model(self): """Get device model. Returns: - str: Device model. + (str): Device model. """ if self._model is None: self._model = self.native.facts.get("model") @@ -265,7 +268,7 @@ def os_version(self): """Get OS version. Returns: - str: OS version. + (str): OS version. """ if self._os_version is None: self._os_version = self.native.facts.get("version") @@ -277,7 +280,7 @@ def serial_number(self): """Get serial number. Returns: - str: Serial number. + (str): Serial number. """ if self._serial_number is None: self._serial_number = self.native.facts.get("serialnumber") @@ -290,6 +293,7 @@ def file_copy(self, src, dest=None, **kwargs): Args: src (str): Name of file to be transferred. dest (str, optional): Path on device to save file. Defaults to None. + kwargs (dict): Additional keyword arguments to pass to the `file_copy` command. Raises: FileTransferError: Raised when unable to verify file was transferred succesfully. @@ -313,9 +317,10 @@ def file_copy_remote_exists(self, src, dest=None, **kwargs): Args: src (str): Source of local file. dest (str, optional): Path of file on device. Defaults to None. + kwargs (dict): Additional keyword arguments to pass to the `file_copy` command. Returns: - bool: True if hashes of the file match. Otherwise, false. + (bool): True if hashes of the file match. Otherwise, false. """ if dest is None: dest = os.path.basename(src) @@ -331,6 +336,7 @@ def install_os(self, image_name, **vendor_specifics): Args: image_name (str): Name of image. + vendor_specifics (dict): Vendor specific options. Raises: NotImplementedError: Method currently not implemented. @@ -347,7 +353,8 @@ def reboot(self, wait_for_reload=False, **kwargs): Reload the controller or controller pair. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + kwargs (dict): Additional keyword arguments to pass to the `reboot` command. Example: >>> device = JunosDevice(**connection_args) @@ -388,7 +395,7 @@ def running_config(self): """Get running configuration. Returns: - str: Running configuration. + (str): Running configuration. """ return self.show("show config") @@ -402,7 +409,7 @@ def save(self, filename=None): filename (str, optional): Filename to save current configuration. Defaults to None. Returns: - bool: True if new file created for save file. Otherwise, just returns if save is to default name. + (bool): True if new file created for save file. Otherwise, just returns if save is to default name. """ if filename is None: self.cu.commit() @@ -460,6 +467,6 @@ def startup_config(self): """Get startup configuration. Returns: - str: Startup configuration. + (str): Startup configuration. """ return self.show("show config") diff --git a/pyntc/devices/nxos_device.py b/pyntc/devices/nxos_device.py index 0e1aefa9..1517c28d 100644 --- a/pyntc/devices/nxos_device.py +++ b/pyntc/devices/nxos_device.py @@ -7,11 +7,10 @@ from pynxos.device import Device as NXOSNative from pynxos.errors import CLIError from pynxos.features.file_copy import FileTransferError as NXOSFileTransferError - -from requests.exceptions import ReadTimeout, ConnectTimeout +from requests.exceptions import ConnectTimeout, ReadTimeout from pyntc import log -from pyntc.devices.base_device import BaseDevice, fix_docs, RollbackError +from pyntc.devices.base_device import BaseDevice, RollbackError, fix_docs from pyntc.errors import ( CommandError, CommandListError, @@ -28,9 +27,8 @@ class NXOSDevice(BaseDevice): vendor = "cisco" - def __init__( - self, host, username, password, transport="http", timeout=30, port=None, verify=True, **kwargs - ): # noqa: D403 + # pylint: disable=too-many-arguments, too-many-positional-arguments + def __init__(self, host, username, password, transport="http", timeout=30, port=None, verify=True, **kwargs): # noqa: D403 """PyNTC Device implementation for Cisco IOS. Args: @@ -41,7 +39,7 @@ def __init__( timeout (int, optional): Timeout in seconds. Defaults to 30. port (int, optional): Port used to connect to device. Defaults to None. verify (bool, optional): SSL verification. - kwargs: Left for compatibility with other tools, for instance nautobot-inventory may pass additional kwargs. + kwargs (dict): Left for compatibility with other tools, for instance nautobot-inventory may pass additional kwargs. """ super().__init__(host, username, password, device_type="cisco_nxos_nxapi") @@ -91,7 +89,7 @@ def boot_options(self): """Get current boot variables. Returns: - dict: e.g . {"kick": "router_kick.img", "sys": "router_sys.img"} + (dict): e.g . {"kick": "router_kick.img", "sys": "router_sys.img"} """ boot_options = self.native.get_boot_options() log.debug("Host %s: the boot options are %s", self.host, boot_options) @@ -139,7 +137,7 @@ def uptime(self): """Get uptime of the device in seconds. Returns: - int: Uptime of the device in seconds. + (int): Uptime of the device in seconds. """ if self._uptime is None: self._uptime = self.native.facts.get("uptime") @@ -152,7 +150,7 @@ def uptime_string(self): """Get uptime in format dd:hh:mm. Returns: - str: Uptime of device. + (str): Uptime of device. """ if self._uptime_string is None: self._uptime_string = self.native.facts.get("uptime_string") @@ -164,7 +162,7 @@ def hostname(self): """Get hostname of the device. Returns: - str: Hostname of the device. + (str): Hostname of the device. """ if self._hostname is None: self._hostname = self.native.facts.get("hostname") @@ -177,7 +175,7 @@ def interfaces(self): """Get list of interfaces. Returns: - list: List of interfaces. + (list): List of interfaces. """ if self._interfaces is None: self._interfaces = self.native.facts.get("interfaces") @@ -190,7 +188,7 @@ def vlans(self): """Get list of vlans. Returns: - list: List of vlans on the device. + (list): List of vlans on the device. """ if self._vlans is None: self._vlans = self.native.facts.get("vlans") @@ -203,7 +201,7 @@ def fqdn(self): """Get fully qualified domain name. Returns: - str: Fully qualified domain name. + (str): Fully qualified domain name. """ if self._fqdn is None: self._fqdn = self.native.facts.get("fqdn") @@ -216,7 +214,7 @@ def model(self): """Get device model. Returns: - str: Model of device. + (str): Model of device. """ if self._model is None: self._model = self.native.facts.get("model") @@ -229,7 +227,7 @@ def os_version(self): """Get device version. Returns: - str: Device version. + (str): Device version. """ if self._os_version is None: self._os_version = self.native.facts.get("os_version") @@ -242,7 +240,7 @@ def serial_number(self): """Get device serial number. Returns: - str: Device serial number. + (str): Device serial number. """ if self._serial_number is None: self._serial_number = self.native.facts.get("serial_number") @@ -291,7 +289,7 @@ def file_copy_remote_exists(self, src, dest=None, file_system="bootflash:"): file_system (str, optional): The file system for the remote file. Defaults to "bootflash:". Returns: - bool: True if the remote file exists. Otherwise, false. + (bool): True if the remote file exists. Otherwise, false. """ dest = dest or os.path.basename(src) log.debug( @@ -307,12 +305,13 @@ def install_os(self, image_name, **vendor_specifics): Args: image_name (str): Name of the image file to upgrade the device to. + vendor_specifics (dict): Vendor specific options. Raises: OSInstallError: Error if boot option is not set to new image. Returns: - bool: True if new image is boot option on device. Otherwise, false. + (bool): True if new image is boot option on device. Otherwise, false. """ self.native.show("terminal dont-ask") timeout = vendor_specifics.get("timeout", 3600) @@ -339,7 +338,8 @@ def reboot(self, wait_for_reload=False, **kwargs): Reload the controller or controller pair. Args: - wait_for_reload: Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + wait_for_reload (bool): Whether or not reboot method should also run _wait_for_device_reboot(). Defaults to False. + kwargs (dict): Additional arguments to pass to reboot method. Raises: RebootTimerError: When the device is still unreachable after the timeout period. @@ -350,7 +350,8 @@ def reboot(self, wait_for_reload=False, **kwargs): >> """ if kwargs.get("confirm"): - log.warning("Passing 'confirm' to reboot method is deprecated.", DeprecationWarning) + log.warning("Passing 'confirm' to reboot method is deprecated.") + raise DeprecationWarning("Passing 'confirm' to reboot method is deprecated.") try: self.native.show_list(["terminal dont-ask", "reload"]) # The native reboot is not always properly disabling confirmation. Above is more consistent. @@ -384,7 +385,7 @@ def running_config(self): """Get running configuration of device. Returns: - str: Running configuration of device. + (str): Running configuration of device. """ log.debug("Host %s: Show running config.", self.host) return self.native.running_config @@ -396,7 +397,7 @@ def save(self, filename="startup-config"): filename (str, optional): Filename to save running configuration to. Defaults to "startup-config". Returns: - bool: True if configuration is saved. + (bool): True if configuration is saved. """ log.debug("Host %s: Copy running config with name %s.", self.host, filename) return self.native.save(filename=filename) @@ -407,6 +408,7 @@ def set_boot_options(self, image_name, kickstart=None, **vendor_specifics): Args: image_name (str): Main system image file. kickstart (str, optional): Kickstart filename. Defaults to None. + vendor_specifics (dict): Vendor specific options. Raises: NTCFileNotFoundError: Error if either image_name or kickstart image not found on device. @@ -454,7 +456,7 @@ def show(self, command, raw_text=False): CommandError: Error message stating which command failed. Returns: - str: Results of the command ran. + (str): Results of the command ran. """ log.debug("Host %s: Successfully executed command 'show' with responses.", self.host) if isinstance(command, list): @@ -476,6 +478,6 @@ def startup_config(self): """Get startup configuration. Returns: - str: Startup configuration. + (str): Startup configuration. """ return self.show("show startup-config", raw_text=True) diff --git a/pyntc/devices/system_features/__init__.py b/pyntc/devices/system_features/__init__.py index e69de29b..3952e5d2 100644 --- a/pyntc/devices/system_features/__init__.py +++ b/pyntc/devices/system_features/__init__.py @@ -0,0 +1 @@ +"""Initialize the system features module.""" diff --git a/pyntc/devices/system_features/vlans/__init__.py b/pyntc/devices/system_features/vlans/__init__.py index e69de29b..1dcf6f20 100644 --- a/pyntc/devices/system_features/vlans/__init__.py +++ b/pyntc/devices/system_features/vlans/__init__.py @@ -0,0 +1 @@ +"""Initialize the system features vlans module.""" diff --git a/pyntc/devices/system_features/vlans/base_vlans.py b/pyntc/devices/system_features/vlans/base_vlans.py index da31f5b4..19602cae 100644 --- a/pyntc/devices/system_features/vlans/base_vlans.py +++ b/pyntc/devices/system_features/vlans/base_vlans.py @@ -19,11 +19,7 @@ class BaseVlans(BaseFeature): class VlanNotInRangeError(NTCError): - """Vlan error. - - Args: - NTCError (str): Vlan range error. - """ + """Vlan error.""" def __init__(self, lower, upper): """Exception for vlan range validation. diff --git a/pyntc/devices/tables/__init__.py b/pyntc/devices/tables/__init__.py index e69de29b..cdb23760 100644 --- a/pyntc/devices/tables/__init__.py +++ b/pyntc/devices/tables/__init__.py @@ -0,0 +1 @@ +"""Initialization for tables.""" diff --git a/pyntc/devices/tables/jnpr/__init__.py b/pyntc/devices/tables/jnpr/__init__.py index e69de29b..10ac1bfb 100644 --- a/pyntc/devices/tables/jnpr/__init__.py +++ b/pyntc/devices/tables/jnpr/__init__.py @@ -0,0 +1 @@ +"""Initialization for Juniper device tables.""" diff --git a/pyntc/log.py b/pyntc/log.py index b2c035a6..15e31f53 100644 --- a/pyntc/log.py +++ b/pyntc/log.py @@ -1,11 +1,9 @@ """Logging utilities for Pyntc.""" -import os import logging - +import os from logging.handlers import RotatingFileHandler - APP = "pyntc" """ Application name, used as the logging root. """ @@ -23,7 +21,7 @@ def get_log(name=None): name (str, optional): Sublogger name. Defaults to None. Returns: - logger: Return a logger instance in the :data:`APP` namespace. + (logger): Return a logger instance in the :data:`APP` namespace. """ logger_name = f"{APP}.{name}" if name else APP # file handler @@ -41,7 +39,7 @@ def init(**kwargs): directly to the :func:`logging.basicConfig` call in turn. Args: - **kwargs: Arguments to pass for logging configuration + **kwargs (dict): Arguments to pass for logging configuration """ @@ -66,7 +64,7 @@ def logger(level): level (str): defines the log level (DEBUG, INFO, WARNING, ERROR, CRITICAL) Returns: - string: Returns logger. type of string. + (str): Returns logger. type of string. """ return getattr(get_log(), level) diff --git a/pyntc/utils/__init__.py b/pyntc/utils/__init__.py index eda608d2..d932ae63 100644 --- a/pyntc/utils/__init__.py +++ b/pyntc/utils/__init__.py @@ -1,7 +1,6 @@ """PyNTC Utilities.""" -from .templates import get_structured_data from .converters import convert_dict_by_key, convert_list_by_key, recursive_key_lookup - +from .templates import get_structured_data __all__ = ["get_structured_data", "convert_dict_by_key", "convert_list_by_key", "recursive_key_lookup"] diff --git a/pyntc/utils/converters.py b/pyntc/utils/converters.py index c729a1e9..61fd2b55 100644 --- a/pyntc/utils/converters.py +++ b/pyntc/utils/converters.py @@ -1,9 +1,7 @@ """Provides methods for manipulating and converting data.""" -def convert_dict_by_key( - original, key_map, fill_in=False, whitelist=[], blacklist=[] -): # pylint: disable=dangerous-default-value +def convert_dict_by_key(original, key_map, fill_in=False, whitelist=[], blacklist=[]): # pylint: disable=dangerous-default-value """Use a key map to convert a dictionary to desired keys. Args: @@ -11,13 +9,13 @@ def convert_dict_by_key( key_map (dict): Key map to use to convert dictionary. fill_in (dict): Whether the returned dictionary should contain keys and values from the original dictionary if not specified in the key map. - whitelist: If fill_in is True, and whitelist isn't empty, only fill in the keys + whitelist (list): If fill_in is True, and whitelist isn't empty, only fill in the keys in the whitelist in the returned dictionary. - blacklist: If fill_in is True, and blacklist isn't empty, fill in with all keys from + blacklist (list): If fill_in is True, and blacklist isn't empty, fill in with all keys from the original dictionary besides those in the blacklist. Returns: - A converted dictionary through the key map. + (dict): A converted dictionary through the key map. """ converted = {} for converted_key in key_map: @@ -43,9 +41,7 @@ def convert_dict_by_key( return converted -def convert_list_by_key( - original_list, key_map, fill_in=False, whitelist=[], blacklist=[] -): # pylint: disable=dangerous-default-value +def convert_list_by_key(original_list, key_map, fill_in=False, whitelist=[], blacklist=[]): # pylint: disable=dangerous-default-value """Apply a list conversion for all items in original_list. Args: @@ -53,13 +49,13 @@ def convert_list_by_key( key_map (dict): Key map to use to convert list. fill_in (dict): Whether the returned list should contain keys and values from the original dictionary if not specified in the key map. - whitelist: If fill_in is True, and whitelist isn't empty, only fill in the keys + whitelist (list): If fill_in is True, and whitelist isn't empty, only fill in the keys in the whitelist in the returned dictionary. - blacklist: If fill_in is True, and blacklist isn't empty, fill in with all keys from + blacklist (list): If fill_in is True, and blacklist isn't empty, fill in with all keys from the original dictionary besides those in the blacklist. Returns: - list: A converted list. + (list): A converted list. """ converted_list = [] for original in list(original_list): diff --git a/pyntc/utils/models.py b/pyntc/utils/models.py new file mode 100644 index 00000000..3d047a81 --- /dev/null +++ b/pyntc/utils/models.py @@ -0,0 +1,75 @@ +"""Data Models for Pyntc.""" + +from dataclasses import asdict, dataclass, field +from typing import Optional +from urllib.parse import urlparse + +# Use Hashing algorithms from Nautobot's supported list. +HASHING_ALGORITHMS = {"md5", "sha1", "sha224", "sha384", "sha256", "sha512", "sha3", "blake2", "blake3"} + + +@dataclass +class FileCopyModel: + """Data class to represent the specification for pulling a file from a URL to a network device. + + Args: + download_url (str): The URL to download the file from. Can include credentials, but it's recommended to use the username and token fields instead for security reasons. + checksum (str): The expected checksum of the file. + file_name (str): The name of the file to be saved on the device. + hashing_algorithm (str, optional): The hashing algorithm to use for checksum verification. Defaults to "md5". + timeout (int, optional): The timeout for the download operation in seconds. Defaults to 900. + file_size (int, optional): The expected size of the file in bytes. Optional but can be used for an additional layer of verification. + username (str, optional): The username for authentication if required by the URL. Optional if credentials are included in the URL. + token (str, optional): The password or token for authentication if required by the URL. Optional if credentials are included in the URL. + vrf (str, optional): The VRF to use for the download if the device supports VRFs. Optional. + ftp_passive (bool, optional): Whether to use passive mode for FTP downloads. Defaults to True. + """ + + download_url: str + checksum: str + file_name: str + hashing_algorithm: str = "md5" + timeout: int = 900 # Timeout for the download operation in seconds + file_size: Optional[int] = None # Size in bytes + username: Optional[str] = None + token: Optional[str] = None # Password/Token + vrf: Optional[str] = None + ftp_passive: bool = True + + # This field is calculated, so we don't pass it in the constructor + clean_url: str = field(init=False) + scheme: str = field(init=False) + + def __post_init__(self): + """Validate the input and prepare the clean URL after initialization.""" + # 1. Validate the hashing algorithm choice + if self.hashing_algorithm.lower() not in HASHING_ALGORITHMS: + raise ValueError(f"Unsupported algorithm. Choose from: {HASHING_ALGORITHMS}") + + # Parse the url to extract components + parsed = urlparse(self.download_url) + + # Extract username/password from URL if not already provided as arguments + if parsed.username and not self.username: + self.username = parsed.username + if parsed.password and not self.token: + self.token = parsed.password + + # 3. Create the 'clean_url' (URL without the credentials) + # This is what you actually send to the device if using ip http client + port = f":{parsed.port}" if parsed.port else "" + self.clean_url = f"{parsed.scheme}://{parsed.hostname}{port}{parsed.path}" + self.scheme = parsed.scheme + + # Handle query params if they exist (though we're avoiding '?' for Cisco) + if parsed.query: + self.clean_url += f"?{parsed.query}" + + @classmethod + def from_dict(cls, data: dict): + """Allows users to just pass a dictionary if they prefer.""" + return cls(**data) + + def to_dict(self): + """Useful for logging or passing to other Nornir tasks.""" + return asdict(self) diff --git a/pyntc/utils/templates/__init__.py b/pyntc/utils/templates/__init__.py index 99e10d29..1029a1d5 100644 --- a/pyntc/utils/templates/__init__.py +++ b/pyntc/utils/templates/__init__.py @@ -1,8 +1,8 @@ """Module to use NTC_TEMPLATES.""" import os -import textfsm +import textfsm TEMPLATE_PATH_ENV_VAR = "NTC_TEMPLATES" @@ -15,7 +15,7 @@ def get_structured_data(template_name, rawtxt): rawtxt (str): Raw output from device. Returns: - list: A dict per entry returned by TextFSM. + (list): A dict per entry returned by TextFSM. """ template_file = get_template(template_name) with open(template_file, encoding="utf-8") as template: @@ -37,7 +37,7 @@ def get_template(template_name): template_name (str): Name of the template. Returns: - str: Path to the template. + (str): Path to the template. """ template_dir = get_template_dir() return os.path.join(template_dir, template_name) @@ -47,7 +47,7 @@ def get_template_dir(): """Get directory of NTC_TEMPLATE os environment. Returns: - str: Path to NTC_TEMPLATES environment variable if set. Otherwise, path to this file. + (str): Path to NTC_TEMPLATES environment variable if set. Otherwise, path to this file. """ try: return os.environ[TEMPLATE_PATH_ENV_VAR] diff --git a/pyproject.toml b/pyproject.toml index 3f8460fa..65c1a775 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,22 +1,21 @@ [tool.poetry] name = "pyntc" -version = "2.0.2" -description = "SDK to simplify common workflows for Network Devices." -authors = ["Network to Code, LLC "] +version = "2.1.0" +description = "Python library focused on tasks related to device level and OS management." +authors = ["Network to Code, LLC "] readme = "README.md" -license = "Apache-2.0" -homepage = "https://pyntc.readthedocs.io" +homepage = "https://pyntc.readthedocs.io/" repository = "https://github.com/networktocode/pyntc" -documentation = "https://pyntc.readthedocs.io" -keywords = ["network", "os-upgrades", "network devices"] +documentation = "https://pyntc.readthedocs.io/" +license = "Apache-2.0" classifiers = [ "Intended Audience :: Developers", - "Development Status :: 4 - Beta", + "Development Status :: 5 - Production/Stable", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.7", - "Programming Language :: Python :: 3.8", - "Programming Language :: Python :: 3.9", - # "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", ] include = [ "LICENSE", @@ -24,7 +23,7 @@ include = [ ] [tool.poetry.dependencies] -python = "^3.8" +python = ">=3.10,<3.14" f5-sdk = "^3.0.21" junos-eznc = "^2.6" netmiko = "^4.0" @@ -37,25 +36,45 @@ scp = "^0.14" pyntc = 'pyntc.cli:main' [tool.poetry.group.dev.dependencies] +coverage = "*" +requests_mock = "*" pytest = "*" mock = "*" -requests_mock = "*" -pyyaml = "*" -black = "*" -pylint = "*" -pydocstyle = "*" -yamllint = "*" -bandit = "*" -invoke = "*" -toml = "*" -# we need to pin flake8 because of package dependencies that cause it to downgrade and -# therefore cause issues with linting since older versions do not take .flake8 as config -flake8 = "^3.9.2" -mkdocs = "1.3.1" -mkdocs-material = "8.3.9" -mkdocstrings = "0.19" -mkdocstrings-python = "0.7.1" -mkdocs-version-annotations = "1.0.0" +griffe = "1.1.1" +pyyaml = "^6.0.1" +pylint = "^3.1.0" +yamllint = "^1.35.1" +invoke = "^2.2.0" +toml = "^0.10.2" +attrs = "^23.2.0" +towncrier = ">=23.6.0,<=24.8.0" +ruff = "*" +Markdown = "*" + +[tool.poetry.group.docs.dependencies] +# Rendering docs to HTML +mkdocs = "1.6.1" +# Embedding YAML files into Markdown documents as tables +markdown-data-tables = "1.0.0" +# Render custom markdown for version added/changed/remove notes +markdown-version-annotations = "1.0.1" +# Automatically generate some files as part of mkdocs build +mkdocs-gen-files = "0.5.0" +# Image lightboxing in mkdocs +mkdocs-glightbox = "0.4.0" +# Use Jinja2 templating in docs - see settings.md +mkdocs-macros-plugin = "1.3.7" +# Material for mkdocs theme +mkdocs-material = "9.6.15" +# Handle docs redirections +mkdocs-redirects = "1.2.2" +# Automatically handle index pages for docs sections +mkdocs-section-index = "0.3.10" +# Automatic documentation from sources, for MkDocs +mkdocstrings = "0.27.0" +# Python-specific extension to mkdocstrings +mkdocstrings-python = "1.13.0" +griffe = "1.1.1" [tool.pyntc] string_required = "some string" @@ -65,55 +84,70 @@ ip_address = "192.168.0.1" ip_network = "2001:db8:3c4d:15::/64" file = "README.md" - -[tool.black] +[tool.ruff] line-length = 120 -target-version = ['py37'] -include = '\.pyi?$' -exclude = ''' -( - /( - \.eggs # exclude a few common directories in the - | \.git # root of the project - | \.hg - | \.mypy_cache - | \.tox - | \.venv - | _build - | buck-out - | build - | dist - )/ - | settings.py # This is where you define files that should not be stylized by black - # the root of the project -) -''' +target-version = "py310" + +[tool.ruff.lint] +select = [ + "D", # pydocstyle + "F", "E", "W", # flake8 + "S", # bandit + "I", # isort +] +ignore = [ + # warning: `one-blank-line-before-class` (D203) and `no-blank-line-before-class` (D211) are incompatible. + "D203", # 1 blank line required before class docstring + + # D212 is enabled by default in google convention, and complains if we have a docstring like: + # """ + # My docstring is on the line after the opening quotes instead of on the same line as them. + # """ + # We've discussed and concluded that we consider this to be a valid style choice. + "D212", # Multi-line docstring summary should start at the first line + "D213", # Multi-line docstring summary should start at the second line + + # Produces a lot of issues in the current codebase. + "D401", # First line of docstring should be in imperative mood + "D407", # Missing dashed underline after section + "D416", # Section name ends in colon + "E501", # Line too long +] + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.per-file-ignores] +"tests/*" = [ + "D", + "S" +] + [tool.pylint.master] ignore=[".venv", "tests"] [tool.pylint.basic] # No docstrings required for private methods (Pylint default), or for test_ functions, or for inner Meta classes. -no-docstring-rgx="^(_|test_|Meta$)" +no-docstring-rgx = "^(_|test_|Meta$)" + [tool.pylint.messages_control] -# Line length is enforced by Black, so pylint doesn't need to check it. -# Pylint and Black disagree about how to format multi-line arrays; Black wins. -disable = """, - abstract-method, - arguments-differ, - arguments-renamed, - attribute-defined-outside-init, - consider-iterating-dictionary, - duplicate-code, - inconsistent-return-statements, - line-too-long, - raise-missing-from, - too-many-arguments, - too-many-instance-attributes, - too-many-lines, - too-many-public-methods, - """ +disable = [ + "abstract-method", + "arguments-differ", + "arguments-renamed", + "attribute-defined-outside-init", + "consider-iterating-dictionary", + "duplicate-code", + "inconsistent-return-statements", + "line-too-long", + "raise-missing-from", + "too-many-arguments", + "too-many-instance-attributes", + "too-many-lines", + "too-many-public-methods" + ] [tool.pylint.miscellaneous] # Don't flag TODO as a failure, let us commit with things that still need to be done in the code @@ -122,20 +156,8 @@ notes = """, XXX, """ -[tool.pydocstyle] -convention = "google" -inherit = false -match = "(?!__init__).*\\.py" -match-dir = "(?!tests|migrations|development)[^\\.].*" -# D212 is enabled by default in google convention, and complains if we have a docstring like: -# """ -# My docstring is on the line after the opening quotes instead of on the same line as them. -# """ -# We've discussed and concluded that we consider this to be a valid style choice. -add_ignore = "D212" - [build-system] -requires = ["poetry_core>=1.0.0"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] @@ -144,3 +166,61 @@ testpaths = [ "tests/" ] addopts = "-vv --doctest-modules -p no:warnings --ignore-glob='*mock*'" + +[tool.towncrier] +package = "pyntc" +directory = "changes" +filename = "docs/admin/release_notes/version_X.Y.md" +template = "towncrier_template.j2" +start_string = "" +issue_format = "[#{issue}](https://github.com/networktocode/pyntc/issues/{issue})" + +[[tool.towncrier.type]] +directory = "breaking" +name = "Breaking Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "security" +name = "Security" +showcontent = true + +[[tool.towncrier.type]] +directory = "added" +name = "Added" +showcontent = true + +[[tool.towncrier.type]] +directory = "changed" +name = "Changed" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecated" +name = "Deprecated" +showcontent = true + +[[tool.towncrier.type]] +directory = "removed" +name = "Removed" +showcontent = true + +[[tool.towncrier.type]] +directory = "fixed" +name = "Fixed" +showcontent = true + +[[tool.towncrier.type]] +directory = "dependencies" +name = "Dependencies" +showcontent = true + +[[tool.towncrier.type]] +directory = "documentation" +name = "Documentation" +showcontent = true + +[[tool.towncrier.type]] +directory = "housekeeping" +name = "Housekeeping" +showcontent = true diff --git a/tasks.py b/tasks.py index 3e7264d7..635e5a4b 100644 --- a/tasks.py +++ b/tasks.py @@ -1,15 +1,11 @@ """Tasks for use with Invoke.""" import os -import sys -from distutils.util import strtobool +import re +from pathlib import Path -from invoke import task - -try: - import toml -except ImportError: - sys.exit("Please make sure to `pip install toml` or enable the Poetry shell and run `poetry install`.") +from invoke import Collection, Exit +from invoke import task as invoke_task def is_truthy(arg): @@ -18,60 +14,91 @@ def is_truthy(arg): Examples: >>> is_truthy('yes') True - Args: arg (str): Truthy string (True values are y, yes, t, true, on and 1; false values are n, no, f, false, off and 0. Raises ValueError if val is anything else. """ if isinstance(arg, bool): return arg - return bool(strtobool(arg)) + val = str(arg).lower() + if val in ("y", "yes", "t", "true", "on", "1"): + return True + if val in ("n", "no", "f", "false", "off", "0"): + return False + raise ValueError(f"Invalid truthy value: `{arg}`") + + +# Use pyinvoke configuration for default values, see http://docs.pyinvoke.org/en/stable/concepts/configuration.html +# Variables may be overwritten in invoke.yml or by the environment variables INVOKE_PYNTC_xxx +namespace = Collection("pyntc") +namespace.configure( + { + "pyntc": { + "project_name": "pyntc", + "python_ver": "3.10", + "local": is_truthy(os.getenv("INVOKE_PYNTC_LOCAL", "false")), + "image_name": "pyntc", + "image_ver": os.getenv("INVOKE_PYNTC_IMAGE_VER", "latest"), + "pwd": Path(__file__).parent, + } + } +) -PYPROJECT_CONFIG = toml.load("pyproject.toml") -TOOL_CONFIG = PYPROJECT_CONFIG["tool"]["poetry"] -# Can be set to a separate Python version to be used for launching or building image -PYTHON_VER = os.getenv("PYTHON_VER", "3.8") -# Name of the docker image/image -IMAGE_NAME = os.getenv("IMAGE_NAME", TOOL_CONFIG["name"]) -# Tag for the image -IMAGE_VER = os.getenv("IMAGE_VER", f"{TOOL_CONFIG['version']}-py{PYTHON_VER}") -# Gather current working directory for Docker commands -PWD = os.getcwd() -# Local or Docker execution provide "local" to run locally without docker execution -INVOKE_LOCAL = is_truthy(os.getenv("INVOKE_LOCAL", False)) # pylint: disable=W1508 +# pylint: disable=keyword-arg-before-vararg +def task(function=None, *args, **kwargs): + """Task decorator to override the default Invoke task decorator and add each task to the invoke namespace.""" + + def task_wrapper(function=None): + """Wrapper around invoke.task to add the task to the namespace as well.""" + if args or kwargs: + task_func = invoke_task(*args, **kwargs)(function) + else: + task_func = invoke_task(function) + namespace.add_task(task_func) + return task_func + + if function: + # The decorator was called with no arguments + return task_wrapper(function) + # The decorator was called with arguments + return task_wrapper -def run_cmd(context, exec_cmd, local=INVOKE_LOCAL, port=None): +def run_command(context, exec_cmd, port=None): """Wrapper to run the invoke task commands. Args: context ([invoke.task]): Invoke task object. exec_cmd ([str]): Command to run. - local (bool): Define as `True` to execute locally port (int): Used to serve local docs. Returns: result (obj): Contains Invoke result from running task. """ - if is_truthy(local): + if is_truthy(context.pyntc.local): print(f"LOCAL - Running command {exec_cmd}") result = context.run(exec_cmd, pty=True) else: - print(f"DOCKER - Running command: {exec_cmd} container: {IMAGE_NAME}:{IMAGE_VER}") + print(f"DOCKER - Running command: {exec_cmd} container: {context.pyntc.image_name}:{context.pyntc.image_ver}") if port: result = context.run( - f"docker run -it -p {port} -v {PWD}:/local {IMAGE_NAME}:{IMAGE_VER} sh -c '{exec_cmd}'", pty=True + f"docker run -it -p {port} -v {context.pyntc.pwd}:/local {context.pyntc.image_name}:{context.pyntc.image_ver} sh -c '{exec_cmd}'", + pty=True, ) else: result = context.run( - f"docker run -it -v {PWD}:/local {IMAGE_NAME}:{IMAGE_VER} sh -c '{exec_cmd}'", pty=True + f"docker run -it -v {context.pyntc.pwd}:/local {context.pyntc.image_name}:{context.pyntc.image_ver} sh -c '{exec_cmd}'", + pty=True, ) return result +# ------------------------------------------------------------------------------ +# BUILD +# ------------------------------------------------------------------------------ @task( help={ "cache": "Whether to use Docker's cache when building images (default enabled)", @@ -81,8 +108,8 @@ def run_cmd(context, exec_cmd, local=INVOKE_LOCAL, port=None): ) def build(context, cache=True, force_rm=False, hide=False): """Build a Docker image.""" - print(f"Building image {IMAGE_NAME}:{IMAGE_VER}") - command = f"docker build --tag {IMAGE_NAME}:{IMAGE_VER} --build-arg PYTHON_VER={PYTHON_VER} -f Dockerfile ." + print(f"Building image {context.pyntc.image_name}:{context.pyntc.image_ver}") + command = f"docker build --tag {context.pyntc.image_name}:{context.pyntc.image_ver} --build-arg PYTHON_VER={context.pyntc.python_ver} -f Dockerfile ." if not cache: command += " --no-cache" @@ -91,15 +118,35 @@ def build(context, cache=True, force_rm=False, hide=False): result = context.run(command, hide=hide) if result.exited != 0: - print(f"Failed to build image {IMAGE_NAME}:{IMAGE_VER}\nError: {result.stderr}") + print(f"Failed to build image {context.pyntc.image_name}:{context.pyntc.image_ver}\nError: {result.stderr}") + + +@task +def generate_packages(context): + """Generate all Python packages inside docker and copy the file locally under dist/.""" + command = "poetry build" + run_command(context, command) + + +@task( + help={ + "check": ( + "If enabled, check for outdated dependencies in the poetry.lock file, " + "instead of generating a new one. (default: disabled)" + ) + } +) +def lock(context, check=False): + """Generate poetry.lock inside the library container.""" + run_command(context, f"poetry {'check' if check else 'lock --no-update'}") @task def clean(context): """Remove the project specific image.""" - print(f"Attempting to forcefully remove image {IMAGE_NAME}:{IMAGE_VER}") - context.run(f"docker rmi {IMAGE_NAME}:{IMAGE_VER} --force") - print(f"Successfully removed image {IMAGE_NAME}:{IMAGE_VER}") + print(f"Attempting to forcefully remove image {context.pyntc.image_name}:{context.pyntc.image_ver}") + context.run(f"docker rmi {context.pyntc.image_name}:{context.pyntc.image_ver} --force") + print(f"Successfully removed image {context.pyntc.image_name}:{context.pyntc.image_ver}") @task @@ -109,78 +156,166 @@ def rebuild(context): build(context, cache=False) -@task(help={"local": "Run locally or within the Docker container"}) -def pytest(context, local=INVOKE_LOCAL, args=""): - """Run pytest test cases.""" - exec_cmd = f"pytest {args}" - run_cmd(context, exec_cmd, local) +@task +def coverage(context): + """Run the coverage report against pytest.""" + exec_cmd = "coverage run --source=pyntc -m pytest" + run_command(context, exec_cmd) + run_command(context, "coverage report") + run_command(context, "coverage html") -@task(help={"local": "Run locally or within the Docker container"}) -def black(context, local=INVOKE_LOCAL): - """Run black to check that Python files adherence to black standards.""" - exec_cmd = "black --check --diff ." - run_cmd(context, exec_cmd, local) +@task +def pytest(context): + """Run pytest test cases.""" + exec_cmd = "coverage run --source=pyntc -m pytest && coverage report" + run_command(context, exec_cmd) -@task(help={"local": "Run locally or within the Docker container"}) -def flake8(context, local=INVOKE_LOCAL): - """Run flake8 code analysis.""" - exec_cmd = "flake8 ." - run_cmd(context, exec_cmd, local) +@task(aliases=("a",)) +def autoformat(context): + """Run code autoformatting.""" + ruff(context, action=["format"], fix=True) -@task(help={"local": "Run locally or within the Docker container"}) -def pylint(context, local=INVOKE_LOCAL): - """Run pylint code analysis.""" - exec_cmd = 'find . -name "*.py" | grep -vE "tests/unit" | xargs pylint' - run_cmd(context, exec_cmd, local) +@task( + help={ + "action": "Available values are `['lint', 'format']`. Can be used multiple times. (default: `['lint', 'format']`)", + "target": "File or directory to inspect, repeatable (default: all files in the project will be inspected)", + "fix": "Automatically fix selected actions. May not be able to fix all issues found. (default: False)", + "output_format": "See https://docs.astral.sh/ruff/settings/#output-format for details. (default: `concise`)", + }, + iterable=["action", "target"], +) +def ruff(context, action=None, target=None, fix=False, output_format="concise"): + """Run ruff to perform code formatting and/or linting.""" + if not action: + action = ["lint", "format"] + if not target: + target = ["."] + + exit_code = 0 + + if "format" in action: + command = "ruff format " + if not fix: + command += "--check " + command += " ".join(target) + if not run_command(context, command): + exit_code = 1 + + if "lint" in action: + command = "ruff check " + if fix: + command += "--fix " + command += f"--output-format {output_format} " + command += " ".join(target) + if not run_command(context, command): + exit_code = 1 + + if exit_code != 0: + raise Exit(code=exit_code) -@task(help={"local": "Run locally or within the Docker container"}) -def yamllint(context, local=INVOKE_LOCAL): - """Run yamllint to validate formatting adheres to NTC defined YAML standards.""" - exec_cmd = "yamllint ." - run_cmd(context, exec_cmd, local) +@task +def pylint(context): + """Run pylint for the specified name and Python version. + Args: + context (obj): Used to run specific commands + """ + exec_cmd = "pylint --verbose pyntc" + run_command(context, exec_cmd) -@task(help={"local": "Run locally or within the Docker container"}) -def pydocstyle(context, local=INVOKE_LOCAL): - """Run pydocstyle to validate docstring formatting adheres to NTC defined standards.""" - exec_cmd = "pydocstyle ." - run_cmd(context, exec_cmd, local) +@task +def yamllint(context): + """Run yamllint to validate formatting adheres to NTC defined YAML standards. -@task(help={"local": "Run locally or within the Docker container"}) -def bandit(context, local=INVOKE_LOCAL): - """Run bandit to validate basic static code security analysis.""" - exec_cmd = "bandit --recursive ./ --configfile .bandit.yml" - run_cmd(context, exec_cmd, local) + Args: + context (obj): Used to run specific commands + """ + exec_cmd = "yamllint ." + run_command(context, exec_cmd) @task def cli(context): - """Enter the image to perform troubleshooting or dev work.""" - dev = f"docker run -it -v {PWD}:/local {IMAGE_NAME}:{IMAGE_VER} /bin/bash" + """Enter the image to perform troubleshooting or dev work. + + Args: + context (obj): Used to run specific commands + """ + dev = f"docker run -it -v {context.pyntc.pwd}:/local {context.pyntc.image_name}:{context.pyntc.image_ver} /bin/bash" context.run(f"{dev}", pty=True) -@task(help={"local": "Run locally or within the Docker container"}) -def tests(context, local=INVOKE_LOCAL): - """Run all tests for this repository.""" - black(context, local) - flake8(context, local) - pylint(context, local) - yamllint(context, local) - pydocstyle(context, local) - bandit(context, local) - pytest(context, local) +@task( + help={ + "lint-only": "Only run linters; unit tests will be excluded. (default: False)", + } +) +def tests(context, lint_only=False): + """Run all tests for the specified name and Python version. + Args: + context (obj): Used to run specific commands + lint_only (bool): If True, only run linters and skip unit tests. + """ + # If we are not running locally, start the docker containers so we don't have to for each test + # Sorted loosely from fastest to slowest + print("Running ruff...") + ruff(context) + print("Running yamllint...") + yamllint(context) + print("Running poetry check...") + lock(context, check=True) + print("Running pylint...") + pylint(context) + print("Running mkdocs...") + build_and_check_docs(context) + if not lint_only: + print("Running unit tests...") + pytest(context) print("All tests have passed!") @task -def docs(context, local=INVOKE_LOCAL): +def build_and_check_docs(context): + """Build documentation and test the configuration.""" + command = "mkdocs build --no-directory-urls --strict" + run_command(context, command) + + # Check for the existence of a release notes file for the current version if it's not a prerelease. + version = context.run("poetry version --short", hide=True) + match = re.match(r"^(\d+)\.(\d+)\.\d+$", version.stdout.strip()) + if match: + major = match.group(1) + minor = match.group(2) + release_notes_file = Path(__file__).parent / "docs" / "admin" / "release_notes" / f"version_{major}.{minor}.md" + if not release_notes_file.exists(): + print(f"Release notes file `version_{major}.{minor}.md` does not exist.") + raise Exit(code=1) + + +@task +def docs(context): """Build and serve docs locally for development.""" - exec_cmd = "mkdocs serve -v --dev-addr=0.0.0.0:8001" - run_cmd(context, exec_cmd, local, port="8001:8001") + exec_cmd = "mkdocs serve -v" + run_command(context, exec_cmd, port="8001:8001") + + +@task( + help={ + "version": "Version of pyntc to generate the release notes for.", + } +) +def generate_release_notes(context, version=""): + """Generate Release Notes using Towncrier.""" + command = "poetry run towncrier build" + if version: + command += f" --version {version}" + else: + command += " --version `poetry version -s`" + # Due to issues with git repo ownership in the containers, this must always run locally. + context.run(command) diff --git a/tests/integration/__init__.py b/tests/integration/__init__.py new file mode 100644 index 00000000..c66cd71b --- /dev/null +++ b/tests/integration/__init__.py @@ -0,0 +1 @@ +"""Integration tests package.""" diff --git a/tests/unit/__init__.py b/tests/unit/__init__.py new file mode 100644 index 00000000..ea3f8b92 --- /dev/null +++ b/tests/unit/__init__.py @@ -0,0 +1 @@ +"""Unit tests package.""" diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index ff30404a..37acf4f5 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -2,6 +2,7 @@ from unittest import mock import pytest + from pyntc.devices import AIREOSDevice, ASADevice, IOSDevice, IOSXEWLCDevice, supported_devices diff --git a/tests/unit/test_basics.py b/tests/unit/test_basics.py new file mode 100644 index 00000000..d8f73c06 --- /dev/null +++ b/tests/unit/test_basics.py @@ -0,0 +1,29 @@ +"""Basic tests that do not require Pyntc.""" + +import os +import re +import unittest + +import toml + + +class TestDocsReleaseNotes(unittest.TestCase): + """Test that mkdocs has the release notes for the current version.""" + + def test_version_file_found(self): + """Verify that if the current version has no letters, which would see in alpha or beta has an associated release note file.""" + parent_path = os.path.dirname(os.path.dirname(os.path.dirname(os.path.realpath(__file__)))) + poetry_path = os.path.join(parent_path, "pyproject.toml") + project_version = toml.load(poetry_path)["tool"]["poetry"]["version"] + + docs_path = os.path.join(parent_path, "docs") + release_notes_files = [file for file in os.listdir(f"{docs_path}/admin/release_notes/") if file.endswith(".md")] + version_pattern = re.compile(r"^(\d+)\.(\d+)\.\d+$") + + match = version_pattern.match(project_version) + # If there is no match, then it is likely an alpha or beta version and we can skip this test. + if match: + major, minor = match.groups() + version_str = f"version_{major}.{minor}.md" + if version_str not in release_notes_files: + self.fail(f"Release note file for version {version_str} not found in release notes folder.") diff --git a/tests/unit/test_devices/device_mocks/eos/__init__.py b/tests/unit/test_devices/device_mocks/eos/__init__.py index cdd4c38d..71e4e1e1 100644 --- a/tests/unit/test_devices/device_mocks/eos/__init__.py +++ b/tests/unit/test_devices/device_mocks/eos/__init__.py @@ -1,8 +1,9 @@ -import os import json -from pyeapi.eapilib import CommandError as EOSCommandError +import os import re +from pyeapi.eapilib import CommandError as EOSCommandError + CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/tests/unit/test_devices/device_mocks/nxos/__init__.py b/tests/unit/test_devices/device_mocks/nxos/__init__.py index fb82b727..e04a60d9 100644 --- a/tests/unit/test_devices/device_mocks/nxos/__init__.py +++ b/tests/unit/test_devices/device_mocks/nxos/__init__.py @@ -1,5 +1,6 @@ -import os import json +import os + from pynxos.errors import CLIError CURRNENT_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/tests/unit/test_devices/test_aireos_device.py b/tests/unit/test_devices/test_aireos_device.py index 430c8813..b6a6cda5 100644 --- a/tests/unit/test_devices/test_aireos_device.py +++ b/tests/unit/test_devices/test_aireos_device.py @@ -3,8 +3,8 @@ import pytest -from pyntc.devices import aireos_device as aireos_module from pyntc.devices import AIREOSDevice +from pyntc.devices import aireos_device as aireos_module @pytest.mark.parametrize( @@ -161,7 +161,7 @@ def test_wait_for_ap_image_download_timeout(mock_ap_image_stats, aireos_device): with pytest.raises(aireos_module.FileTransferError) as fte: aireos_device._wait_for_ap_image_download(timeout=1) assert fte.value.message == ( - "Failed waiting for AP image to be transferred to all devices:\n" "Total: 2\nDownloaded: 1" + "Failed waiting for AP image to be transferred to all devices:\nTotal: 2\nDownloaded: 1" ) @@ -480,7 +480,7 @@ def test_disable_wlans_all_fail(mock_disabled_wlans, mock_wlans, aireos_device, aireos_device.disable_wlans("all") assert disable_err.value.message == ( - "Unable to disable WLAN IDs on host\n" "Expected: [5, 15, 16, 20, 21, 22, 24]\n" "Found: [16, 21, 24]\n" + "Unable to disable WLAN IDs on host\nExpected: [5, 15, 16, 20, 21, 22, 24]\nFound: [16, 21, 24]\n" ) @@ -523,7 +523,7 @@ def test_disable_wlans_subset_fail(mock_disabled_wlans, mock_config, aireos_devi aireos_device.disable_wlans([15]) assert disable_err.value.message == ( - "Unable to disable WLAN IDs on host\n" "Expected: [15, 16, 21, 24]\n" "Found: [16, 21, 24]\n" + "Unable to disable WLAN IDs on host\nExpected: [15, 16, 21, 24]\nFound: [16, 21, 24]\n" ) @@ -600,7 +600,7 @@ def test_enable_wlans_all_fail(mock_enabled_wlans, mock_wlans, mock_config, aire aireos_device.enable_wlans("all") assert enable_err.value.message == ( - "Unable to enable WLAN IDs on host\n" "Expected: [5, 15, 16, 20, 21, 22, 24]\n" "Found: [5, 15, 20, 22]\n" + "Unable to enable WLAN IDs on host\nExpected: [5, 15, 16, 20, 21, 22, 24]\nFound: [5, 15, 20, 22]\n" ) @@ -643,7 +643,7 @@ def test_enable_wlans_subset_fail(mock_enabled_wlans, mock_config, aireos_device aireos_device.enable_wlans([16]) assert enable_err.value.message == ( - "Unable to enable WLAN IDs on host\n" "Expected: [5, 15, 16, 20, 22]\n" "Found: [5, 15, 20, 22]\n" + "Unable to enable WLAN IDs on host\nExpected: [5, 15, 16, 20, 22]\nFound: [5, 15, 20, 22]\n" ) diff --git a/tests/unit/test_devices/test_asa_device.py b/tests/unit/test_devices/test_asa_device.py index 226159a0..f89ffb4b 100644 --- a/tests/unit/test_devices/test_asa_device.py +++ b/tests/unit/test_devices/test_asa_device.py @@ -4,8 +4,8 @@ import pytest -from pyntc.devices import asa_device as asa_module from pyntc.devices import ASADevice +from pyntc.devices import asa_device as asa_module from .device_mocks.asa import send_command diff --git a/tests/unit/test_devices/test_ios_device.py b/tests/unit/test_devices/test_ios_device.py index a2b53b2d..5cdfc4d5 100644 --- a/tests/unit/test_devices/test_ios_device.py +++ b/tests/unit/test_devices/test_ios_device.py @@ -5,9 +5,10 @@ import mock import pytest -from pyntc.devices import ios_device as ios_module from pyntc.devices import IOSDevice +from pyntc.devices import ios_device as ios_module from pyntc.devices.base_device import RollbackError +from pyntc.utils.models import FileCopyModel from .device_mocks.ios import send_command, send_command_expect @@ -137,14 +138,17 @@ def test_file_copy_remote_exists_not(self, mock_ft): self.assertFalse(result) + @mock.patch.object(IOSDevice, "get_local_checksum") + @mock.patch.object(IOSDevice, "verify_file") @mock.patch("pyntc.devices.ios_device.FileTransfer", autospec=True) @mock.patch.object(IOSDevice, "open") - def test_file_copy(self, mock_open, mock_ft): + def test_file_copy(self, mock_open, mock_ft, mock_verify_file, mock_get_local_checksum): self.device.native.send_command.side_effect = None self.device.native.send_command.return_value = "flash: /dev/null" mock_ft_instance = mock_ft.return_value - mock_ft_instance.check_file_exists.side_effect = [False, True] + mock_verify_file.side_effect = [False, True] + mock_get_local_checksum.return_value = "dummy_checksum" self.device.file_copy("path/to/source_file") mock_ft.assert_called_with(self.device.native, "path/to/source_file", "source_file", file_system="flash:") @@ -153,14 +157,16 @@ def test_file_copy(self, mock_open, mock_ft): mock_ft_instance.transfer_file.assert_any_call() mock_open.assert_called_once() + @mock.patch.object(IOSDevice, "get_local_checksum") + @mock.patch.object(IOSDevice, "verify_file") @mock.patch("pyntc.devices.ios_device.FileTransfer", autospec=True) @mock.patch.object(IOSDevice, "open") - def test_file_copy_different_dest(self, mock_open, mock_ft): + def test_file_copy_different_dest(self, mock_open, mock_ft, mock_verify_file, mock_get_local_checksum): self.device.native.send_command_timing.side_effect = None self.device.native.send_command.return_value = "flash: /dev/null" mock_ft_instance = mock_ft.return_value - mock_ft_instance.check_file_exists.side_effect = [False, True] + mock_verify_file.side_effect = [False, True] self.device.file_copy("source_file", "dest_file") mock_ft.assert_called_with(self.device.native, "source_file", "dest_file", file_system="flash:") @@ -169,29 +175,33 @@ def test_file_copy_different_dest(self, mock_open, mock_ft): mock_ft_instance.transfer_file.assert_any_call() mock_open.assert_called_once() + @mock.patch.object(IOSDevice, "get_local_checksum") + @mock.patch.object(IOSDevice, "verify_file") @mock.patch("pyntc.devices.ios_device.FileTransfer", autospec=True) @mock.patch.object(IOSDevice, "open") - def test_file_copy_fail(self, mock_open, mock_ft): + def test_file_copy_fail(self, mock_open, mock_ft, mock_verify_file, mock_get_local_checksum): self.device.native.send_command_timing.side_effect = None self.device.native.send_command.return_value = "flash: /dev/null" mock_ft_instance = mock_ft.return_value mock_ft_instance.transfer_file.side_effect = Exception - mock_ft_instance.check_file_exists.return_value = False + mock_verify_file.return_value = False with self.assertRaises(ios_module.FileTransferError): self.device.file_copy("source_file") mock_open.assert_not_called() + @mock.patch.object(IOSDevice, "get_local_checksum") + @mock.patch.object(IOSDevice, "verify_file") @mock.patch("pyntc.devices.ios_device.FileTransfer", autospec=True) @mock.patch.object(IOSDevice, "open") - def test_file_copy_socket_closed_good_md5(self, mock_open, mock_ft): + def test_file_copy_socket_closed_good_md5(self, mock_open, mock_ft, mock_verify_file, mock_get_local_checksum): self.device.native.send_command_timing.side_effect = None self.device.native.send_command.return_value = "flash: /dev/null" mock_ft_instance = mock_ft.return_value mock_ft_instance.transfer_file.side_effect = OSError - mock_ft_instance.check_file_exists.side_effect = [False, True] - mock_ft_instance.compare_md5.side_effect = [True, True] + mock_verify_file.side_effect = [False, True] + mock_ft_instance.compare_md5.return_value = True self.device.file_copy("path/to/source_file") @@ -199,17 +209,21 @@ def test_file_copy_socket_closed_good_md5(self, mock_open, mock_ft): mock_ft_instance.enable_scp.assert_any_call() mock_ft_instance.establish_scp_conn.assert_any_call() mock_ft_instance.transfer_file.assert_any_call() - mock_ft_instance.compare_md5.assert_has_calls([mock.call(), mock.call()]) + mock_ft_instance.compare_md5.assert_has_calls([mock.call()]) mock_open.assert_called_once() + @mock.patch.object(IOSDevice, "get_local_checksum") + @mock.patch.object(IOSDevice, "verify_file") @mock.patch("pyntc.devices.ios_device.FileTransfer", autospec=True) @mock.patch.object(IOSDevice, "open") - def test_file_copy_fail_socket_closed_bad_md5(self, mock_open, mock_ft): + def test_file_copy_fail_socket_closed_bad_md5(self, mock_open, mock_ft, mock_verify_file, mock_get_local_checksum): self.device.native.send_command_timing.side_effect = None self.device.native.send_command.return_value = "flash: /dev/null" mock_ft_instance = mock_ft.return_value mock_ft_instance.transfer_file.side_effect = OSError mock_ft_instance.check_file_exists.return_value = False + mock_verify_file.return_value = False + mock_get_local_checksum.return_value = "dummy_checksum" mock_ft_instance.compare_md5.return_value = False with self.assertRaises(ios_module.SocketClosedError): @@ -389,6 +403,197 @@ def test_install_os_error(self, mock_wait, mock_reboot, mock_set_boot, mock_imag mock_raw_version_data.return_value = DEVICE_FACTS self.assertRaises(ios_module.OSInstallError, self.device.install_os, BOOT_IMAGE) + @mock.patch.object(IOSDevice, "os_version", new_callable=mock.PropertyMock) + @mock.patch.object(IOSDevice, "_image_booted", side_effect=[False, True]) + @mock.patch.object(IOSDevice, "set_boot_options") + @mock.patch.object(IOSDevice, "show") + @mock.patch.object(IOSDevice, "reboot") + @mock.patch.object(IOSDevice, "_wait_for_device_reboot") + @mock.patch.object(IOSDevice, "_raw_version_data") + def test_install_os_not_enough_space( + self, + mock_raw_version_data, + mock_wait, + mock_reboot, + mock_show, + mock_set_boot, + mock_image_booted, + mock_os_version, + ): + mock_raw_version_data.return_value = DEVICE_FACTS + mock_os_version.return_value = "17.4.3" + mock_show.return_value = "FAILED: There is not enough free disk available to perform this operation on switch 1. At least 1276287 KB of free disk is required" + self.assertRaises(ios_module.OSInstallError, self.device.install_os, image_name=BOOT_IMAGE, install_mode=True) + mock_wait.assert_not_called() + mock_reboot.assert_not_called() + + def test_get_local_checksum(self): + # Create a temporary file with known content + test_file_path = "test_file.txt" + test_content = "This is a test file for checksum." + with open(test_file_path, "w") as f: + f.write(test_content) + + import hashlib + + with self.subTest("Test get_local_checksum returns correct md5 checksum"): + expected_checksum = hashlib.md5(test_content.encode()).hexdigest() + actual_checksum = self.device.get_local_checksum(test_file_path) + self.assertEqual(actual_checksum, expected_checksum) + + with self.subTest("Test get_local_checksum returns correct sha512 checksum"): + expected_checksum = hashlib.sha512(test_content.encode()).hexdigest() + actual_checksum = self.device.get_local_checksum(test_file_path, hashing_algorithm="sha512") + self.assertEqual(actual_checksum, expected_checksum) + + with self.subTest("Test get_local_checksum with add_newline=True"): + expected_checksum = hashlib.md5((test_content + "\n").encode()).hexdigest() + actual_checksum = self.device.get_local_checksum(test_file_path, add_newline=True) + self.assertEqual(actual_checksum, expected_checksum) + + with self.subTest("Test get_local_checksum with invalid hashing algorithm"): + with self.assertRaises(ValueError): + self.device.get_local_checksum(test_file_path, hashing_algorithm="invalid_algo") + + # Clean up the temporary file + os.remove(test_file_path) + + def test_check_file_exists(self): + with self.subTest("Test check_file_exists returns True when file exists"): + self.device.native.send_command_expect.side_effect = None + self.device.native.send_command.return_value = "Directory of flash:/file.txt\n" + self.assertTrue(self.device.check_file_exists("file.txt", file_system="flash:")) + + with self.subTest("Test check_file_exists returns False when file does not exist"): + self.device.native.send_command_expect.side_effect = None + self.device.native.send_command.return_value = "%Error opening flash:/file.txt\n" + self.assertFalse(self.device.check_file_exists("file.txt", file_system="flash:")) + + def test_get_remote_checksum(self): + with self.subTest("Test get_remote_checksum returns correct md5 checksum"): + self.device.native.send_command_timing.side_effect = None + self.device.native.send_command_timing.return_value = "MD5 (flash:/file.txt) = dummy_checksum" + self.assertEqual(self.device.get_remote_checksum("file.txt", file_system="flash:"), "dummy_checksum") + + with self.subTest("Test get_remote_checksum with invalid hashing algorithm"): + with self.assertRaises(ValueError): + self.device.get_remote_checksum("file.txt", hashing_algorithm="invalid_algo", file_system="flash:") + + @mock.patch.object(IOSDevice, "verify_file") + def test_remote_file_copy_success(self, mock_verify): + # Setup file model + src = FileCopyModel( + download_url="sftp://user:test@1.1.1.1/test.bin", + checksum="12345", + file_name="test.bin", + hashing_algorithm="md5", + timeout=900, + ) + self.assertEqual(src.clean_url, "sftp://1.1.1.1/test.bin") + self.assertEqual(src.username, "user") + self.assertEqual(src.token, "test") + + # Scenario: First verify_file fails (needs copy), second verify_file succeeds (copy worked) + mock_verify.side_effect = [False, True] + + # Simulate the prompt sequence: + # 1. Initial command -> gets "Address or name of remote host" + # 2. Responds with "" -> gets "Copy complete" + self.device.native.send_command.side_effect = [ + "Address or name of remote host [1.1.1.1]?", + "Source username?", + "Password:", + "123456 bytes copied in 10.2 secs. Copy complete.", + ] + self.device.native.find_prompt.return_value = "Router#" + + with self.subTest("Test successful copy with interactive prompts"): + self.device.remote_file_copy(src, dest="test.bin", file_system="flash:") + + # Verify the command sent was correct + self.device.native.send_command.assert_any_call( + "copy sftp://1.1.1.1/test.bin flash:test.bin", expect_string=mock.ANY, read_timeout=900 + ) + self.device.native.send_command.assert_has_calls( + [ + mock.call("copy sftp://1.1.1.1/test.bin flash:test.bin", expect_string=mock.ANY, read_timeout=900), + mock.call( + "", expect_string=mock.ANY, read_timeout=900, cmd_verify=True + ), # Respond to "Address or name of remote host" + mock.call( + "user", expect_string=mock.ANY, read_timeout=900, cmd_verify=True + ), # Respond to "Source username?" + mock.call( + "test", expect_string=mock.ANY, read_timeout=900, cmd_verify=False + ), # Respond to "Password:" + ] + ) + + @mock.patch.object(IOSDevice, "verify_file") + def test_remote_file_copy_no_dest(self, mock_verify): + # Setup file model + src = FileCopyModel( + download_url="sftp://1.1.1.1/test.bin", + checksum="12345", + file_name="test.bin", + hashing_algorithm="md5", + timeout=300, + ) + self.assertEqual(src.clean_url, src.download_url) + self.assertIsNone(src.username) + self.assertIsNone(src.token) + + # Scenario: First verify_file fails (needs copy), second verify_file succeeds (copy worked) + mock_verify.side_effect = [False, True] + + # Simulate the prompt sequence: + # 1. Initial command -> gets "Address or name of remote host" + # 2. Responds with "" -> gets "Copy complete" + self.device.native.send_command.side_effect = [ + "Address or name of remote host [1.1.1.1]?", + "123456 bytes copied in 10.2 secs. Copy complete.", + ] + self.device.native.find_prompt.return_value = "Router#" + + with self.subTest("Test successful copy with interactive prompts"): + self.device.remote_file_copy(src, file_system="flash:") + + # Verify the command sent was correct + self.device.native.send_command.assert_has_calls( + [ + mock.call("copy sftp://1.1.1.1/test.bin flash:test.bin", expect_string=mock.ANY, read_timeout=300), + mock.call( + "", expect_string=mock.ANY, read_timeout=300, cmd_verify=True + ), # Respond to "Address or name of remote host" + ] + ) + + def test_remote_file_copy_type_error(self): + with self.subTest("Test raises TypeError if src is not FileCopyModel"): + with self.assertRaises(TypeError): + self.device.remote_file_copy("not_a_model") + + @mock.patch.object(IOSDevice, "verify_file") + def test_remote_file_copy_failure_on_error_output(self, mock_verify): + # Setup file model + src = FileCopyModel( + download_url="tftp://1.1.1.1/test.bin", + checksum="12345", + file_name="test.bin", + hashing_algorithm="md5", + ) + mock_verify.return_value = False + self.device.native.find_prompt.return_value = "Router#" + + # Simulate a network error returned by the device + self.device.native.send_command.return_value = "%Error opening tftp://1.1.1.1/test.bin (Timed out)" + + with self.subTest("Test raises FileTransferError when device returns Error string"): + from pyntc.errors import FileTransferError + + with self.assertRaises(FileTransferError): + self.device.remote_file_copy(src) + if __name__ == "__main__": unittest.main() @@ -1262,6 +1467,7 @@ def test_install_os_install_mode_with_retries( mock_has_reload_happened_recently.side_effect = [False, False, True] mock_image_booted.side_effect = [False, True] mock_sleep.return_value = None + mock_show.return_value = "show must go on" # Call the install os function actual = ios_device.install_os(image_name, install_mode=True) diff --git a/tests/unit/test_errors.py b/tests/unit/test_errors.py index 5f88e508..2b4edc0b 100644 --- a/tests/unit/test_errors.py +++ b/tests/unit/test_errors.py @@ -55,12 +55,7 @@ def test_command_error(): def test_command_list_error(): error_message = ( - "\n" - "Command fail failed with message: '% invalid command'\n" - "Command List: \n" - "\tcommand 1\n" - "\tfail\n" - "\tcommand 2\n" + "\nCommand fail failed with message: '% invalid command'\nCommand List: \n\tcommand 1\n\tfail\n\tcommand 2\n" ) error_class = ntc_errors.CommandListError error = error_class( @@ -75,7 +70,7 @@ def test_command_list_error(): def test_device_not_active_error(): - expected = "ntc_host is not the active device.\n\n" "device state: standby hot\n" "peer state: active\n" + expected = "ntc_host is not the active device.\n\ndevice state: standby hot\npeer state: active\n" error_class = ntc_errors.DeviceNotActiveError error = error_class("ntc_host", "standby hot", "active") with pytest.raises(error_class) as err: @@ -170,7 +165,7 @@ def test_peer_failed_to_form_error(): raise error assert err.value.message == ( - 'host1 was unable to form a redundancy state of "standby hot" with peer.\n' 'The current state is "disabled".' + 'host1 was unable to form a redundancy state of "standby hot" with peer.\nThe current state is "disabled".' ) diff --git a/tests/unit/test_infra.py b/tests/unit/test_infra.py index 14349898..d1351bd6 100644 --- a/tests/unit/test_infra.py +++ b/tests/unit/test_infra.py @@ -1,11 +1,11 @@ import os + import mock import pytest from pyntc import ntc_device, ntc_device_by_name -from pyntc.errors import UnsupportedDeviceError, ConfFileNotFoundError -from pyntc.devices import EOSDevice, NXOSDevice, IOSDevice - +from pyntc.devices import EOSDevice, IOSDevice, NXOSDevice +from pyntc.errors import ConfFileNotFoundError, UnsupportedDeviceError BAD_DEVICE_TYPE = "238nzsvkn3981" FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "..", "fixtures") diff --git a/tests/unit/test_system_features/test_vlan/mocks/eos/__init__.py b/tests/unit/test_system_features/test_vlan/mocks/eos/__init__.py index 7dcc45b0..4dbc137d 100644 --- a/tests/unit/test_system_features/test_vlan/mocks/eos/__init__.py +++ b/tests/unit/test_system_features/test_vlan/mocks/eos/__init__.py @@ -1,6 +1,5 @@ -import os import json - +import os CURRENT_DIR = os.path.dirname(os.path.realpath(__file__)) diff --git a/tests/unit/test_system_features/test_vlan/test_eos_vlan.py b/tests/unit/test_system_features/test_vlan/test_eos_vlan.py index 393ea822..79b915ad 100644 --- a/tests/unit/test_system_features/test_vlan/test_eos_vlan.py +++ b/tests/unit/test_system_features/test_vlan/test_eos_vlan.py @@ -1,9 +1,11 @@ -import mock import unittest -from .mocks.eos import get, getall -from pyntc.devices.system_features.vlans.eos_vlans import EOSVlans +import mock + from pyntc.devices.system_features.vlans.base_vlans import VlanNotInRangeError +from pyntc.devices.system_features.vlans.eos_vlans import EOSVlans + +from .mocks.eos import get, getall class TestEOSVlan(unittest.TestCase): diff --git a/towncrier_template.j2 b/towncrier_template.j2 new file mode 100644 index 00000000..f69a5668 --- /dev/null +++ b/towncrier_template.j2 @@ -0,0 +1,43 @@ + +# v{{ versiondata.version.split(".")[:2] | join(".") }} Release Notes + +This document describes all new features and changes in the release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## Release Overview + +- Major features or milestones +- Changes to compatibility with Nautobot and/or other apps, libraries etc. + +{% if render_title %} +## [v{{ versiondata.version }} ({{ versiondata.date }})](https://github.com/networktocode/pyntc/releases/tag/v{{ versiondata.version}}) + +{% endif %} +{% for section, _ in sections.items() %} +{% if sections[section] %} +{% for category, val in definitions.items() if category in sections[section] %} +{% if sections[section][category]|length != 0 %} +### {{ definitions[category]['name'] }} + +{% if definitions[category]['showcontent'] %} +{% for text, values in sections[section][category].items() %} +{% for item in text.split('\n') %} +{% if values %} +- {{ values|join(', ') }} - {{ item.strip() }} +{% else %} +- {{ item.strip() }} +{% endif %} +{% endfor %} +{% endfor %} + +{% else %} +- {{ sections[section][category]['']|join(', ') }} + +{% endif %} +{% endif %} +{% endfor %} +{% else %} +No significant changes. + +{% endif %} +{% endfor %} +