pax_global_header00006660000000000000000000000064147571514530014526gustar00rootroot0000000000000052 comment=7b95e7c61a30bc2cda8307a8165e448dd97ee3ed pytest-httpserver-1.1.2/000077500000000000000000000000001475715145300152635ustar00rootroot00000000000000pytest-httpserver-1.1.2/.github/000077500000000000000000000000001475715145300166235ustar00rootroot00000000000000pytest-httpserver-1.1.2/.github/actions/000077500000000000000000000000001475715145300202635ustar00rootroot00000000000000pytest-httpserver-1.1.2/.github/actions/setup/000077500000000000000000000000001475715145300214235ustar00rootroot00000000000000pytest-httpserver-1.1.2/.github/actions/setup/action.yml000066400000000000000000000033731475715145300234310ustar00rootroot00000000000000name: "setup poetry and python" description: "Setup python, poetry, and development environment" inputs: type: description: "Type of the venv to create" required: true python-version: description: "Python version to use" required: true poetry-version: description: "Poetry version to use" required: true runs: using: "composite" steps: - name: Setup python id: setup_python uses: actions/setup-python@v5 with: python-version: ${{ inputs.python-version }} - name: Cache venv id: cache-venv uses: actions/cache@v4 with: path: .venv key: venv-v4-${{ inputs.type }}-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('poetry.lock') }} - name: Cache pre-commit id: cache-pre-commit uses: actions/cache@v4 with: path: ~/.cache/pre-commit key: pre-commit-v4-${{ inputs.type }}-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ hashFiles('.pre-commit-config.yaml') }} - name: Cache setup-poetry id: cache-setup-poetry uses: actions/cache@v4 with: path: | ~/.local/share/pypoetry ~/.local/share/virtualenv ~/.local/bin/poetry key: setup-poetry-v4-${{ runner.os }}-${{ steps.setup_python.outputs.python-version }}-${{ inputs.poetry-version }} - name: Setup poetry uses: Gr1N/setup-poetry@v8 id: setup_poetry if: steps.cache-setup-poetry.outputs.cache-hit != 'true' with: poetry-version: ${{ inputs.poetry-version }} - name: Bootstrap environment shell: bash run: | make dev EXTRAS=${{ inputs.type }} pytest-httpserver-1.1.2/.github/dependabot.yml000066400000000000000000000005161475715145300214550ustar00rootroot00000000000000 version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "monthly" groups: actions-deps: patterns: - "*" - package-ecosystem: "pip" directory: "/" schedule: interval: "monthly" groups: deps: patterns: - "*" pytest-httpserver-1.1.2/.github/workflows/000077500000000000000000000000001475715145300206605ustar00rootroot00000000000000pytest-httpserver-1.1.2/.github/workflows/ci.yml000066400000000000000000000041331475715145300217770ustar00rootroot00000000000000name: build on: push: branches: - master pull_request: branches: - master env: POETRY_VERSION: "1.8.4" jobs: cs: name: Coding style checks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup with: type: test python-version: "3.11" poetry-version: ${{ env.POETRY_VERSION }} - name: Style run: | make precommit - name: Lint run: | make mypy test: name: Test with python ${{ matrix.python-version }} / ${{ matrix.os-version }} strategy: matrix: python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"] os-version: ["ubuntu-latest", "windows-latest"] exclude: - os-version: windows-latest include: - os-version: windows-latest python-version: 3.13 runs-on: ${{ matrix.os-version }} steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup with: type: test python-version: ${{ matrix.python-version }} poetry-version: ${{ env.POETRY_VERSION }} - name: Test if: runner.os == 'Linux' run: | make cov - name: Test if: runner.os == 'Windows' shell: bash env: PYTEST_HTTPSERVER_HOST: '127.0.0.1' run: | set -e poetry run pytest tests -s -vv --release poetry run pytest tests -s -vv --ssl - name: Codecov upload uses: codecov/codecov-action@v5 if: runner.os == 'Linux' with: token: ${{ secrets.CODECOV_TOKEN }} test-doc: name: Test doc build runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup uses: ./.github/actions/setup with: type: doc python-version: "3.13" poetry-version: ${{ env.POETRY_VERSION }} - name: Test run: | make doc pytest-httpserver-1.1.2/.github/workflows/dependabot-validate.yml000066400000000000000000000007511475715145300253020ustar00rootroot00000000000000name: dependabot validate on: pull_request: paths: - '.github/dependabot.yml' - '.github/workflows/dependabot-validate.yml' jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: marocchino/validate-dependabot@v3 id: validate - uses: marocchino/sticky-pull-request-comment@v2 if: always() with: header: validate-dependabot message: ${{ steps.validate.outputs.markdown }} pytest-httpserver-1.1.2/.gitignore000066400000000000000000000002521475715145300172520ustar00rootroot00000000000000.venv/ .pytest_cache/ .vscode/.ropeproject/ *.egg-info/ .cache/ .coverage coverage.xml htmlcov/ build/ dist/ .eggs/ doc/_build/ .tox/ .idea/ .python-version __pycache__/ pytest-httpserver-1.1.2/.pre-commit-config.yaml000066400000000000000000000015761475715145300215550ustar00rootroot00000000000000repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.9.7 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] - repo: https://github.com/psf/black rev: 25.1.0 hooks: - id: black args: [--line-length=120, --safe] - repo: https://github.com/pre-commit/pre-commit-hooks rev: v5.0.0 hooks: - id: check-case-conflict - id: check-merge-conflict - id: check-symlinks - id: end-of-file-fixer - id: trailing-whitespace - id: debug-statements - id: requirements-txt-fixer - repo: https://github.com/pycqa/isort rev: 6.0.0 hooks: - id: isort name: isort (python) args: ['--force-single-line-imports', '--profile', 'black'] - repo: https://github.com/asottile/blacken-docs rev: 1.19.1 hooks: - id: blacken-docs additional_dependencies: [ black ] pytest-httpserver-1.1.2/.readthedocs.yml000066400000000000000000000006131475715145300203510ustar00rootroot00000000000000# .readthedocs.yaml # Read the Docs configuration file # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details version: 2 build: os: ubuntu-22.04 tools: python: "3.11" jobs: post_create_environment: - pip install poetry post_install: - VIRTUAL_ENV=$READTHEDOCS_VIRTUALENV_PATH poetry install --with doc sphinx: configuration: doc/conf.py pytest-httpserver-1.1.2/.vscode/000077500000000000000000000000001475715145300166245ustar00rootroot00000000000000pytest-httpserver-1.1.2/.vscode/settings.json000066400000000000000000000016121475715145300213570ustar00rootroot00000000000000{ "python.testing.pytestEnabled": true, "python.testing.pytestPath": "${workspaceFolder}/.venv/bin/pytest", "python.testing.pytestArgs": [ "tests" ], "editor.formatOnSave": true, "isort.args": [ "--force-single-line-imports", "--profile", "black" ], "files.trimTrailingWhitespace": true, "json.format.enable": true, "files.insertFinalNewline": true, "nixEnvSelector.nixFile": "${workspaceRoot}/shell.nix", "[python]": { "editor.defaultFormatter": "ms-python.black-formatter" }, "ruff.enable": true, "ruff.codeAction.disableRuleComment": { "enable": true }, "ruff.codeAction.fixViolation": { "enable": true }, "ruff.organizeImports": true, "ruff.path": [ ".venv/bin/ruff" ], "black-formatter.args": [ "--line-length=120", "--safe" ] } pytest-httpserver-1.1.2/CHANGES.rst000066400000000000000000000324351475715145300170740ustar00rootroot00000000000000============= Release Notes ============= .. _Release Notes_1.1.2: 1.1.2 ===== .. _Release Notes_1.1.2_Deprecation Notes: Deprecation Notes ----------------- - Python versions earlier than 3.9 have been deprecated in order to make the code more type safe. Python 3.8 has reached EOL on 2024-10-07. .. _Release Notes_1.1.1: 1.1.1 ===== .. _Release Notes_1.1.1_New Features: New Features ------------ - Add a new ``expect`` method to the ``HTTPServer`` object which allows developers to provide their own request matcher object. .. _Release Notes_1.1.0: 1.1.0 ===== .. _Release Notes_1.1.0_Bug Fixes: Bug Fixes --------- - Fixed an issue related to the leak of httpserver state between the tests when httpserver is destructed before the other fixtures. `#352 `_ .. _Release Notes_1.0.12: 1.0.12 ====== .. _Release Notes_1.0.12_Bug Fixes: Bug Fixes --------- - Fix pytest-httpserver's own tests related to log querying. No functional changes in pytest-httpserver code itself. `#345 `_ .. _Release Notes_1.0.11: 1.0.11 ====== .. _Release Notes_1.0.11_New Features: New Features ------------ - Hooks API - New methods added to query for matching requests in the log. - Threading support to serve requests in parallel .. _Release Notes_1.0.10: 1.0.10 ====== .. _Release Notes_1.0.10_New Features: New Features ------------ - When there's no handler for the request, add more details to the response sent by the server about the request to help debugging. .. _Release Notes_1.0.10_Other Notes: Other Notes ----------- - Use ruff for linting. It includes some source code changes which should not introduce functional changes, or API changes. .. _Release Notes_1.0.9: 1.0.9 ===== .. _Release Notes_1.0.9_New Features: New Features ------------ - Add ``__repr__`` to ``RequestHandler`` object so when it is compared (eg. with the ``log`` attribute of the server) it will show the matcher parameters. .. _Release Notes_1.0.8: 1.0.8 ===== .. _Release Notes_1.0.8_Bug Fixes: Bug Fixes --------- - Version 1.0.7 has been released with incorrect dependencies. This is fixed now. .. _Release Notes_1.0.7: 1.0.7 ===== .. _Release Notes_1.0.7_Upgrade Notes: Upgrade Notes ------------- - With werkzeug 2.3.x the headers type has been updated to not allow integers as header values. This restriction followed up in pytest-httpserver. .. _Release Notes_1.0.7_Deprecation Notes: Deprecation Notes ----------------- - Python versions earlier than 3.8 have been deprecated in order to support the latest werkzeug. Users using 3.7 or earlier python may use pytest-httpserver with earlier werkzeug versions but tests are no longer run for these python versions. .. _Release Notes_1.0.7_Bug Fixes: Bug Fixes --------- - Type hinting for header_value_matcher has been fixed. From now, specifying a callable as ``Callable[[str, Optional[str], str], bool]`` will be accepted also. Providing a ``HeaderValueMatcher`` object will be also accepted as before, as it provides the same callable signature. - Fix Werkzeug deprecation warning about ``parse_authorization_header`` call. Replace ``parse_authorization_header`` with ``Authorization.from_header`` as suggested. This fix should not introduce any functional change for the users. - Fix Werkzeug deprecation warning about ``werkzeug.urls.url_decode`` call. This call has been changed to ``urllib.parse.parse_qsl`` in the implementation. This fix should not introduce any functional change for the users. .. _Release Notes_1.0.6: 1.0.6 ===== .. _Release Notes_1.0.6_New Features: New Features ------------ - Add a new way of running tests with the blocking mode. In this mode, the http server is synchronized to the main thread and the client code is run in a separate thread. .. _Release Notes_1.0.6_Bug Fixes: Bug Fixes --------- - Python version classifier updated in pyproject.toml (which updates pypi also) .. _Release Notes_1.0.5: 1.0.5 ===== .. _Release Notes_1.0.5_Bug Fixes: Bug Fixes --------- - Packaging of sdist and the wheel fixed by adding the extra files only to the sdist and not to the wheel. .. _Release Notes_1.0.4: 1.0.4 ===== .. _Release Notes_1.0.4_Bug Fixes: Bug Fixes --------- - Fixed type hinting of ``HeaderValueMatcher.DEFAULT_MATCHERS``, which did not allow modifications, however it is explicitly allowed in the documentation. .. _Release Notes_1.0.4_Other Notes: Other Notes ----------- - Version of flake8 library updated to require 4.0.0+ at minimum. This is required to make flake8 working on recent python versions. .. _Release Notes_1.0.3: 1.0.3 ===== .. _Release Notes_1.0.3_New Features: New Features ------------ - Additional type hints improvements to make the library more mypy compliant. Imports in `__init__.py` have been updated to indicate that this is a namespace package. .. _Release Notes_1.0.3_Other Notes: Other Notes ----------- - Package deployment and CI has been migrated to poetry. poetry.lock will be kept up to date. Make target "quick-test" renamed to "test". Also, minor adjustments were made regarding documentation generation. Make targets should be identical. Build results like sdist, and wheel are almost identical to the one which was made by setuptools. .. _Release Notes_1.0.2: 1.0.2 ===== .. _Release Notes_1.0.2_New Features: New Features ------------ - Type hints updated to conform to 'mypy' type checking tool. Also, py.typed file is added as package data according to PEP 561. .. _Release Notes_1.0.2_Deprecation Notes: Deprecation Notes ----------------- - Python 3.4 and 3.5 versions have been deprecated in order to support type hints in the source code. Users using 3.5 and earlier releases encouraged to upgrade to later versions. Please node that 3.5 reached EOL in September of 2020 and no longer receives security fixes. .. _Release Notes_1.0.1: 1.0.1 ===== .. _Release Notes_1.0.1_New Features: New Features ------------ - Improved error handling of custom request handlers. Request handlers added with ``respond_with_handler`` now can use the ``assert`` statement. Those errors will be reported when a further ``check_assertions()`` call is made. Also, unhandled exceptions raised in the request handlers can be re-raised by calling the new ``check_handler_errors()`` method. A new method called ``check()`` has been added which calls these two in sequence. .. _Release Notes_1.0.0: 1.0.0 ===== .. _Release Notes_1.0.0_Prelude: Prelude ------- Functionally the same as 1.0.0rc1. For the list of changes between 0.3.8 and 1.0.0 see the changelist for 1.0.0rc1. .. _Release Notes_1.0.0rc1: 1.0.0rc1 ======== .. _Release Notes_1.0.0rc1_New Features: New Features ------------ - Added a new session scoped fixture ``make_httpserver`` which creates the object for the ``httpserver`` fixture. It can be overridden to add further customizations and it must yield a ``HTTPServer`` object - see ``pytest_plugin.py`` for an implementation. As this fixture is session scoped, it will be called only once when the first test using httpserver is started. This addition also deprecates the use of ``PluginHTTPServer`` which was used in earlier versions as a way to customize server object creation. ``PluginHTTPServer`` can still be used but it may be subject to deprecation in the future. - Added a new session scoped fixture ``httpserver_ssl_context`` which provides the SSL context for the server. By default it returns ``None``, so SSL is not enabled, but can be overridden to return a valid ``ssl.SSLContext`` object which will be used for SSL connections. See test_ssl.py for example. .. _Release Notes_1.0.0rc1_Upgrade Notes: Upgrade Notes ------------- - **Breaking change**: The scope of ``httpserver_listen_address`` fixture changed from **function** to **session**. This is a requirement to implement the other features listed in this release. See the `upgrade guide `_ for the details. .. _Release Notes_0.3.8: 0.3.8 ===== .. _Release Notes_0.3.8_Deprecation Notes: Deprecation Notes ----------------- - Deprecation warnings were added to prepare changes to 1.0.0. More details: https://pytest-httpserver.readthedocs.io/en/latest/upgrade.html .. _Release Notes_0.3.7: 0.3.7 ===== .. _Release Notes_0.3.7_Other Notes: Other Notes ----------- - Removed pytest-runner from setup.py as it is deprecated and makes packaging inconvenient as it needs to be installed before running setup.py. .. _Release Notes_0.3.6: 0.3.6 ===== .. _Release Notes_0.3.6_New Features: New Features ------------ - HTTP methods are case insensitive. The HTTP method specified is converted to uppercase in the library. - It is now possible to specify a JSON-serializable python value (such as dict, list, etc) and match the request to it as JSON. The request's body is loaded as JSON and it will be compared to the expected value. - The http response code sent when no handler is found for the request can be changed. It is set to 500 by default. .. _Release Notes_0.3.5: 0.3.5 ===== .. _Release Notes_0.3.5_New Features: New Features ------------ - Extend URI matching by allowing to specify URIPattern object or a compiled regular expression, which will be matched against the URI. URIPattern class is defined as abstract in the library so the user need to implement a new class based on it. .. _Release Notes_0.3.4: 0.3.4 ===== .. _Release Notes_0.3.4_Bug Fixes: Bug Fixes --------- - Fix the tests assets created for SSL/TLS tests by extending their expiration time. Also update the Makefile which can be used to update these assets. .. _Release Notes_0.3.3: 0.3.3 ===== .. _Release Notes_0.3.3_New Features: New Features ------------ - Besides bytes and string, dict and MultiDict objects can be specified as query_string. When these objects are used, the query string gets parsed into a dict (or MultiDict), and comparison is made accordingly. This enables the developer to ignore the order of the keys in the query_string when expecting a request. .. _Release Notes_0.3.3_Bug Fixes: Bug Fixes --------- - Fixed issue \#16 by converting string object passed as query_string to bytes which is the type of the query string in werkzeug, and also allowing bytes as the parameter. - Fix release tagging. 0.3.2 was released in a mistake by tagging 3.0.2 to the branch. .. _Release Notes_0.3.3_Other Notes: Other Notes ----------- - Add more files to source distribution (sdist). It now contains tests, assets, examples and other files. .. _Release Notes_0.3.1: 0.3.1 ===== .. _Release Notes_0.3.1_New Features: New Features ------------ - Add httpserver_listen_address fixture which is used to set up the bind address and port of the server. Setting bind address and port is possible by overriding this fixture. .. _Release Notes_0.3.0: 0.3.0 ===== .. _Release Notes_0.3.0_New Features: New Features ------------ - Support ephemeral port. This can be used by specify 0 as the port number to the HTTPServer instance. In such case, an unused port will be picked up and the server will start listening on that port. Querying the port attribute after server start reveals the real port where the server is actually listening. - Unify request functions of the HTTPServer class to make the API more straightforward to use. .. _Release Notes_0.3.0_Upgrade Notes: Upgrade Notes ------------- - The default port has been changed to 0, which results that the server will be staring on an ephemeral port. - The following methods of HTTPServer have been changed in a backward-incompatible way: * :py:meth:`pytest_httpserver.HTTPServer.expect_request` becomes a general function accepting handler_type parameter so it can create any kind of request handlers * :py:meth:`pytest_httpserver.HTTPServer.expect_oneshot_request` no longer accepts the ordered parameter, and it creates an unordered oneshot request handler * :py:meth:`pytest_httpserver.HTTPServer.expect_ordered_request` is a new method creating an ordered request handler .. _Release Notes_0.2.2: 0.2.2 ===== .. _Release Notes_0.2.2_New Features: New Features ------------ - Make it possible to intelligently compare headers. To accomplish that HeaderValueMatcher was added. It already contains logic to compare unknown headers and authorization headers. Patch by Roman Inflianskas. .. _Release Notes_0.2.1: 0.2.1 ===== .. _Release Notes_0.2.1_Prelude: Prelude ------- Minor fixes in setup.py and build environment. No actual code change in library .py files. .. _Release Notes_0.2: 0.2 === .. _Release Notes_0.2_New Features: New Features ------------ - When using pytest plugin, specifying the bind address and bind port can also be possible via environment variables. Setting PYTEST_HTTPSERVER_HOST and PYTEST_HTTPSERVER_PORT will change the bind host and bind port, respectively. - SSL/TLS support added with using the SSL/TLS support provided by werkzeug. This is based on the ssl module from the standard library. .. _Release Notes_0.1.1: 0.1.1 ===== .. _Release Notes_0.1.1_Prelude: Prelude ------- Minor fixes in setup.py and build environment. No actual code change in library .py files. .. _Release Notes_0.1: 0.1 === .. _Release Notes_0.1_Prelude: Prelude ------- First release pytest-httpserver-1.1.2/CONTRIBUTION.md000066400000000000000000000062311475715145300175260ustar00rootroot00000000000000# Contribution guide This document describes how to contribute to pytest-httpserver. In the case you want to add your own code to the source code, create a pull request, and it will be reviewed in a few days. Currently all developers working on this software in their spare time so please be patient. This software has only one main purpose: to be useful for the developers and the users, to help them to achieve what they intend to do with this small library. It was created by a few people who are doing it in their spare time. This piece of software is provided for free under the MIT license. There's a section in the documentation explaining the design decisions and the main concepts about the library. Please read it: https://pytest-httpserver.readthedocs.io/en/latest/background.html ## Rules There are a few rules you are kindly asked to accept: * Coding style is checked by `pre-commit`. You can run `make precommit` before proceeding with the PR. To install the pre-commit hooks to your git (so it will be run for each commit), run `pre-commit install`. * Tests should be written for the new code. If there's a complex logic implemented, it should be tested on different valid and invalid inputs and scenarios. * The software is released under the MIT license, which is simple and liberal. Due to the size of the project, there are no contribution agreements, but you are informally advised to accept that license. * It may be obvious but your code should make the software better, not worse. ## How to start developing * The development is arranged around a virtualenv which needs to be created by the `make dev` command. It will create it in the `.venv` directory. * You can let your IDE of your choice to use the `.venv/bin/python` interpreter, so it will know all the dependencies. * running tests on the localhost can be done by issuing `make test`. Note that the library can be used by many supported interpreters and unless it is absolutely required, we don't want to drop support. * running tests on multiple versions of interpreter locally can be done by `tox`. Keep in mind that the CI job uses github actions with caching for effective use, and `tox` is provided for the developers only. ## More technical details * Release notes must be written for significant changes. This is done by the `reno` tool. If you don't write any notes, no problem, it will be written by someone who merges your PR. * Documentation also needs to be written and updated. It means mostly docstrings, but if the PR changes the code and the way of working conceptually, the main documentation (located in the doc directory) needs to be updated and extended. * nix files are provided on a best-effort basis. `tox.nix` can be used to run `tox`, `shell.nix` can be used instead of poetry for development. No tests have been written for these (yet!), so they may be out of sync occasionally. * to release a new version, you can use the `scripts/release.py` script to make the wheels and sdist, generate the changelog, and tag the commit. This tool won't upload the artifacts as they need to be checked manually (by installing the wheel to a new venv, for example). pytest-httpserver-1.1.2/LICENSE000066400000000000000000000020551475715145300162720ustar00rootroot00000000000000MIT License Copyright (c) 2020 Zsolt Cserna Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. pytest-httpserver-1.1.2/Makefile000066400000000000000000000026051475715145300167260ustar00rootroot00000000000000export POETRY_VIRTUALENVS_IN_PROJECT=true EXTRAS ?= develop SPHINXOPTS ?= -n # do poetry install only in case if poetry.lock or pyproject.toml where updated and # we require a rebuilt. .venv/.st-venv-completed: poetry.lock pyproject.toml poetry install --verbose --with $(EXTRAS) touch .venv/.st-venv-completed .PHONY: dev dev: .venv/.st-venv-completed .PHONY: precommit precommit: dev poetry run pre-commit run -a .PHONY: mypy mypy: dev .venv/bin/mypy .PHONY: mrproper mrproper: clean rm -rf dist .PHONY: clean clean: cov-clean doc-clean rm -rf .venv *.egg-info build .eggs __pycache__ */__pycache__ .tox .PHONY: test test: dev .venv/bin/pytest tests -s -vv --release .venv/bin/pytest tests -s -vv --ssl .PHONY: test-pdb test-pdb: .venv/bin/pytest tests -s -vv --pdb .PHONY: cov cov: cov-clean .venv/bin/coverage run -m pytest -vv tests --release .venv/bin/coverage run -a -m pytest -vv tests --ssl .venv/bin/coverage xml .PHONY: cov-clean cov-clean: rm -rf htmlcov coverage.xml .coverage .PHONY: doc doc: dev .venv/bin/sphinx-build -M html doc doc/_build $(SPHINXOPTS) $(O) .PHONY: doc-clean doc-clean: rm -rf doc/_build .PHONY: doc-clean doc-preview: xdg-open doc/_build/html/index.html .PHONY: changes changes: dev .venv/bin/reno report --output CHANGES.rst --no-show-source poetry run pre-commit run --files CHANGES.rst || true poetry run pre-commit run --files CHANGES.rst pytest-httpserver-1.1.2/README.md000066400000000000000000000116201475715145300165420ustar00rootroot00000000000000[![Build Status](https://github.com/csernazs/pytest-httpserver/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/csernazs/pytest-httpserver/actions?query=workflow%3Abuild+branch%3Amaster) [![Documentation Status](https://readthedocs.org/projects/pytest-httpserver/badge/?version=latest)](https://pytest-httpserver.readthedocs.io/en/latest/?badge=latest) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) [![codecov](https://codecov.io/gh/csernazs/pytest-httpserver/branch/master/graph/badge.svg?token=MX2JXbHqRH)](https://codecov.io/gh/csernazs/pytest-httpserver) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Ruff](https://img.shields.io/endpoint?url=https://raw.githubusercontent.com/astral-sh/ruff/main/assets/badge/v2.json)](https://github.com/astral-sh/ruff) [![Downloads](https://static.pepy.tech/badge/pytest-httpserver/month)](https://pepy.tech/project/pytest-httpserver) [![pre-commit](https://img.shields.io/badge/pre--commit-enabled-brightgreen?logo=pre-commit)](https://github.com/pre-commit/pre-commit) ## pytest_httpserver HTTP server for pytest ### Nutshell This library is designed to help to test http clients without contacting the real http server. In other words, it is a fake http server which is accessible via localhost can be started with the pre-defined expected http requests and their responses. ### Example #### Handling a simple GET request ```python def test_my_client( httpserver, ): # httpserver is a pytest fixture which starts the server # set up the server to serve /foobar with the json httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) # check that the request is served assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} ``` #### Handing a POST request with an expected json body ```python def test_json_request( httpserver, ): # httpserver is a pytest fixture which starts the server # set up the server to serve /foobar with the json httpserver.expect_request( "/foobar", method="POST", json={"id": 12, "name": "foo"} ).respond_with_json({"foo": "bar"}) # check that the request is served assert requests.post( httpserver.url_for("/foobar"), json={"id": 12, "name": "foo"} ).json() == {"foo": "bar"} ``` You can also use the library without pytest. There's a with statement to ensure that the server is stopped. ```python with HTTPServer() as httpserver: # set up the server to serve /foobar with the json httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) # check that the request is served print(requests.get(httpserver.url_for("/foobar")).json()) ``` ### Documentation Please find the API documentation at https://pytest-httpserver.readthedocs.io/en/latest/. ### Features You can set up a dozen of expectations for the requests, and also what response should be sent by the server to the client. #### Requests There are three different types: - **permanent**: this will be always served when there's match for this request, you can make as many HTTP requests as you want - **oneshot**: this will be served only once when there's a match for this request, you can only make 1 HTTP request - **ordered**: same as oneshot but the order must be strictly matched to the order of setting up You can also fine-tune the expected request. The following can be specified: - URI (this is a must) - HTTP method - headers - query string - data (HTTP body of the request) - JSON (HTTP body loaded as JSON) #### Responses Once you have the expectations for the request set up, you should also define the response you want to send back. The following is supported currently: - respond arbitrary data (string or bytearray) - respond a json (a python dict converted in-place to json) - respond a Response object of werkzeug - use your own function Similar to requests, you can fine-tune what response you want to send: - HTTP status - headers - data #### Behave support Using the `BlockingHTTPServer` class, the assertion for a request and the response can be performed in real order. For more info, see the [test](tests/test_blocking_httpserver.py), the [howto](https://pytest-httpserver.readthedocs.io/en/latest/howto.html#running-httpserver-in-blocking-mode) and the [API documentation](https://pytest-httpserver.readthedocs.io/en/latest/api.html#blockinghttpserver). ### Missing features * HTTP/2 * Keepalive * ~~TLS~~ ### Donation Currently, this project is based heavily on werkzeug and pytest. Werkzeug does all the heavy lifting behind the scenes, parsing HTTP request and defining Request and Response objects, which are currently transparent in the API. If you wish to donate to werkzeug: https://palletsprojects.com/donate Pytest is the de-facto test library for python. If you wish to donate to pytest: https://opencollective.com/pytest pytest-httpserver-1.1.2/codecov.yml000066400000000000000000000002431475715145300174270ustar00rootroot00000000000000ignore: - "tests/**" coverage: status: project: default: target: 90% threshold: 50% patch: off pytest-httpserver-1.1.2/doc/000077500000000000000000000000001475715145300160305ustar00rootroot00000000000000pytest-httpserver-1.1.2/doc/Makefile000066400000000000000000000011471475715145300174730ustar00rootroot00000000000000# Minimal makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXPROJ = pytest_httpserver SOURCEDIR = . BUILDDIR = _build # Put it first so that "make" without argument is like "make help". help: @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) .PHONY: help Makefile # Catch-all target: route all unknown targets to Sphinx using the new # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). %: Makefile @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) pytest-httpserver-1.1.2/doc/_static/000077500000000000000000000000001475715145300174565ustar00rootroot00000000000000pytest-httpserver-1.1.2/doc/_static/.placeholder000066400000000000000000000000001475715145300217270ustar00rootroot00000000000000pytest-httpserver-1.1.2/doc/api.rst000066400000000000000000000040301475715145300173300ustar00rootroot00000000000000 .. _api-documentation: API documentation ================= pytest_httpserver ----------------- .. automodule:: pytest_httpserver HTTPServer ~~~~~~~~~~ .. autoclass:: HTTPServer :members: :inherited-members: RequestHandler ~~~~~~~~~~~~~~ .. autoclass:: RequestHandler :members: :inherited-members: RequestMatcher ~~~~~~~~~~~~~~ .. autoclass:: RequestMatcher :members: BlockingHTTPServer ~~~~~~~~~~~~~~~~~~ .. autoclass:: BlockingHTTPServer :members: :inherited-members: BlockingRequestHandler ~~~~~~~~~~~~~~~~~~~~~~ .. autoclass:: BlockingRequestHandler :members: :inherited-members: WaitingSettings ~~~~~~~~~~~~~~~ .. autoclass:: WaitingSettings :members: HeaderValueMatcher ~~~~~~~~~~~~~~~~~~ .. autoclass:: HeaderValueMatcher :members: URIPattern ~~~~~~~~~~ .. autoclass:: URIPattern :members: HTTPServerError ~~~~~~~~~~~~~~~ .. autoclass:: HTTPServerError :members: NoHandlerError ~~~~~~~~~~~~~~ .. autoclass:: NoHandlerError :members: pytest_httpserver.httpserver ---------------------------- This module contains some internal classes which are normally not instantiated by the user. .. automodule:: pytest_httpserver.httpserver .. autoclass:: RequestMatcher :members: .. autoclass:: pytest_httpserver.httpserver.HTTPServerBase :members: .. autoclass:: pytest_httpserver.httpserver.Error :members: .. autoclass:: pytest_httpserver.httpserver.NoHandlerError :members: .. autoclass:: pytest_httpserver.httpserver.HTTPServerError :members: .. autoclass:: pytest_httpserver.httpserver.RequestHandlerList :members: pytest_httpserver.hooks ----------------------- .. automodule:: pytest_httpserver.hooks .. autoclass:: pytest_httpserver.hooks.Chain :members: .. autoclass:: pytest_httpserver.hooks.Delay :members: .. autoclass:: pytest_httpserver.hooks.Garbage :members: pytest-httpserver-1.1.2/doc/background.rst000066400000000000000000000210601475715145300207000ustar00rootroot00000000000000.. _background: Background ========== This document describes what design decisions were made during the development of this library. It also describes how the library works in detail. This document assumes that you can use the library and have at least limited knowledge about the source code. If you feel that it is not true for you, you may want to read the :ref:`tutorial` and :ref:`howto`. API design ---------- The API should be simple for use to simple cases, but also provide great flexibility for the advanced cases. When increasing flexibility of the API it should not change the simple API unless it is absolutely required. API compatibility is paramount. API breaking is only allowed when it is on par with the gain of the new functionality. Adding new parameters to functions which have default value is not considered a breaking API change. Simple API ~~~~~~~~~~ API should be kept as simple as possible. It means that describing an expected request and its response should be trivial for the user. For this reason, the API is flat: it contains a handful of functions which have many parameters accepting built-in python types (such as bytes, string, int, etc) in contrast to more classes and functions with less arguments. This API allows to define an expected request and the response which will be sent back to the client in a single line. This is one of the key features so using the library is not complicated. Example: .. literalinclude :: ../tests/examples/test_example_query_params1.py :language: python It is simple in the most simple cases, but once the expectation is more specific, the line can grow significantly, so here the user is expected to put the literals into variables: .. literalinclude :: ../tests/examples/test_example_query_params2.py :language: python If the user wants something more complex, classes are available for this which can be instantiated and then specified for the parameters normally accepting only built-in types. The easy case should be made easy, with the possibility of making advanced things in a bit more complex way. Flexible API ~~~~~~~~~~~~ The API should be also made flexible as possible but it should not break the simple API and not make the simple API complicated. A good example for this is the `respond_with_handler` method, which accepts a callable object (eg. a function) which receives the request object and returns the response object. The user can implement the required logic there. Adding this flexibility however did not cause any change in the simple API, the simple cases can be still used as before. Higher-level API ~~~~~~~~~~~~~~~~ In the early days of this library, it wanted to support the low-level http protocol elements: request status, headers, etc to provide full coverage for the protocol itself. This was made in order to make the most advanced customizations possible. Then the project received a few PRs adding `HeaderValueMatcher` and support for authorization which relied on the low-level API to add a higher-level API without breaking it. In the opposite case, adding a low-level API to a high-level would not be possible. Transparency ~~~~~~~~~~~~ The API provided by *pytest-httpserver* is transparent. That means that the objects (most importantly the `Request` and `Response` objects) defined by *werkzeug* are visible by the user of *pytest-httpserver*, there is no wrapping made. This is done by the sake of simplicity. As *werkzeug* provides a stable API, there's no need to change this in the future, however this also limits the library to stick with *werkzeug* in the long term. Replacing *werkzeug* to something else would break the API due to this transparency. Requirements ------------ This section describes how to work with pytest-httpserver's requirements. These are the packages used by the library. Number of requirements ~~~~~~~~~~~~~~~~~~~~~~ It is required to keep the requirements at minimum. When adding a new library to the package requirements, research in the following topics should be done: * code quality * activity of the development and maintenance * number of open issues, and their content * how many people using that library * python interpreter versions supported * amount of API breaking changes * license Sometimes, it is better to have the own implementation instead of having a tiny library added to the requirements, which may cause compatibility issues. Requirements version restrictions ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ In general, the package requirements should have no version restrictions. For example, the *werkzeug* library has no restrictions, which means that if a new version comes out of it, it is assumed that *pytest-httpserver* will be able to run with it. Many people uses this library in an environment having full of other packages and limiting version here will limit their versions in their requirements also. For example if there's a software using *werkzeug* `1.0.0` and our requirements have `<0.9` specified it will make *pytest-httpserver* incompatible with their software. Requirements testing ~~~~~~~~~~~~~~~~~~~~ Currently it is required to test with only the latest version of the required packages. However, if there's an API breaking change which affects *pytest-httpserver*, a decision should be made: * apply version restrictions, possibly making *pytest-httpserver* incompatible with some other software * add workaround to *pytest-httpserver* to support both APIs HTTP server ----------- The chosen HTTP server which drives this library is implemented by the *werkzeug* library. The reason behind this decision is that *werkzeug* is used by Flask, a very popular web framework and it provides a proven, stable API in the long term. Supported python versions ------------------------- Supporting the latest python versions (such as 3.7 and 3.8 at the time of writing this), is a must. Supporting the older versions is preferred, following the state of the officially supported python versions by PSF. The library should be tested periodically on the supported versions. Dropping support for old python versions is possible if supporting would cause an issue or require extensive workaround. Python support for a given version is also dropped if it is near to the end of support or when a dependency deprecates it - this is needed to move forward with the community in order to support the latest versions of the dependencies. Testing and coverage -------------------- It is not required to have 100% test coverage but all possible use-cases should be covered. Github actions is used to test the library on all the supported python versions, and tox.ini is provided if local testing is desired. When a bug is reported, there should be a test for it, which would re-produce the error and it should pass with the fix. Server starting and stopping ---------------------------- The server is started when the first test is run which uses the httpserver fixture. It will be running till the end of the session, and new tests will use the same instance. A cleanup is done between the tests which restores the clean state (no handlers registered, empty log, etc) to avoid cross-talk. The reason behind this is the time required to stop the server. For some reason, *werkzeug* (the http server used) needs about 1 second to stop itself. Adding this time to each test is not acceptable in most of the cases. Note that it is still compatible with *pytest-xdist* (a popular pytest extension to run the tests in parallel) as in such case, distinct test sessions will be run and those will have their own http server instance. Fixture scope ------------- Due to the nature of the http server (it is run only once), it seems to be a good recommendation to keep the httpserver fixture session scoped, not function scoped. The problem is that the cleanup which needs to be done between the tests (as the server is run only once, see above), and that cleanup needs to be attached to a function scoped fixture. HTTP port selection ------------------- In early versions of the library, the user had to specify which port the server should be bound. This later changed to have an so-called ephemeral port, which is a random free port number chosen by the kernel. It is good because it guarantees that it will be available and it allows parallel test runnings for example. In some cases it is not desired (eg if the code being tested has wired-in port number), in such cases it is still possible to specify the port number. Also, the host can be specified which allows to bind on "0.0.0.0" so the server is accessible from the network in case you want to test a javascript code running on a different server in a browser. pytest-httpserver-1.1.2/doc/changes.rst000066400000000000000000000000521475715145300201670ustar00rootroot00000000000000.. _changes: .. include:: ../CHANGES.rst pytest-httpserver-1.1.2/doc/conf.py000066400000000000000000000134241475715145300173330ustar00rootroot00000000000000#!/usr/bin/env python3 # -*- coding: utf-8 -*- # # pytest_httpserver documentation build configuration file, created by # sphinx-quickstart on Sat Aug 11 08:07:37 2018. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # import os import sys from typing import Dict sys.path.insert(0, os.path.abspath("..")) import doc.patch # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ "sphinx.ext.autodoc", "sphinx.ext.intersphinx", "sphinx.ext.autosectionlabel", ] intersphinx_mapping = { "python": ("https://docs.python.org/3", (None, "python-inv.txt")), "werkzeug": ("https://werkzeug.palletsprojects.com/en/3.0.x", None), } # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # # source_suffix = ['.rst', '.md'] source_suffix = ".rst" # The master toctree document. master_doc = "index" # General information about the project. project = "pytest_httpserver" copyright = "2020, Zsolt Cserna" author = "Zsolt Cserna" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. version = "1.1.2" # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. language = "en" # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This patterns also effect to html_static_path and html_extra_path exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. pygments_style = "sphinx" # If true, `todo` and `todoList` produce output, else they produce nothing. todo_include_todos = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # html_theme = "sphinx_rtd_theme" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # # html_theme_options = {} html_theme_options = { "canonical_url": "", "analytics_id": "", "logo_only": False, "display_version": True, "prev_next_buttons_location": "bottom", "style_external_links": False, # Toc options "collapse_navigation": True, "sticky_navigation": True, "navigation_depth": 4, "includehidden": True, "titles_only": False, } # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ["_static"] # Custom sidebar templates, must be a dictionary that maps document names # to template names. # # This is required for the alabaster theme # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars html_sidebars = { "**": [ "relations.html", # needs 'show_related': True theme option to display "searchbox.html", ] } # -- Options for HTMLHelp output ------------------------------------------ # Output file base name for HTML help builder. htmlhelp_basename = "pytest_httpserverdoc" # -- Options for LaTeX output --------------------------------------------- latex_elements: Dict[str, str] = { # The paper size ('letterpaper' or 'a4paper'). # # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # # 'preamble': '', # Latex figure (float) alignment # # 'figure_align': 'htbp', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ (master_doc, "pytest_httpserver.tex", "pytest\\_httpserver Documentation", "Zsolt Cserna", "manual"), ] # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [(master_doc, "pytest_httpserver", "pytest_httpserver Documentation", [author], 1)] # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ( master_doc, "pytest_httpserver", "pytest_httpserver Documentation", author, "pytest_httpserver", "One line description of project.", "Miscellaneous", ), ] pytest-httpserver-1.1.2/doc/fixtures.rst000066400000000000000000000045641475715145300204440ustar00rootroot00000000000000 .. _fixtures: Fixtures ======== pytest-httpserver provides the following pytest fixtures. These fixtures can be overridden the usual name, by defining a fixture with the same name. Make sure that you are defining the fixture with the same scope as the original one. For more details, read the related part of the `pytest howto`_. httpserver ---------- Scope function Type :py:class:`pytest_httpserver.HTTPServer` This fixture provides the main functionality for the library. It is a httpserver instance where you can add your handlers and expectations. It is a function scoped fixture as the server's state needs to be cleared between the tests. httpserver_listen_address ------------------------- Scope session (in 1.0.0 and above, *function* otherwise) Type: ``Tuple[str, int]`` Default: ``("localhost", 0)`` This fixture can return the address and port where the server will bind. If port is given is 0, the server to an ephemeral port, which is an available randomly selected port. If you run your tests in parallel, this should be used so multiple servers can be started. httpserver_listen_address httpserver_ssl_context ---------------------- Scope session Type: ``ssl.SSLContext`` Default: ``None`` This fixture should return the ssl context which will be used to run a https server. For more details please see the `ssl`_ module documentation of the standard library. make_httpserver --------------- Scope session Type: :py:class:`pytest_httpserver.HTTPServer` Default: A running :py:class:`pytest_httpserver.HTTPServer` instance. This is a factory fixture which creates the instance of the httpserver which will be used by the ``httpserver`` fixture. By default, it uses the ``httpserver_listen_address`` and the ``httpserver_ssl_context`` fixtures but can be overridden to add more customization. It yields a running HTTPServer instance and also stops it when it is no longer needed at the end of the session. If you want to customize this fixture it is highly recommended to look at its definition in `pytest_plugin.py`_. .. _pytest_plugin.py: https://github.com/csernazs/pytest-httpserver/blob/master/pytest_httpserver/pytest_plugin.py .. _pytest howto: https://docs.pytest.org/en/documentation-restructure/how-to/fixture.html#overriding-fixtures-on-various-levels .. _ssl: https://docs.python.org/3/library/ssl.html pytest-httpserver-1.1.2/doc/guide.rst000066400000000000000000000001531475715145300176560ustar00rootroot00000000000000 User's Guide ============ User's guide has been superseded by the :ref:`tutorial` and the :ref:`howto`. pytest-httpserver-1.1.2/doc/howto.rst000066400000000000000000000561631475715145300177350ustar00rootroot00000000000000 .. _howto: Howto ===== This documentation is a collection of the most common use cases, and their solutions. If you have not used this library before, it may be better to read the :ref:`tutorial` first. Matching query parameters ------------------------- To match query parameters, you must not included them to the URI, as this will not work: .. literalinclude :: ../tests/examples/test_howto_query_params_never_do_this.py :language: python There's an explicit place where the query string should go: .. literalinclude :: ../tests/examples/test_howto_query_params_proper_use.py :language: python The ``query_string`` is the parameter which does not contain the leading question mark ``?``. .. note:: The reason behind this is the underlying http server library *werkzeug*, which provides the ``Request`` object which is used for the matching the request with the handlers. This object has the ``query_string`` attribute which contains the query. As the order of the parameters in the query string usually does not matter, you can specify a dict for the ``query_string`` parameter (the naming may look a bit strange but we wanted to keep API compatibility and this dict matching feature was added later). .. literalinclude :: ../tests/examples/test_howto_query_params_dict.py :language: python In the example above, both requests pass the test as we specified the expected query string as a dictionary. Behind the scenes an additional step is done by the library: it parses up the query_string into the dict and then compares it with the dict provided. URI matching ------------ The simplest form of URI matching is providing as a string. This is a equality match, if the URI of the request is not equal with the specified one, the request will not be handled. If this is not desired, you can specify a regexp object (returned by the ``re.compile()`` call). .. literalinclude :: ../tests/examples/test_howto_regexp.py :language: python The above will match every URI starting with "/foo". There's an additional way to extend this functionality. You can specify your own method which will receive the URI. All you need is to subclass from the ``URIPattern`` class and define the ``match()`` method which will get the uri as string and should return a boolean value. .. literalinclude :: ../tests/examples/test_howto_url_matcher.py :language: python Authentication -------------- When doing http digest authentication, the client may send a request like this: .. code:: GET /dir/index.html HTTP/1.0 Host: localhost Authorization: Digest username="Mufasa", realm="testrealm@host.com", nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093", uri="/dir/index.html", qop=auth, nc=00000001, cnonce="0a4f113b", response="6629fae49393a05397450978507c4ef1", opaque="5ccc069c403ebaf9f0171e9517f40e41" Implementing a matcher is difficult for this request as the order of the parameters in the ``Authorization`` header value is arbitrary. By default, pytest-httpserver includes an Authorization header parser so the order of the parameters in the ``Authorization`` header does not matter. .. literalinclude :: ../tests/examples/test_howto_authorization_headers.py :language: python JSON matching ------------- Matching the request data can be done in two different ways. One way is to provide a python string (or bytes object) whose value will be compared to the request body. When the request contains a json, matching to will be error prone as an object can be represented as json in different ways, for example when different length of indentation is used. To match the body as json, you need to add the python data structure (which could be dict, list or anything which can be the result of `json.loads()` call). The request's body will be loaded as json and the result will be compared to the provided object. If the request's body cannot be loaded as json, the matcher will fail and *pytest-httpserver* will proceed with the next registered matcher. Example: .. literalinclude :: ../tests/examples/test_howto_json_matcher.py :language: python .. note:: JSON requests usually come with ``Content-Type: application/json`` header. *pytest-httpserver* provides the *headers* parameter to match the headers of the request, however matching json body does not imply matching the *Content-Type* header. If matching the header is intended, specify the expected *Content-Type* header and its value to the headers parameter. .. note:: *json* and *data* parameters are mutually exclusive so both of then cannot be specified as in such case the behavior is ambiguous. .. note:: The request body is decoded by using the *data_encoding* parameter, which is default to *utf-8*. If the request comes in a different encoding, and the decoding fails, the request won't match with the expected json. Advanced header matching ------------------------ For each http header, you can specify a callable object (eg. a python function) which will be called with the header name, header actual value and the expected value, and will be able to determine the matching. You need to implement such a function and then use it: .. literalinclude :: ../tests/examples/test_howto_case_insensitive_matcher.py :language: python .. note:: Header value matcher is the basis of the ``Authorization`` header parsing. If you want to change the matching of only one header, you may want to use the ``HeaderValueMatcher`` class. In case you want to do it globally, you can add the header name and the callable to the ``HeaderValueMatcher.DEFAULT_MATCHERS`` dict. .. code:: python from pytest_httpserver import HeaderValueMatcher def case_insensitive_compare(actual: str, expected: str) -> bool: return actual.lower() == expected.lower() HeaderValueMatcher.DEFAULT_MATCHERS["X-Foo"] = case_insensitive_compare def test_case_insensitive_matching(httpserver: HTTPServer): httpserver.expect_request("/", headers={"X-Foo": "bar"}).respond_with_data("OK") assert ( requests.get(httpserver.url_for("/"), headers={"X-Foo": "bar"}).status_code == 200 ) assert ( requests.get(httpserver.url_for("/"), headers={"X-Foo": "BAR"}).status_code == 200 ) In case you don't want to change the defaults, you can provide the ``HeaderValueMatcher`` object itself. .. literalinclude :: ../tests/examples/test_howto_header_value_matcher.py :language: python Using custom request handler ---------------------------- In the case the response is not static, for example it depends on the request, you can pass a function to the ``respond_with_handler`` function. This function will be called with a request object and it should return a Response object. .. literalinclude :: ../tests/examples/test_howto_custom_handler.py :language: python The above code implements a handler which returns a random number between 1 and 10. Not particularly useful but shows that the handler can return any computed or derived value. In the response handler you can also use the ``assert`` statement, similar to the tests, but there's a big difference. As the server is running in its own thread, this will cause a HTTP 500 error returned, and the exception registered into a list. To get that error, you need to call ``check_assertions()`` method of the httpserver. In case you want to ensure that there was no other exception raised which was unhandled, you can call the ``check_handler_errors()`` method of the httpserver. Two notable examples for this: .. literalinclude :: ../tests/examples/test_howto_check_handler_errors.py :language: python If you want to call both methods (``check_handler_errors()`` and ``check_assertions()``) you can call the ``check()`` method, which will call these. .. literalinclude :: ../tests/examples/test_howto_check.py :language: python .. note:: The scope of the errors checked by the ``check()`` method may change in the future - it is added to check all possible errors happened in the server. Using custom request matcher ---------------------------- In the case when you want to extend or modify the request matcher in *pytest-httpserrver*, then you can use your own request matcher. Example: .. literalinclude :: ../tests/examples/test_howto_custom_request_matcher.py :language: python Customizing host and port ------------------------- By default, the server run by pytest-httpserver will listen on localhost on a random available port. In most cases it works well as you want to test your app in the local environment. If you need to change this behavior, there are a plenty of options. It is very important to make these changes before starting the server, eg. before running any test using the httpserver fixture. Use IP address *0.0.0.0* to listen globally. .. warning:: You should be careful when listening on a non-local ip (such as *0.0.0.0*). In this case anyone knowing your machine's IP address and the port can connect to the server. Environment variables ~~~~~~~~~~~~~~~~~~~~~ Set ``PYTEST_HTTPSERVER_HOST`` and/or ``PYTEST_HTTPSERVER_PORT`` environment variables to the desired values. Class attributes ~~~~~~~~~~~~~~~~ Changing ``HTTPServer.DEFAULT_LISTEN_HOST`` and ``HTTPServer.DEFAULT_LISTEN_PORT`` attributes. Make sure that you do this before running any test requiring the ``httpserver`` fixture. One ideal place for this is putting it into ``conftest.py``. Fixture ~~~~~~~ Overriding the ``httpserver_listen_address`` fixture. Similar to the solutions above, this needs to be done before starting the server (eg. before referencing the ``httpserver`` fixture). .. code-block:: python import pytest @pytest.fixture(scope="session") def httpserver_listen_address(): return ("127.0.0.1", 8000) Multi-threading support ----------------------- When your client runs in a thread, everything completes without waiting for the first response. To overcome this problem, you can wait until all the handlers have been served or there's some error happened. This is available only for oneshot and ordered handlers, as permanent handlers last forever. To have this feature enabled, use the context object returned by the ``wait()`` method of the ``httpserver`` object. This method accepts the following parameters: * raise_assertions: whether raise assertions on unexpected request or timeout or not * stop_on_nohandler: whether stop on unexpected request or not * timeout: time (in seconds) until time is out Behind the scenes it synchronizes the state of the server with the main thread. Last, you need to assert on the ``result`` attribute of the context object. .. literalinclude :: ../tests/examples/test_howto_wait_success.py :language: python In the above code, all the request.get() calls could be in a different thread, eg. running in parallel, but the exit condition of the context object is to wait for the specified conditions. Emulating connection refused error ---------------------------------- If by any chance, you want to emulate network errors such as *Connection reset by peer* or *Connection refused*, you can simply do it by connecting to a random port number where no service is listening: .. literalinclude :: ../tests/examples/test_howto_timeout_requests.py :language: python However, connecting to the port where the httpserver had been started will still succeed as the server is running continuously. This is working by design as starting/stopping the server is costly. .. code-block:: python import pytest import requests # setting a fixed port for httpserver @pytest.fixture(scope="session") def httpserver_listen_address(): return ("127.0.0.1", 8000) # this test will pass def test_normal_connection(httpserver): httpserver.expect_request("/foo").respond_with_data("foo") assert requests.get("http://localhost:8000/foo").text == "foo" # this tess will FAIL, as httpserver started in test_normal_connection is # still running def test_connection_refused(): with pytest.raises(requests.exceptions.ConnectionError): # this won't get Connection refused error as the server is still # running. # it will get HTTP status 500 as the handlers registered in # test_normal_connection have been removed requests.get("http://localhost:8000/foo") To solve the issue, the httpserver can be stopped explicitly. It will start implicitly when the first test starts to use it. So the ``test_connection_refused`` test can be re-written to this: .. code-block:: python def test_connection_refused(httpserver): httpserver.stop() # stop the server explicitly with pytest.raises(requests.exceptions.ConnectionError): requests.get("http://localhost:8000/foo") Emulating timeout ----------------- To emulate timeout, there's one way to register a handler function which will sleep for a given amount of time. .. code-block:: python import time from pytest_httpserver import HTTPServer import pytest import requests def sleeping(request): time.sleep(2) # this should be greater than the client's timeout parameter def test_timeout(httpserver: HTTPServer): httpserver.expect_request("/baz").respond_with_handler(sleeping) with pytest.raises(requests.exceptions.ReadTimeout): assert requests.get(httpserver.url_for("/baz"), timeout=1) There's one drawback though: the test takes 2 seconds to run as it waits the handler thread to be completed. Running an HTTPS server ----------------------- To run an https server, `trustme` can be used to do the heavy lifting: .. code-block:: python @pytest.fixture(scope="session") def ca(): return trustme.CA() @pytest.fixture(scope="session") def httpserver_ssl_context(ca): context = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER) localhost_cert = ca.issue_cert("localhost") localhost_cert.configure_cert(context) return context @pytest.fixture(scope="session") def httpclient_ssl_context(ca): with ca.cert_pem.tempfile() as ca_temp_path: return ssl.create_default_context(cafile=ca_temp_path) @pytest.mark.asyncio async def test_aiohttp(httpserver, httpclient_ssl_context): import aiohttp httpserver.expect_request("/").respond_with_data("hello world!") connector = aiohttp.TCPConnector(ssl=httpclient_ssl_context) async with aiohttp.ClientSession(connector=connector) as session: async with session.get(httpserver.url_for("/")) as result: assert (await result.text()) == "hello world!" def test_requests(httpserver, ca): import requests httpserver.expect_request("/").respond_with_data("hello world!") with ca.cert_pem.tempfile() as ca_temp_path: result = requests.get(httpserver.url_for("/"), verify=ca_temp_path) assert result.text == "hello world!" def test_httpx(httpserver, httpclient_ssl_context): import httpx httpserver.expect_request("/").respond_with_data("hello world!") result = httpx.get(httpserver.url_for("/"), verify=httpclient_ssl_context) assert result.text == "hello world!" Using httpserver on a dual-stack (IPv4 and IPv6) system ------------------------------------------------------- *pytest-httpserver* can only listen on one address and it also means that address family is determined by that. As it relies on *Werkzeug*, it passes the provided host parameter to it and then it is up to *Werkzeug* how the port binding is done. *Werkzeug* determines the address family by examining the string provided. If it contains a colon (``:``) then it will be an IPv6 (``AF_INET6``) socket, otherwise, it will be an IPv4 (``AF_INET``) socket. The default string in *pytest-httpserver* is ``localhost`` so by default, the httpserver listens on IPv4. If you want it to listen on IPv6 address, provide an IPv6 address (``::1`` for example) to it. It should be noted that dual-stack systems are still working with *pytest-httpserver* because the clients obtain the possible addresses for the a given name by calling ``getaddrinfo()`` or similar function which returns the addresses together with address families, and the client iterates over this list. In the case when *pytest-httpserver* is listening on ``127.0.0.1``, and the client uses ``localhost`` name in the url, it will try ``::1`` first, and then it will move on to ``127.0.0.1``, which will succeed, or vica-versa, where ``127.0.0.1`` will be successful first. If you want to test a connection error case in your test (such as TLS error), the client can fail in a strange way as we seen in `this issue `_. In such case, client tries with ``127.0.0.1`` first, then reaches a TLS error (which is normal as the test case is about testing for the TLS issue), then it moves on to ``::1``, then it fails with ``Connection reset``. In such case fixing the bind address to ``127.0.0.1`` (and thereby fixing the host part of the URL returned by the `url_for` call) solves the issue as the client will receive the address (``127.0.0.1``) instead of the name (``localhost``) so it won't move on to the IPv6 address. Running httpserver in blocking mode ----------------------------------- In this mode, the code which is being tested (the client) is executed in a background thread, while the server events are synchronized to the main thread, so it looks like it is running in the main thread. This allows to catch the assertions occured on the server side synchronously, and assertions are raised to the main thread. You need to call `check_assertions` at the end for only the unexpected requests. This is an experimental feature so *pytest-httpserver* has no fixture for it yet. If you find this feature useful any you have ideas or suggestions related to this, feel free to open an issue. Example: .. literalinclude :: ../tests/examples/test_example_blocking_httpserver.py :language: python Querying the log ---------------- *pytest-httpserver* keeps a log of request-response pairs in a python list. This log can be accessed by the ``log`` attibute of the httpserver instance, but there are methods made specifically to query the log. Each of the log querying methods accepts a :py:class:`pytest_httpserver.RequestMatcher` object which uses the same matching logic which is used by the server itself. Its parameters are the same to the parameters specified for the server's `except_request` (and the similar) methods. The methods for querying: * :py:meth:`pytest_httpserver.HTTPServer.get_matching_requests_count` returns how many requests are matching in the log as an int * :py:meth:`pytest_httpserver.HTTPServer.assert_request_made` asserts the given amount of requests are matching in the log. By default it checks for one (1) request but other value can be specified. For example, 0 can be specified to check for requests not made. * :py:meth:`pytest_httpserver.HTTPServer.iter_matching_requests` is a generator yielding Request-Response tuples of the matching entries in the log. This offers greater flexibility (compared to the other methods) Example: .. literalinclude :: ../tests/examples/test_howto_log_querying.py :language: python Serving requests in parallel ---------------------------- *pytest-httpserver* serves the request in a single-threaded, blocking way. That means that if multiple requests are made to it, those will be served one by one. There can be cases where parallel processing is required, for those cases *pytest-httpserver* allows running a server which start one thread per request handler, so the requests are served in parallel way (depending on Global Interpreter Lock this is not truly parallel, but from the I/O point of view it is). To set this up, you have two possibilities. Overriding httpserver fixture ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ One is to customize how the HTTPServer object is created. This is possible by defining the following fixture: .. code:: python @pytest.fixture(scope="session") def make_httpserver() -> Iterable[HTTPServer]: server = HTTPServer(threaded=True) # set threaded=True to enable thread support server.start() yield server server.clear() if server.is_running(): server.stop() This will override the ``httpserver`` fixture in your tests. Creating a different httpserver fixture ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ This way, you can create a different httpserver fixture and you can use it besides the main one. .. code:: python @pytest.fixture() def threaded() -> Iterable[HTTPServer]: server = HTTPServer(threaded=True) server.start() yield server server.clear() if server.is_running(): server.stop() def test_threaded(threaded: HTTPServer): ... This will start and stop the server for each tests, which causes about 0.5 seconds waiting when the server is stopped. It won't override the ``httpserver`` fixture so you can keep the original single-threaded behavior. .. warning:: Handler threads which are still running when the test is finished, will be left behind and won't be join()ed between the tests. If you want to ensure that all threads are properly cleaned up and you want to wait for them, consider using the second option (:ref:`Creating a different httpserver fixture`) described above. Adding side effects ------------------- Sometimes there's a need to add side effects to the handling of the requests. Such side effect could be adding some amount of delay to the serving or adding some garbage to response data. While these can be achieved by using :py:meth:`pytest_httpserver.RequestHandler.respond_with_handler` where you can implement your own function to serve the request, *pytest-httpserver* provides a hooks API where you can add side effects to request handlers such as :py:meth:`pytest_httpserver.RequestHandler.respond_with_json` and others. This allows to use the existing API of registering handlers. Example: .. literalinclude :: ../tests/examples/test_howto_hooks.py :language: python :py:mod:`pytest_httpserver.hooks` module provides some pre-defined hooks to use. You can implement your own hook as well. The requirement is to have a callable object (a function) ``Callable[[Request, Response], Response]``. In details: * Parameter :py:class:`werkzeug.Request` which represents the request sent by the client. * Parameter :py:class:`werkzeug.Response` which represents the response made by the handler. * Returns a :py:class:`werkzeug.Response` object which represents the response will be returned to the client. Example: .. literalinclude :: ../tests/examples/test_howto_custom_hooks.py :language: python ``with_post_hook`` can be called multiple times, in this case *pytest-httpserver* will register the hooks, and hooks will be called sequentially, one by one. Each hook will receive the response what the previous hook returned, and the last hook called will return the final response which will be sent back to the client. pytest-httpserver-1.1.2/doc/index.rst000066400000000000000000000024401475715145300176710ustar00rootroot00000000000000 pytest-httpserver ================= pytest-httpserver is a python package which allows you to start a real HTTP server for your tests. The server can be configured programmatically to how to respond to requests. This project aims to provide an easy to use API to start the server, configure the request handlers and then shut it down gracefully. All of these without touching a configuration file or dealing with daemons. As the HTTP server is spawned in a different thread and listening on a TCP port, you can use any HTTP client. This library also helps you migrating to a different HTTP client library without the need to re-write any test for your client application. This library can be used with pytest most conveniently but if you prefer to use other test frameworks, you can still use it with the context API or by writing a wrapper for it. Example ------- .. code:: python import requests def test_json_client(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} For further details, please read the :doc:`guide` or the :doc:`api`. .. toctree:: :maxdepth: 2 tutorial howto fixtures api background changes upgrade pytest-httpserver-1.1.2/doc/patch.py000066400000000000000000000007771475715145300175140ustar00rootroot00000000000000# this is required to make sphinx able to find references for classes put inside # typing.TYPE_CHECKING block from ssl import SSLContext from werkzeug import Request from werkzeug import Response import pytest_httpserver.blocking_httpserver import pytest_httpserver.httpserver pytest_httpserver.httpserver.SSLContext = SSLContext pytest_httpserver.blocking_httpserver.SSLContext = SSLContext pytest_httpserver.blocking_httpserver.Request = Request pytest_httpserver.blocking_httpserver.Response = Response pytest-httpserver-1.1.2/doc/tutorial.rst000066400000000000000000000431451475715145300204340ustar00rootroot00000000000000 .. _tutorial: Tutorial ======== If you haven't worked with this library yet, this document is for you. Writing your first test ----------------------- With pytest-httpserver, a test looks like this: .. code:: python import requests def test_json_client(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} In the first line of the code, we are setting up an expectation. The expectation contains the http request which is expected to be made: .. code:: python httpserver.expect_request("/foobar") This code tells that the httpserver, which is started automatically and running on localhost, should accept the request "http://localhost/foobar". Configuring how to handle this request is then done with the following method: .. code:: python respond_with_json({"foo": "bar"}) This tells that when the request arrives to the *http://localhost/foobar* URL, it must respond with the provided json. The library accepts here any python object which is json serializable. Here, a dict is provided. .. note:: It is important to specify what response to be sent back to the client otherwise *pytest-httpserver* will error with ``Matching request handler found but no response defined`` message on an incoming request. In the next line, an http request is sent with the *requests* library: .. code:: python assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} There's no customization (such as mocking) to be made. You don't need to figure out the port number where the server is running, as there's the ``url_for()`` method provided to format the URL. As you can see there are two different part of the httpserver configuration: 1. setting up what kind of request we are expecting 2. telling how the request should be handled and which content should be responded. Important note on server port number ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The test should be run with an unprivileged user. As it is not possible to bind to the default http port (80), the library binds the server to an available port which is higher than 1024. In the examples on this page when we are referring to the url *http://localhost/...* it is assumed that the url contains the http port also. It is advised to use the ``url_for()`` method to construct an URL as it will always contain the correct port number in the URL. If you need the http port as an integer, you can get it by the ``port`` attribute of the ``httpserver`` object. How to test your http client ---------------------------- .. note:: This section describes the various ways of http client testing. If you are sure that pytest-httpserver is the right library for you, you can skip this section. You've written your first http client application and you want to write a test for it. You have the following options: 1. Test your application against the production http server 2. Mock your http calls, so they won't reach any real server 3. Run a fake http server listening on localhost behaving like the real http server pytest-httpserver provides API for the 3rd option: it runs a real http server on localhost so you can test your client connecting to it. However, there's no silver bullet and the possibilities above have their pros and cons. Test your application against the production http server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pros: * It needs almost no change in the source code and you can run the tests with no issues. * Writing tests is simple. Cons: * The tests will use a real connection to the real server, it will generate some load on the server, which may be acceptable or not. If the real server is down or you have some connectivity issue, you can't run tests. * If the server has some state, for example, a backend database with user data, authentication, etc, you have to solve the *shared resource* problem if you want to allow multiple test runnings on different hosts. For example, if there are more than one developers and/or testers. * Ensuring that there's no crosstalk is very important: if there's some change made by one instance, it should be invisible to the other. It should either revert the changes or do it in a separate namespace which will be cleaned up by some other means such as periodic jobs. Also, the test should not have inconsistent state behind. Mock your http calls, so they won't reach any real server ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pros: * It needs almost no change in the source code and you can run the tests with no issues. * There are excellent libraries supporting mocking such as **responses** and **pytest-vcr**. * No need to ensure crosstalk or manage shared resources. * Tests work offline. Cons: * No actual http requests are sent. It needs great effort to mock the existing behavior of the original library (such as **requests**) and you need to keep the two libraries in sync. * Mocking must support the http client library of your choice. Eg. if you use **requests** you need to use **responses**. If you are using different libraries, the complexity raises. * At some point, it is not like black-box testing as you need to know the implementation details of the original code. * It is required to set up the expected requests and their responses. If the server doesn't work like your setup, the code will break when it is run with the real server. Run a fake http server listening on localhost ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Pros: * Writing tests is simple. * No need to ensure crosstalk or manage shared resources. * Tests work offline. * Actual http requests are sent. There's a real http server running speaking http protocol so you can test all the special cases you need. You can customize every http request expectations and their responses to the end. * Testing connectivity issues is possible. * There's no mocking, no code injection or class replacement. * It is black-box testing as there's no need to know anything about the original code. Cons: * Some code changes required in the original source code. The code should accept the server endpoint (host and port) as a parameter or by some means of configuration. This endpoint will be set to localhost during the test running. If it is not possible, you need to tweak name resolution. * It is required to set up the expected requests and their responses. If the server doesn't work like your setup, the code will break when it is run with the real server. * Setting up TLS/SSL requires additional knowledge (cert generation, for example) Specifying the expectations and constraints ------------------------------------------- In the above code, the most simple case was shown. The library provides many ways to customize the expectations. In the example above, the code expected a request to */foobar* with any method (such as *GET*, *PUT*, *POST*, *DELETE*). If you want to limit the method to the *GET* method only, you can specify: .. code:: python httpserver.expect_request("/foobar", method="GET") Similarly, specifying the query parameters is possible: .. code:: python httpserver.expect_request("/foobar", query_string="user=user1", method="GET") This will match the GET request made to the http://localhost/foobar?user=user1 URL. If more constraint is specified to the ``expect_request()`` method, the expectation will be narrower, eg. it is similar when using logical AND. If you want, you can specify the query string as a dictionary so the order of the key-value pairs does not matter: .. code:: python httpserver.expect_request( "/foobar", query_string={"user": "user1", "group": "group1"}, method="GET" ) Similar to query parameters, it is possible to specify constraints for http headers also. For many parameters, you can specify either string or some expression (such as the dict in the example above). For example, specifying a regexp pattern for the URI Is also possible by specifying a compiled regexp object: .. code:: python httpserver.expect_request( re.compile("^/foo"), query_string={"user": "user1", "group": "group1"}, method="GET" ) The above will match every URI starting with "/foo". All of these are documented in the :ref:`api-documentation`. Specifying responses -------------------- Once you have set up the expected request, it is required to set up the response which will be returned to the client. In the example we used ``respond_with_json()`` but it is also possible to respond with an arbitrary content. .. code:: python respond_with_data("Hello world!", content_type="text/plain") In the example above, we are responding a text/plain content. You can specify the status also: .. code:: python respond_with_data("Not found", status=404, content_type="text/plain") With this method, it is possible to set the response headers, mime type. In some cases you need to create your own Response instance (which is the Response object from the underlying werkzeug library), so you can respond with it. This allows more customization, however, in most cases the respond_with_data is sufficient: .. code:: python respond_with_response(Response("Hello world!")) # same as respond_with_data("Hello world!") If you need to produce dynamic content, use the ``respond_with_handler`` method, which accepts a callable (eg. a python function): .. code:: python def my_handler(request): # here, examine the request object return Response("Hello world!") respond_with_handler(my_handler) Ordered and oneshot expectations -------------------------------- In the above examples, we used ``expect_request()`` method, which registered the request to be handled. During the test running you can issue requests to this endpoint as many times as you want, and you will get the same response (unless you used the ``respond_with_handler()`` method, detailed above). There are two other additional limitations which can be used: * ordered handling, which specifies the order of the requests * oneshot handling, which specifies the lifetime of the handlers for only one request Ordered handling ~~~~~~~~~~~~~~~~ The ordered handling specifies the order of the requests. It must be the same as the order of the registration: .. code:: python def test_ordered(httpserver: HTTPServer): httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") requests.get(httpserver.url_for("/foobar")) requests.get(httpserver.url_for("/foobaz")) The above code passes the test running. The first request matches the first handler, and the second request matches the second one. When making the requests in a reverse order, it will fail: .. code:: python def test_ordered(httpserver: HTTPServer): httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") requests.get(httpserver.url_for("/foobaz")) requests.get(httpserver.url_for("/foobar")) # <- fail? If you run the above code you will notice that no test failed. This is because the http server is running in its own thread, separately from the client code. It has no way to raise an assertion error in the client thread. However, this test checks nothing but runs two subsequent queries and that's it. Checking the http status code would make it fail: .. code:: python def test_ordered(httpserver: HTTPServer): httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") assert requests.get(httpserver.url_for("/foobaz")).status_code == 200 assert requests.get(httpserver.url_for("/foobar")).status_code == 200 # <- fail! For further details about error handling, please read the :ref:`handling-test-errors` chapter. Oneshot handling ~~~~~~~~~~~~~~~~ Oneshot handling is useful when you want to ensure that the client makes only one request to the specified URI. Once the request is handled and the response is sent, the handler is no longer registered and a further call to the same URL will be erroneous. .. code:: python def test_oneshot(httpserver: HTTPServer): httpserver.expect_oneshot_request("/foobar").respond_with_data("OK") requests.get(httpserver.url_for("/foobar")) requests.get(httpserver.url_for("/foobar")) # this will get http status 500 If you run the above code you will notice that no test failed. This is because the http server is running in its own thread, separately from the client code. It has no way to raise an assertion error in the client thread. However, this test checks nothing but runs two subsequent queries and that's it. Checking the http status code would make it fail: .. code:: python def test_oneshot(httpserver: HTTPServer): httpserver.expect_oneshot_request("/foobar").respond_with_data("OK") assert requests.get(httpserver.url_for("/foobar")).status_code == 200 assert requests.get(httpserver.url_for("/foobar")).status_code == 200 # fail! For further details about error handling, please read the :ref:`handling-test-errors` chapter. .. _handling-test-errors: Handling test errors ~~~~~~~~~~~~~~~~~~~~ If you look at carefully at the test running, you realize that the second request (and all further requests) will get an http status 500 code, explaining the issue in the response body. When a properly written http client gets http status 500, it should raise an exception, which will be unhandled and in the end the test will be failed. In some cases, however, you want to make sure that everything is ok so far, and raise AssertionError when something is not good. Call the ``check_assertions()`` method of the httpserver object, and this will look at the server's internal state (which is running in the other thread) and if there's something not right (such as the order of the requests not matching, or there was a non-matching request), it will raise an AssertionError and your test will properly fail: .. code:: python def test_ordered_ok(httpserver: HTTPServer): httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") requests.get(httpserver.url_for("/foobaz")) requests.get(httpserver.url_for("/foobar")) # gets 500 httpserver.check_assertions() # this will raise AssertionError and make the test failing The server writes a log about the requests and responses which were processed. This can be accessed in the `log` attribute of the http server. This log is a python list with 2-element tuples (request, response). Server lifetime ~~~~~~~~~~~~~~~ Http server is started when the first test uses the `httpserver` fixture, and it will be running for the rest of the session. The server is not stopped and started between the tests as it is an expensive operation, it takes up to 1 second to properly stop the server. To avoid crosstalk (eg one test leaving its state behind), the server's state is cleaned up between test runnings. Debugging ~~~~~~~~~ If you having multiple requests for the server, adding the call to ``check_assertions()`` may to debug as it will make the test failed as soon as possible. .. code:: python import requests def test_json_client(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) requests.get(httpserver.url_for("/foo")) requests.get(httpserver.url_for("/bar")) requests.get(httpserver.url_for("/foobar")) httpserver.check_assertions() In the above code, the first request (to **/foo**) is not successful (it gets http status 500), but as the response status is not checked (or any of the response), and there's no call to ``check_assertions()``, the test continues the running. It gets through the **/bar** request, which is also not successful (and gets http status 500 also like the first one), then goes the last request which is successful (as there's a handler defined for it) In the end, when checking the check_assertions() raise the error for the first request, but it is a bit late: figuring out the request which caused the problem could be troublesome. Also, it will report the problem for the first request only. Adding more call of ``check_assertions()`` will help. .. code:: python import requests def test_json_client(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) requests.get(httpserver.url_for("/foo")) httpserver.check_assertions() requests.get(httpserver.url_for("/bar")) httpserver.check_assertions() requests.get(httpserver.url_for("/foobar")) httpserver.check_assertions() In the above code, the test will fail after the first request. In case you do not want to fail the test, you can use any of these options: * ``assertions`` attribute of the ``httpserver`` object is a list of the known errors. If it is non-empty, then there was an issue. * ``format_matchers()`` method of the ``httpserver`` object returns which handlers have been registered to the server. In some cases, registering non-matching handlers causes the problem so printing this string can help to diagnose the problem. Advanced topics --------------- This is the end of the tutorial, however, not everything is covered here and this library offers a lot more. Further readings: * :ref:`api-documentation` * :ref:`howto` pytest-httpserver-1.1.2/doc/upgrade.rst000066400000000000000000000042651475715145300202200ustar00rootroot00000000000000.. _upgrade: Upgrade guides ============== The following document describes how to upgrade to a given version of the library which introduces breaking changes. Introducing breaking changes ---------------------------- When a breaking change is about to be made in the library, an intermediate release is released which generates deprecation warnings when the functionality to be removed is used. This does not break any functionality but shows a warning instead. Together with this intermediate release, a new *pre-release* is released to *pypi*. This release removes the functionality described by the warning, but *pip* does not install this version unless you specify the *--pre* parameter to *pip install*. Once you made the required changes to make your code compatible with the new version, you can install the new version by *pip install --pre pytest-httpserver*. After a given time period, a new non-pre release is released, this will be installed by pip similar to other releases and it will break your code if you have not made the required changes. If this happens, you can still pin the version in requirements.txt or other places. Usually specifying the version with `==` operator fixes the version, but for more details please read the documentation of the tool you are using in manage dependencies. 1.0.0 ----- In pytest-httpserver 1.0.0 the following breaking changes were made. * The scope of ``httpserver_listen_address`` fixture changed from **function** to **session** In order to make your code compatible with the new version of pytest-httpserver, you need to specify the `session` scope explicitly. Example ~~~~~~~ Old code: .. code-block:: python import pytest @pytest.fixture def httpserver_listen_address(): return ("127.0.0.1", 8888) New code: .. code-block:: python import pytest @pytest.fixture(scope="session") def httpserver_listen_address(): return ("127.0.0.1", 8888) As this fixture is now defined with session scope, it will be called only once, when it is first referenced by a test or by another fixture. .. note:: There were other, non-breaking changes introduced to 1.0.0. For details, please read the :ref:`changes`. pytest-httpserver-1.1.2/example.py000077500000000000000000000006071475715145300172760ustar00rootroot00000000000000#!.venv/bin/python3 import urllib.error import urllib.request from pytest_httpserver import HTTPServer server = HTTPServer(port=4000) server.expect_request("/foobar").respond_with_json({"foo": "bar"}) server.start() try: print(urllib.request.urlopen("http://localhost:4000/foobar?name=John%20Smith&age=123").read()) except urllib.error.HTTPError as err: print(err) server.stop() pytest-httpserver-1.1.2/example_pytest.py000066400000000000000000000024631475715145300207050ustar00rootroot00000000000000# Run this code as 'pytest example_pytest.py' import pytest import requests from pytest_httpserver import HTTPServer # specify where the server should bind to # you can return 0 as the port, in this case it will bind to a free (ephemeral) TCP port @pytest.fixture(scope="session") def httpserver_listen_address(): return ("127.0.0.1", 8000) # specify httpserver fixture def test_oneshot_and_permanent_happy_path1(httpserver: HTTPServer): # define some request handlers # more details in the documentation httpserver.expect_request("/permanent").respond_with_data("OK permanent") httpserver.expect_oneshot_request("/oneshot1").respond_with_data("OK oneshot1") httpserver.expect_oneshot_request("/oneshot2").respond_with_data("OK oneshot2") # query those handlers with a real HTTP client (requests in this example but could by anything) # the 'url_for' method formats the final URL, so there's no need to wire-in any ports assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert requests.get(httpserver.url_for("/oneshot1")).text == "OK oneshot1" assert requests.get(httpserver.url_for("/oneshot2")).text == "OK oneshot2" assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert len(httpserver.oneshot_handlers) == 0 pytest-httpserver-1.1.2/poetry.lock000066400000000000000000002742461475715145300174760ustar00rootroot00000000000000# This file is automatically @generated by Poetry 1.8.4 and should not be changed by hand. [[package]] name = "alabaster" version = "0.7.16" description = "A light, configurable Sphinx theme" optional = false python-versions = ">=3.9" files = [ {file = "alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92"}, {file = "alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65"}, ] [[package]] name = "babel" version = "2.17.0" description = "Internationalization utilities" optional = false python-versions = ">=3.8" files = [ {file = "babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2"}, {file = "babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d"}, ] [package.extras] dev = ["backports.zoneinfo", "freezegun (>=1.0,<2.0)", "jinja2 (>=3.0)", "pytest (>=6.0)", "pytest-cov", "pytz", "setuptools", "tzdata"] [[package]] name = "black" version = "25.1.0" description = "The uncompromising code formatter." optional = false python-versions = ">=3.9" files = [ {file = "black-25.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:759e7ec1e050a15f89b770cefbf91ebee8917aac5c20483bc2d80a6c3a04df32"}, {file = "black-25.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e519ecf93120f34243e6b0054db49c00a35f84f195d5bce7e9f5cfc578fc2da"}, {file = "black-25.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:055e59b198df7ac0b7efca5ad7ff2516bca343276c466be72eb04a3bcc1f82d7"}, {file = "black-25.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:db8ea9917d6f8fc62abd90d944920d95e73c83a5ee3383493e35d271aca872e9"}, {file = "black-25.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a39337598244de4bae26475f77dda852ea00a93bd4c728e09eacd827ec929df0"}, {file = "black-25.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96c1c7cd856bba8e20094e36e0f948718dc688dba4a9d78c3adde52b9e6c2299"}, {file = "black-25.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bce2e264d59c91e52d8000d507eb20a9aca4a778731a08cfff7e5ac4a4bb7096"}, {file = "black-25.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:172b1dbff09f86ce6f4eb8edf9dede08b1fce58ba194c87d7a4f1a5aa2f5b3c2"}, {file = "black-25.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4b60580e829091e6f9238c848ea6750efed72140b91b048770b64e74fe04908b"}, {file = "black-25.1.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e2978f6df243b155ef5fa7e558a43037c3079093ed5d10fd84c43900f2d8ecc"}, {file = "black-25.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3b48735872ec535027d979e8dcb20bf4f70b5ac75a8ea99f127c106a7d7aba9f"}, {file = "black-25.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:ea0213189960bda9cf99be5b8c8ce66bb054af5e9e861249cd23471bd7b0b3ba"}, {file = "black-25.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8f0b18a02996a836cc9c9c78e5babec10930862827b1b724ddfe98ccf2f2fe4f"}, {file = "black-25.1.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:afebb7098bfbc70037a053b91ae8437c3857482d3a690fefc03e9ff7aa9a5fd3"}, {file = "black-25.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:030b9759066a4ee5e5aca28c3c77f9c64789cdd4de8ac1df642c40b708be6171"}, {file = "black-25.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:a22f402b410566e2d1c950708c77ebf5ebd5d0d88a6a2e87c86d9fb48afa0d18"}, {file = "black-25.1.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1ee0a0c330f7b5130ce0caed9936a904793576ef4d2b98c40835d6a65afa6a0"}, {file = "black-25.1.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f3df5f1bf91d36002b0a75389ca8663510cf0531cca8aa5c1ef695b46d98655f"}, {file = "black-25.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9e6827d563a2c820772b32ce8a42828dc6790f095f441beef18f96aa6f8294e"}, {file = "black-25.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:bacabb307dca5ebaf9c118d2d2f6903da0d62c9faa82bd21a33eecc319559355"}, {file = "black-25.1.0-py3-none-any.whl", hash = "sha256:95e8176dae143ba9097f351d174fdaf0ccd29efb414b362ae3fd72bf0f710717"}, {file = "black-25.1.0.tar.gz", hash = "sha256:33496d5cd1222ad73391352b4ae8da15253c5de89b93a80b3e2c8d9a19ec2666"}, ] [package.dependencies] click = ">=8.0.0" mypy-extensions = ">=0.4.3" packaging = ">=22.0" pathspec = ">=0.9.0" platformdirs = ">=2" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} [package.extras] colorama = ["colorama (>=0.4.3)"] d = ["aiohttp (>=3.10)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] [[package]] name = "certifi" version = "2025.1.31" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.6" files = [ {file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"}, {file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"}, ] [[package]] name = "cfgv" version = "3.4.0" description = "Validate configuration and produce human readable error messages." optional = false python-versions = ">=3.8" files = [ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, ] [[package]] name = "charset-normalizer" version = "3.4.1" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." optional = false python-versions = ">=3.7" files = [ {file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a"}, {file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d"}, {file = "charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807"}, {file = "charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f"}, {file = "charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f"}, {file = "charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:234ac59ea147c59ee4da87a0c0f098e9c8d169f4dc2a159ef720f1a61bbe27cd"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd4ec41f914fa74ad1b8304bbc634b3de73d2a0889bd32076342a573e0779e00"}, {file = "charset_normalizer-3.4.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eea6ee1db730b3483adf394ea72f808b6e18cf3cb6454b4d86e04fa8c4327a12"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c96836c97b1238e9c9e3fe90844c947d5afbf4f4c92762679acfe19927d81d77"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4d86f7aff21ee58f26dcf5ae81a9addbd914115cdebcbb2217e4f0ed8982e146"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:09b5e6733cbd160dcc09589227187e242a30a49ca5cefa5a7edd3f9d19ed53fd"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:5777ee0881f9499ed0f71cc82cf873d9a0ca8af166dfa0af8ec4e675b7df48e6"}, {file = "charset_normalizer-3.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:237bdbe6159cff53b4f24f397d43c6336c6b0b42affbe857970cefbb620911c8"}, {file = "charset_normalizer-3.4.1-cp311-cp311-win32.whl", hash = "sha256:8417cb1f36cc0bc7eaba8ccb0e04d55f0ee52df06df3ad55259b9a323555fc8b"}, {file = "charset_normalizer-3.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:d7f50a1f8c450f3925cb367d011448c39239bb3eb4117c36a6d354794de4ce76"}, {file = "charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d"}, {file = "charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9"}, {file = "charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1"}, {file = "charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35"}, {file = "charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f"}, {file = "charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11"}, {file = "charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601"}, {file = "charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd"}, {file = "charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407"}, {file = "charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f30bf9fd9be89ecb2360c7d94a711f00c09b976258846efe40db3d05828e8089"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:97f68b8d6831127e4787ad15e6757232e14e12060bec17091b85eb1486b91d8d"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7974a0b5ecd505609e3b19742b60cee7aa2aa2fb3151bc917e6e2646d7667dcf"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc54db6c8593ef7d4b2a331b58653356cf04f67c960f584edb7c3d8c97e8f39e"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:311f30128d7d333eebd7896965bfcfbd0065f1716ec92bd5638d7748eb6f936a"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:7d053096f67cd1241601111b698f5cad775f97ab25d81567d3f59219b5f1adbd"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:807f52c1f798eef6cf26beb819eeb8819b1622ddfeef9d0977a8502d4db6d534"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:dccbe65bd2f7f7ec22c4ff99ed56faa1e9f785482b9bbd7c717e26fd723a1d1e"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:2fb9bd477fdea8684f78791a6de97a953c51831ee2981f8e4f583ff3b9d9687e"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:01732659ba9b5b873fc117534143e4feefecf3b2078b0a6a2e925271bb6f4cfa"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-win32.whl", hash = "sha256:7a4f97a081603d2050bfaffdefa5b02a9ec823f8348a572e39032caa8404a487"}, {file = "charset_normalizer-3.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:7b1bef6280950ee6c177b326508f86cad7ad4dff12454483b51d8b7d673a2c5d"}, {file = "charset_normalizer-3.4.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ecddf25bee22fe4fe3737a399d0d177d72bc22be6913acfab364b40bce1ba83c"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c60ca7339acd497a55b0ea5d506b2a2612afb2826560416f6894e8b5770d4a9"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b7b2d86dd06bfc2ade3312a83a5c364c7ec2e3498f8734282c6c3d4b07b346b8"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dd78cfcda14a1ef52584dbb008f7ac81c1328c0f58184bf9a84c49c605002da6"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e27f48bcd0957c6d4cb9d6fa6b61d192d0b13d5ef563e5f2ae35feafc0d179c"}, {file = "charset_normalizer-3.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:01ad647cdd609225c5350561d084b42ddf732f4eeefe6e678765636791e78b9a"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:619a609aa74ae43d90ed2e89bdd784765de0a25ca761b93e196d938b8fd1dbbd"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:89149166622f4db9b4b6a449256291dc87a99ee53151c74cbd82a53c8c2f6ccd"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:7709f51f5f7c853f0fb938bcd3bc59cdfdc5203635ffd18bf354f6967ea0f824"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:345b0426edd4e18138d6528aed636de7a9ed169b4aaf9d61a8c19e39d26838ca"}, {file = "charset_normalizer-3.4.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0907f11d019260cdc3f94fbdb23ff9125f6b5d1039b76003b5b0ac9d6a6c9d5b"}, {file = "charset_normalizer-3.4.1-cp38-cp38-win32.whl", hash = "sha256:ea0d8d539afa5eb2728aa1932a988a9a7af94f18582ffae4bc10b3fbdad0626e"}, {file = "charset_normalizer-3.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:329ce159e82018d646c7ac45b01a430369d526569ec08516081727a20e9e4af4"}, {file = "charset_normalizer-3.4.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:b97e690a2118911e39b4042088092771b4ae3fc3aa86518f84b8cf6888dbdb41"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:78baa6d91634dfb69ec52a463534bc0df05dbd546209b79a3880a34487f4b84f"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1a2bc9f351a75ef49d664206d51f8e5ede9da246602dc2d2726837620ea034b2"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75832c08354f595c760a804588b9357d34ec00ba1c940c15e31e96d902093770"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0af291f4fe114be0280cdd29d533696a77b5b49cfde5467176ecab32353395c4"}, {file = "charset_normalizer-3.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0167ddc8ab6508fe81860a57dd472b2ef4060e8d378f0cc555707126830f2537"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:2a75d49014d118e4198bcee5ee0a6f25856b29b12dbf7cd012791f8a6cc5c496"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:363e2f92b0f0174b2f8238240a1a30142e3db7b957a5dd5689b0e75fb717cc78"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:ab36c8eb7e454e34e60eb55ca5d241a5d18b2c6244f6827a30e451c42410b5f7"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:4c0907b1928a36d5a998d72d64d8eaa7244989f7aaaf947500d3a800c83a3fd6"}, {file = "charset_normalizer-3.4.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:04432ad9479fa40ec0f387795ddad4437a2b50417c69fa275e212933519ff294"}, {file = "charset_normalizer-3.4.1-cp39-cp39-win32.whl", hash = "sha256:3bed14e9c89dcb10e8f3a29f9ccac4955aebe93c71ae803af79265c9ca5644c5"}, {file = "charset_normalizer-3.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:49402233c892a461407c512a19435d1ce275543138294f7ef013f0b63d5d3765"}, {file = "charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85"}, {file = "charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3"}, ] [[package]] name = "click" version = "8.1.8" description = "Composable command line interface toolkit" optional = false python-versions = ">=3.7" files = [ {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, ] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} [[package]] name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 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.6.12" description = "Code coverage measurement for Python" optional = false python-versions = ">=3.9" files = [ {file = "coverage-7.6.12-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:704c8c8c6ce6569286ae9622e534b4f5b9759b6f2cd643f1c1a61f666d534fe8"}, {file = "coverage-7.6.12-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ad7525bf0241e5502168ae9c643a2f6c219fa0a283001cee4cf23a9b7da75879"}, {file = "coverage-7.6.12-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:06097c7abfa611c91edb9e6920264e5be1d6ceb374efb4986f38b09eed4cb2fe"}, {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:220fa6c0ad7d9caef57f2c8771918324563ef0d8272c94974717c3909664e674"}, {file = "coverage-7.6.12-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3688b99604a24492bcfe1c106278c45586eb819bf66a654d8a9a1433022fb2eb"}, {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d1a987778b9c71da2fc8948e6f2656da6ef68f59298b7e9786849634c35d2c3c"}, {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:cec6b9ce3bd2b7853d4a4563801292bfee40b030c05a3d29555fd2a8ee9bd68c"}, {file = "coverage-7.6.12-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ace9048de91293e467b44bce0f0381345078389814ff6e18dbac8fdbf896360e"}, {file = "coverage-7.6.12-cp310-cp310-win32.whl", hash = "sha256:ea31689f05043d520113e0552f039603c4dd71fa4c287b64cb3606140c66f425"}, {file = "coverage-7.6.12-cp310-cp310-win_amd64.whl", hash = "sha256:676f92141e3c5492d2a1596d52287d0d963df21bf5e55c8b03075a60e1ddf8aa"}, {file = "coverage-7.6.12-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e18aafdfb3e9ec0d261c942d35bd7c28d031c5855dadb491d2723ba54f4c3015"}, {file = "coverage-7.6.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66fe626fd7aa5982cdebad23e49e78ef7dbb3e3c2a5960a2b53632f1f703ea45"}, {file = "coverage-7.6.12-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ef01d70198431719af0b1f5dcbefc557d44a190e749004042927b2a3fed0702"}, {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e92ae5a289a4bc4c0aae710c0948d3c7892e20fd3588224ebe242039573bf0"}, {file = "coverage-7.6.12-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e695df2c58ce526eeab11a2e915448d3eb76f75dffe338ea613c1201b33bab2f"}, {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d74c08e9aaef995f8c4ef6d202dbd219c318450fe2a76da624f2ebb9c8ec5d9f"}, {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e995b3b76ccedc27fe4f477b349b7d64597e53a43fc2961db9d3fbace085d69d"}, {file = "coverage-7.6.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b1f097878d74fe51e1ddd1be62d8e3682748875b461232cf4b52ddc6e6db0bba"}, {file = "coverage-7.6.12-cp311-cp311-win32.whl", hash = "sha256:1f7ffa05da41754e20512202c866d0ebfc440bba3b0ed15133070e20bf5aeb5f"}, {file = "coverage-7.6.12-cp311-cp311-win_amd64.whl", hash = "sha256:e216c5c45f89ef8971373fd1c5d8d1164b81f7f5f06bbf23c37e7908d19e8558"}, {file = "coverage-7.6.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b172f8e030e8ef247b3104902cc671e20df80163b60a203653150d2fc204d1ad"}, {file = "coverage-7.6.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:641dfe0ab73deb7069fb972d4d9725bf11c239c309ce694dd50b1473c0f641c3"}, {file = "coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574"}, {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985"}, {file = "coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750"}, {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea"}, {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3"}, {file = "coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a"}, {file = "coverage-7.6.12-cp312-cp312-win32.whl", hash = "sha256:00b2086892cf06c7c2d74983c9595dc511acca00665480b3ddff749ec4fb2a95"}, {file = "coverage-7.6.12-cp312-cp312-win_amd64.whl", hash = "sha256:7ae6eabf519bc7871ce117fb18bf14e0e343eeb96c377667e3e5dd12095e0288"}, {file = "coverage-7.6.12-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:488c27b3db0ebee97a830e6b5a3ea930c4a6e2c07f27a5e67e1b3532e76b9ef1"}, {file = "coverage-7.6.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:5d1095bbee1851269f79fd8e0c9b5544e4c00c0c24965e66d8cba2eb5bb535fd"}, {file = "coverage-7.6.12-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0533adc29adf6a69c1baa88c3d7dbcaadcffa21afbed3ca7a225a440e4744bf9"}, {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53c56358d470fa507a2b6e67a68fd002364d23c83741dbc4c2e0680d80ca227e"}, {file = "coverage-7.6.12-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64cbb1a3027c79ca6310bf101014614f6e6e18c226474606cf725238cf5bc2d4"}, {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:79cac3390bfa9836bb795be377395f28410811c9066bc4eefd8015258a7578c6"}, {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:9b148068e881faa26d878ff63e79650e208e95cf1c22bd3f77c3ca7b1d9821a3"}, {file = "coverage-7.6.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8bec2ac5da793c2685ce5319ca9bcf4eee683b8a1679051f8e6ec04c4f2fd7dc"}, {file = "coverage-7.6.12-cp313-cp313-win32.whl", hash = "sha256:200e10beb6ddd7c3ded322a4186313d5ca9e63e33d8fab4faa67ef46d3460af3"}, {file = "coverage-7.6.12-cp313-cp313-win_amd64.whl", hash = "sha256:2b996819ced9f7dbb812c701485d58f261bef08f9b85304d41219b1496b591ef"}, {file = "coverage-7.6.12-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:299cf973a7abff87a30609879c10df0b3bfc33d021e1adabc29138a48888841e"}, {file = "coverage-7.6.12-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4b467a8c56974bf06e543e69ad803c6865249d7a5ccf6980457ed2bc50312703"}, {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2458f275944db8129f95d91aee32c828a408481ecde3b30af31d552c2ce284a0"}, {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a9d8be07fb0832636a0f72b80d2a652fe665e80e720301fb22b191c3434d924"}, {file = "coverage-7.6.12-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:14d47376a4f445e9743f6c83291e60adb1b127607a3618e3185bbc8091f0467b"}, {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b95574d06aa9d2bd6e5cc35a5bbe35696342c96760b69dc4287dbd5abd4ad51d"}, {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:ecea0c38c9079570163d663c0433a9af4094a60aafdca491c6a3d248c7432827"}, {file = "coverage-7.6.12-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2251fabcfee0a55a8578a9d29cecfee5f2de02f11530e7d5c5a05859aa85aee9"}, {file = "coverage-7.6.12-cp313-cp313t-win32.whl", hash = "sha256:eb5507795caabd9b2ae3f1adc95f67b1104971c22c624bb354232d65c4fc90b3"}, {file = "coverage-7.6.12-cp313-cp313t-win_amd64.whl", hash = "sha256:f60a297c3987c6c02ffb29effc70eadcbb412fe76947d394a1091a3615948e2f"}, {file = "coverage-7.6.12-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e7575ab65ca8399c8c4f9a7d61bbd2d204c8b8e447aab9d355682205c9dd948d"}, {file = "coverage-7.6.12-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:8161d9fbc7e9fe2326de89cd0abb9f3599bccc1287db0aba285cb68d204ce929"}, {file = "coverage-7.6.12-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3a1e465f398c713f1b212400b4e79a09829cd42aebd360362cd89c5bdc44eb87"}, {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f25d8b92a4e31ff1bd873654ec367ae811b3a943583e05432ea29264782dc32c"}, {file = "coverage-7.6.12-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a936309a65cc5ca80fa9f20a442ff9e2d06927ec9a4f54bcba9c14c066323f2"}, {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:aa6f302a3a0b5f240ee201297fff0bbfe2fa0d415a94aeb257d8b461032389bd"}, {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:f973643ef532d4f9be71dd88cf7588936685fdb576d93a79fe9f65bc337d9d73"}, {file = "coverage-7.6.12-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:78f5243bb6b1060aed6213d5107744c19f9571ec76d54c99cc15938eb69e0e86"}, {file = "coverage-7.6.12-cp39-cp39-win32.whl", hash = "sha256:69e62c5034291c845fc4df7f8155e8544178b6c774f97a99e2734b05eb5bed31"}, {file = "coverage-7.6.12-cp39-cp39-win_amd64.whl", hash = "sha256:b01a840ecc25dce235ae4c1b6a0daefb2a203dba0e6e980637ee9c2f6ee0df57"}, {file = "coverage-7.6.12-pp39.pp310-none-any.whl", hash = "sha256:7e39e845c4d764208e7b8f6a21c541ade741e2c41afabdfa1caa28687a3c98cf"}, {file = "coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953"}, {file = "coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] toml = ["tomli"] [[package]] name = "distlib" version = "0.3.9" description = "Distribution utilities" optional = false python-versions = "*" files = [ {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, ] [[package]] name = "docutils" version = "0.21.2" description = "Docutils -- Python Documentation Utilities" optional = false python-versions = ">=3.9" files = [ {file = "docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2"}, {file = "docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f"}, ] [[package]] name = "dulwich" version = "0.22.7" description = "Python Git Library" optional = false python-versions = ">=3.9" files = [ {file = "dulwich-0.22.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:01e484d44014fef78cdef3b3adc34564808b4677497a57a0950c90a1d6349be3"}, {file = "dulwich-0.22.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb258c62d7fb4cfe03b3fba09f702ebb84a924f2f004833435e32c93fe8a7f13"}, {file = "dulwich-0.22.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdbd087e9e99bc809b15864ebc79dbefe869e3038b64c953d7736f6e6b382dc7"}, {file = "dulwich-0.22.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c830d63c691b5f979964a2d6b325930b7a53f14836598352690601cd205f04b"}, {file = "dulwich-0.22.7-cp310-cp310-win32.whl", hash = "sha256:925cec97aeefda3f950e45e8d4c247e4ce6f83b6ee96e383c82f9bced626151f"}, {file = "dulwich-0.22.7-cp310-cp310-win_amd64.whl", hash = "sha256:f73668ecc29e0a20d20970489fffe2ba466e5486eae2f20104bc38bcbe611f64"}, {file = "dulwich-0.22.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:df5a179e5d95ac0263b5e0ccd53311eac486091979dcac106c5cc9e0ee4f2aa2"}, {file = "dulwich-0.22.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca7ed207956001e6a8a2e3f319cdc37591e53f7eb04aedafa78f96768048c53e"}, {file = "dulwich-0.22.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:052715452b729544c611a107b2eef6111e527f041c1b666f8ed36c04e39c36b5"}, {file = "dulwich-0.22.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:74b7cf6f0d46ac777be617dad7c1b992380004de74c0e0652bed174686249f34"}, {file = "dulwich-0.22.7-cp311-cp311-win32.whl", hash = "sha256:5b9806a75f4b74fa891926b1d830e21f9cead80ed6dd803ed668369b26fb8b5f"}, {file = "dulwich-0.22.7-cp311-cp311-win_amd64.whl", hash = "sha256:01544915c4056d0820de8cf126b971f7c180743ff64c4435c89168e44b30df4b"}, {file = "dulwich-0.22.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2b7a3ac4baa49bd988cc0d0891a93aa26307c01f35caeed8729b7928a1f483af"}, {file = "dulwich-0.22.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d72ce1377eac23bd77aa3541ceb91f2d8bd68687659f8625af8301f0b6b0a63"}, {file = "dulwich-0.22.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6bda2eca0847c30a9312a72f219af9e63feb7d2ca89f47fdaa240b0d0cdd6b84"}, {file = "dulwich-0.22.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a8886b2c9750ba15193356d9e8608e031cd89a780d0afc53b3101391605b3793"}, {file = "dulwich-0.22.7-cp312-cp312-win32.whl", hash = "sha256:1782854c10878b5cb8423e74b0ef4256c3667f7b0266513af028ac28dbab1f2d"}, {file = "dulwich-0.22.7-cp312-cp312-win_amd64.whl", hash = "sha256:fe324dc40b93e8be996c9fa9291a439bef835a92a2e4cb5c8cbdb1171c168fd6"}, {file = "dulwich-0.22.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2220c8b7cac5794e2260a924e81b05baa7836c18ba805d5a6731071a5ff6b860"}, {file = "dulwich-0.22.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cbd5ecbc95e18c745965fc7b2b71209443987a99e499c7bb074234d7c6142e2"}, {file = "dulwich-0.22.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f418779837a3249b7dfc4b3dc7266fa40687e5f0249eedfa7185560ba1ee148"}, {file = "dulwich-0.22.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9c01db2ef6d5f5b9192c0011624701b0de328868fe0c32601368cd337e77cd1a"}, {file = "dulwich-0.22.7-cp313-cp313-win32.whl", hash = "sha256:a64e61fa6ab60db0f897f1c30f32b26b330d3a9dc264f089ee9c44f5900fb657"}, {file = "dulwich-0.22.7-cp313-cp313-win_amd64.whl", hash = "sha256:9f5954cd491313743d7bd3623d323b72afceb83d2c2a47921f621bdd9d4c615b"}, {file = "dulwich-0.22.7-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6bea11b98e854ff2abec390eeac752586b83921a22091dae65470ccbb003fc1b"}, {file = "dulwich-0.22.7-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdbcf206d4b1e5ba2affc6189948cb292cc647593876b96a0b71db44e79a05a1"}, {file = "dulwich-0.22.7-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0bb9afa799c0301b2760e9af99083a2b08f655c55037945b6a5e227566adc1"}, {file = "dulwich-0.22.7-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62027dfccee97268eadf0c54df3d72ce30e4402cf5cf06c021e474b9a9eb3536"}, {file = "dulwich-0.22.7-cp39-cp39-win32.whl", hash = "sha256:637a9ac27512b8c04e6a29bf92e3f73386cd85dfe8609f523ffbc96e659bde4b"}, {file = "dulwich-0.22.7-cp39-cp39-win_amd64.whl", hash = "sha256:986943e27a5c94c0be42fdcc688be1ae1a1349a3dbaa773fa7f9bdada1232b68"}, {file = "dulwich-0.22.7-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b25848041c51d09affafd2708236205cc4483bed8f7f43ecbe63b6a66b447604"}, {file = "dulwich-0.22.7-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b20bd6a25658e968e813eb69164332d3a2ab6029b51d3c6af8b64f2471847a"}, {file = "dulwich-0.22.7-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c68ab3540809bedcdd9b99e51c12adf11c2ab26554f74d899d8cf55bfa2639a6"}, {file = "dulwich-0.22.7-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:257abd49a768a52cf7f508daf2d30fe73f54fd32b7a674abd43817f66b0ca17b"}, {file = "dulwich-0.22.7-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:8dd5df3919c648887e550e836f87b4b83f1429876adce5ead5b5977e333c874d"}, {file = "dulwich-0.22.7-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5ada6a2fd400a4f51adfedd0267bfb08c61e2d9846c18ea653b0eb88a7b851d0"}, {file = "dulwich-0.22.7-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:007d8160b511bb149d31c08548307982f6ce752a46e7088b020517de00c3bd46"}, {file = "dulwich-0.22.7-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:40260034a6ecc3141a0d42360e888a73e58b9c0c9363c454cae182957fe602ac"}, {file = "dulwich-0.22.7-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:753eec461434f0ccbe0956ec825250e12230e8f1b365c8be1604386d94c2d8d0"}, {file = "dulwich-0.22.7-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7649f0c9b4760d72768805155e66579761f282fdca123e351019c85efce811eb"}, {file = "dulwich-0.22.7-py3-none-any.whl", hash = "sha256:10c5ee20430714ea6a79dde22c1f77078848930d27021aa810204738bc175e95"}, {file = "dulwich-0.22.7.tar.gz", hash = "sha256:d53935832dd182d4c1415042187093efcee988af5cd397fb1f394f5bb27f0707"}, ] [package.dependencies] urllib3 = ">=1.25" [package.extras] dev = ["mypy (==1.13.0)", "ruff (==0.8.3)"] fastimport = ["fastimport"] https = ["urllib3 (>=1.24.1)"] paramiko = ["paramiko"] pgp = ["gpg"] [[package]] name = "exceptiongroup" version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, ] [package.extras] test = ["pytest (>=6)"] [[package]] name = "filelock" version = "3.17.0" description = "A platform independent file lock." optional = false python-versions = ">=3.9" files = [ {file = "filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338"}, {file = "filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e"}, ] [package.extras] docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "identify" version = "2.6.8" description = "File identification library for Python" optional = false python-versions = ">=3.9" files = [ {file = "identify-2.6.8-py2.py3-none-any.whl", hash = "sha256:83657f0f766a3c8d0eaea16d4ef42494b39b34629a4b3192a9d020d349b3e255"}, {file = "identify-2.6.8.tar.gz", hash = "sha256:61491417ea2c0c5c670484fd8abbb34de34cdae1e5f39a73ee65e48e4bb663fc"}, ] [package.extras] license = ["ukkonen"] [[package]] name = "idna" version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" optional = false python-versions = ">=3.6" files = [ {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, ] [package.extras] all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "imagesize" version = "1.4.1" description = "Getting image size from png/jpeg/jpeg2000/gif file" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ {file = "imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b"}, {file = "imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a"}, ] [[package]] name = "importlib-metadata" version = "8.6.1" description = "Read metadata from Python packages" optional = false python-versions = ">=3.9" files = [ {file = "importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e"}, {file = "importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580"}, ] [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" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] [[package]] name = "jinja2" version = "3.1.5" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" files = [ {file = "jinja2-3.1.5-py3-none-any.whl", hash = "sha256:aba0f4dc9ed8013c424088f68a5c226f7d6097ed89b246d7749c2ec4175c6adb"}, {file = "jinja2-3.1.5.tar.gz", hash = "sha256:8fefff8dc3034e27bb80d67c671eb8a9bc424c0ef4c0826edbff304cceff43bb"}, ] [package.dependencies] MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, ] [[package]] name = "mypy" version = "1.15.0" description = "Optional static typing for Python" optional = false python-versions = ">=3.9" files = [ {file = "mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13"}, {file = "mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559"}, {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b"}, {file = "mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3"}, {file = "mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b"}, {file = "mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828"}, {file = "mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f"}, {file = "mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5"}, {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e"}, {file = "mypy-1.15.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a95fb17c13e29d2d5195869262f8125dfdb5c134dc8d9a9d0aecf7525b10c2c"}, {file = "mypy-1.15.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1905f494bfd7d85a23a88c5d97840888a7bd516545fc5aaedff0267e0bb54e2f"}, {file = "mypy-1.15.0-cp311-cp311-win_amd64.whl", hash = "sha256:c9817fa23833ff189db061e6d2eff49b2f3b6ed9856b4a0a73046e41932d744f"}, {file = "mypy-1.15.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aea39e0583d05124836ea645f412e88a5c7d0fd77a6d694b60d9b6b2d9f184fd"}, {file = "mypy-1.15.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f2147ab812b75e5b5499b01ade1f4a81489a147c01585cda36019102538615f"}, {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce436f4c6d218a070048ed6a44c0bbb10cd2cc5e272b29e7845f6a2f57ee4464"}, {file = "mypy-1.15.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8023ff13985661b50a5928fc7a5ca15f3d1affb41e5f0a9952cb68ef090b31ee"}, {file = "mypy-1.15.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1124a18bc11a6a62887e3e137f37f53fbae476dc36c185d549d4f837a2a6a14e"}, {file = "mypy-1.15.0-cp312-cp312-win_amd64.whl", hash = "sha256:171a9ca9a40cd1843abeca0e405bc1940cd9b305eaeea2dda769ba096932bb22"}, {file = "mypy-1.15.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:93faf3fdb04768d44bf28693293f3904bbb555d076b781ad2530214ee53e3445"}, {file = "mypy-1.15.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:811aeccadfb730024c5d3e326b2fbe9249bb7413553f15499a4050f7c30e801d"}, {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:98b7b9b9aedb65fe628c62a6dc57f6d5088ef2dfca37903a7d9ee374d03acca5"}, {file = "mypy-1.15.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c43a7682e24b4f576d93072216bf56eeff70d9140241f9edec0c104d0c515036"}, {file = "mypy-1.15.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:baefc32840a9f00babd83251560e0ae1573e2f9d1b067719479bfb0e987c6357"}, {file = "mypy-1.15.0-cp313-cp313-win_amd64.whl", hash = "sha256:b9378e2c00146c44793c98b8d5a61039a048e31f429fb0eb546d93f4b000bedf"}, {file = "mypy-1.15.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:e601a7fa172c2131bff456bb3ee08a88360760d0d2f8cbd7a75a65497e2df078"}, {file = "mypy-1.15.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:712e962a6357634fef20412699a3655c610110e01cdaa6180acec7fc9f8513ba"}, {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95579473af29ab73a10bada2f9722856792a36ec5af5399b653aa28360290a5"}, {file = "mypy-1.15.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f8722560a14cde92fdb1e31597760dc35f9f5524cce17836c0d22841830fd5b"}, {file = "mypy-1.15.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:1fbb8da62dc352133d7d7ca90ed2fb0e9d42bb1a32724c287d3c76c58cbaa9c2"}, {file = "mypy-1.15.0-cp39-cp39-win_amd64.whl", hash = "sha256:d10d994b41fb3497719bbf866f227b3489048ea4bbbb5015357db306249f7980"}, {file = "mypy-1.15.0-py3-none-any.whl", hash = "sha256:5469affef548bd1895d86d3bf10ce2b44e33d86923c29e4d675b3e323437ea3e"}, {file = "mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43"}, ] [package.dependencies] mypy_extensions = ">=1.0.0" tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} typing_extensions = ">=4.6.0" [package.extras] dmypy = ["psutil (>=4.0)"] faster-cache = ["orjson"] install-types = ["pip"] mypyc = ["setuptools (>=50)"] reports = ["lxml"] [[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 = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, ] [[package]] name = "packaging" version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" files = [ {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] [[package]] name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." optional = false python-versions = ">=3.8" 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.1" description = "Python Build Reasonableness" optional = false python-versions = ">=2.6" files = [ {file = "pbr-6.1.1-py2.py3-none-any.whl", hash = "sha256:38d4daea5d9fa63b3f626131b9d34947fd0c8be9b05a29276870580050a25a76"}, {file = "pbr-6.1.1.tar.gz", hash = "sha256:93ea72ce6989eb2eed99d0f75721474f69ad88128afdef5ac377eb797c4bf76b"}, ] [package.dependencies] setuptools = "*" [[package]] name = "platformdirs" version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, ] [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)"] [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, ] [package.extras] dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] [[package]] name = "pre-commit" version = "3.8.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." optional = false python-versions = ">=3.9" files = [ {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"}, {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"}, ] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" nodeenv = ">=0.11.1" pyyaml = ">=5.1" virtualenv = ">=20.10.0" [[package]] name = "pygments" version = "2.19.1" description = "Pygments is a syntax highlighting package written in Python." optional = false python-versions = ">=3.8" files = [ {file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"}, {file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"}, ] [package.extras] windows-terminal = ["colorama (>=0.4.6)"] [[package]] name = "pytest" version = "8.3.4" description = "pytest: simple powerful testing with Python" optional = false python-versions = ">=3.8" files = [ {file = "pytest-8.3.4-py3-none-any.whl", hash = "sha256:50e16d954148559c9a74109af1eaf0c945ba2d8f30f0a3d3335edde19788b6f6"}, {file = "pytest-8.3.4.tar.gz", hash = "sha256:965370d062bce11e73868e0335abac31b4d3de0e82f4007408d242b4f8610761"}, ] [package.dependencies] colorama = {version = "*", markers = "sys_platform == \"win32\""} exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} iniconfig = "*" packaging = "*" pluggy = ">=1.5,<2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] [[package]] name = "pytest-cov" version = "5.0.0" description = "Pytest plugin for measuring coverage." optional = false python-versions = ">=3.8" files = [ {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, ] [package.dependencies] coverage = {version = ">=5.2.1", extras = ["toml"]} pytest = ">=4.6" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pyyaml" version = "6.0.2" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" files = [ {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] [[package]] name = "reno" version = "4.1.0" description = "RElease NOtes manager" optional = false python-versions = ">=3.6" files = [ {file = "reno-4.1.0-py3-none-any.whl", hash = "sha256:9b6a2cb768ffb7f7c74bbd76822acff70840a1219f45bcec5080dbc108df4f96"}, {file = "reno-4.1.0.tar.gz", hash = "sha256:f992f1fdbd16215ec9de47af08131d53a2830c9e78439eb563ce8d6a7f625370"}, ] [package.dependencies] dulwich = ">=0.15.0" packaging = ">=20.4" pbr = "*" PyYAML = ">=5.3.1" [package.extras] sphinx = ["docutils (>=0.11)", "sphinx (>=2.0.0,!=2.1.0)"] test = ["coverage (>=4.0,!=4.4)", "openstackdocstheme (>=2.2.1)", "python-subunit (>=0.0.18)", "stestr (>=2.0.0)", "testscenarios (>=0.4)", "testtools (>=1.4.0)"] [[package]] name = "requests" version = "2.32.3" description = "Python HTTP for Humans." optional = false python-versions = ">=3.8" files = [ {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, ] [package.dependencies] certifi = ">=2017.4.17" charset-normalizer = ">=2,<4" idna = ">=2.5,<4" urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] [[package]] name = "ruff" version = "0.9.7" description = "An extremely fast Python linter and code formatter, written in Rust." optional = false python-versions = ">=3.7" files = [ {file = "ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4"}, {file = "ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66"}, {file = "ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef"}, {file = "ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb"}, {file = "ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0"}, {file = "ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62"}, {file = "ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0"}, {file = "ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606"}, {file = "ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d"}, {file = "ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c"}, {file = "ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037"}, {file = "ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6"}, ] [[package]] name = "setuptools" version = "75.8.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" optional = false python-versions = ">=3.9" files = [ {file = "setuptools-75.8.0-py3-none-any.whl", hash = "sha256:e3982f444617239225d675215d51f6ba05f845d4eec313da4418fdbb56fb27e3"}, {file = "setuptools-75.8.0.tar.gz", hash = "sha256:c5afc8f407c626b8313a86e10311dd3f661c6cd9c09d4bf8c15c0e11f9f2b0e6"}, ] [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"] core = ["importlib_metadata (>=6)", "jaraco.collections", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "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.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", "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.14.*)", "pytest-mypy"] [[package]] name = "snowballstemmer" version = "2.2.0" description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms." optional = false python-versions = "*" files = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] [[package]] name = "sphinx" version = "7.4.7" description = "Python documentation generator" optional = false python-versions = ">=3.9" files = [ {file = "sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239"}, {file = "sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe"}, ] [package.dependencies] alabaster = ">=0.7.14,<0.8.0" babel = ">=2.13" colorama = {version = ">=0.4.6", markers = "sys_platform == \"win32\""} docutils = ">=0.20,<0.22" imagesize = ">=1.3" importlib-metadata = {version = ">=6.0", markers = "python_version < \"3.10\""} Jinja2 = ">=3.1" packaging = ">=23.0" Pygments = ">=2.17" requests = ">=2.30.0" snowballstemmer = ">=2.2" sphinxcontrib-applehelp = "*" sphinxcontrib-devhelp = "*" sphinxcontrib-htmlhelp = ">=2.0.0" sphinxcontrib-jsmath = "*" sphinxcontrib-qthelp = "*" sphinxcontrib-serializinghtml = ">=1.1.9" tomli = {version = ">=2", markers = "python_version < \"3.11\""} [package.extras] docs = ["sphinxcontrib-websupport"] lint = ["flake8 (>=6.0)", "importlib-metadata (>=6.0)", "mypy (==1.10.1)", "pytest (>=6.0)", "ruff (==0.5.2)", "sphinx-lint (>=0.9)", "tomli (>=2)", "types-docutils (==0.21.0.20240711)", "types-requests (>=2.30.0)"] test = ["cython (>=3.0)", "defusedxml (>=0.7.1)", "pytest (>=8.0)", "setuptools (>=70.0)", "typing_extensions (>=4.9)"] [[package]] name = "sphinx-rtd-theme" version = "3.0.2" description = "Read the Docs theme for Sphinx" optional = false python-versions = ">=3.8" files = [ {file = "sphinx_rtd_theme-3.0.2-py2.py3-none-any.whl", hash = "sha256:422ccc750c3a3a311de4ae327e82affdaf59eb695ba4936538552f3b00f4ee13"}, {file = "sphinx_rtd_theme-3.0.2.tar.gz", hash = "sha256:b7457bc25dda723b20b086a670b9953c859eab60a2a03ee8eb2bb23e176e5f85"}, ] [package.dependencies] docutils = ">0.18,<0.22" sphinx = ">=6,<9" sphinxcontrib-jquery = ">=4,<5" [package.extras] dev = ["bump2version", "transifex-client", "twine", "wheel"] [[package]] name = "sphinxcontrib-applehelp" version = "2.0.0" description = "sphinxcontrib-applehelp is a Sphinx extension which outputs Apple help books" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5"}, {file = "sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-devhelp" version = "2.0.0" description = "sphinxcontrib-devhelp is a sphinx extension which outputs Devhelp documents" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2"}, {file = "sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "sphinxcontrib-htmlhelp" version = "2.1.0" description = "sphinxcontrib-htmlhelp is a sphinx extension which renders HTML help files" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8"}, {file = "sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["html5lib", "pytest"] [[package]] name = "sphinxcontrib-jquery" version = "4.1" description = "Extension to include jQuery on newer Sphinx releases" optional = false python-versions = ">=2.7" files = [ {file = "sphinxcontrib-jquery-4.1.tar.gz", hash = "sha256:1620739f04e36a2c779f1a131a2dfd49b2fd07351bf1968ced074365933abc7a"}, {file = "sphinxcontrib_jquery-4.1-py2.py3-none-any.whl", hash = "sha256:f936030d7d0147dd026a4f2b5a57343d233f1fc7b363f68b3d4f1cb0993878ae"}, ] [package.dependencies] Sphinx = ">=1.8" [[package]] name = "sphinxcontrib-jsmath" version = "1.0.1" description = "A sphinx extension which renders display math in HTML via JavaScript" optional = false python-versions = ">=3.5" files = [ {file = "sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8"}, {file = "sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178"}, ] [package.extras] test = ["flake8", "mypy", "pytest"] [[package]] name = "sphinxcontrib-qthelp" version = "2.0.0" description = "sphinxcontrib-qthelp is a sphinx extension which outputs QtHelp documents" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb"}, {file = "sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["defusedxml (>=0.7.1)", "pytest"] [[package]] name = "sphinxcontrib-serializinghtml" version = "2.0.0" description = "sphinxcontrib-serializinghtml is a sphinx extension which outputs \"serialized\" HTML files (json and pickle)" optional = false python-versions = ">=3.9" files = [ {file = "sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331"}, {file = "sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d"}, ] [package.extras] lint = ["mypy", "ruff (==0.5.5)", "types-docutils"] standalone = ["Sphinx (>=5)"] test = ["pytest"] [[package]] name = "tomli" version = "2.2.1" description = "A lil' TOML parser" optional = false python-versions = ">=3.8" files = [ {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, ] [[package]] name = "types-requests" version = "2.32.0.20241016" description = "Typing stubs for requests" optional = false python-versions = ">=3.8" files = [ {file = "types-requests-2.32.0.20241016.tar.gz", hash = "sha256:0d9cad2f27515d0e3e3da7134a1b6f28fb97129d86b867f24d9c726452634d95"}, {file = "types_requests-2.32.0.20241016-py3-none-any.whl", hash = "sha256:4195d62d6d3e043a4eaaf08ff8a62184584d2e8684e9d2aa178c7915a7da3747"}, ] [package.dependencies] urllib3 = ">=2" [[package]] name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" optional = false python-versions = ">=3.8" 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"}, ] [[package]] name = "urllib3" version = "2.3.0" description = "HTTP library with thread-safe connection pooling, file post, and more." optional = false python-versions = ">=3.9" files = [ {file = "urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df"}, {file = "urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d"}, ] [package.extras] brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" version = "20.29.2" description = "Virtual Python Environment builder" optional = false python-versions = ">=3.8" files = [ {file = "virtualenv-20.29.2-py3-none-any.whl", hash = "sha256:febddfc3d1ea571bdb1dc0f98d7b45d24def7428214d4fb73cc486c9568cce6a"}, {file = "virtualenv-20.29.2.tar.gz", hash = "sha256:fdaabebf6d03b5ba83ae0a02cfe96f48a716f4fae556461d180825866f75b728"}, ] [package.dependencies] distlib = ">=0.3.7,<1" filelock = ">=3.12.2,<4" platformdirs = ">=3.9.1,<5" [package.extras] docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "werkzeug" version = "3.1.3" description = "The comprehensive WSGI web application library." optional = false python-versions = ">=3.9" files = [ {file = "werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e"}, {file = "werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746"}, ] [package.dependencies] MarkupSafe = ">=2.1.1" [package.extras] watchdog = ["watchdog (>=2.3)"] [[package]] name = "zipp" version = "3.21.0" description = "Backport of pathlib-compatible object wrapper for zip files" optional = false python-versions = ">=3.9" files = [ {file = "zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931"}, {file = "zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4"}, ] [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.9" content-hash = "f77a0e474c352508e5310456b075310800fa48ba8c32ff668513c3da98b0c134" pytest-httpserver-1.1.2/pyproject.toml000066400000000000000000000052661475715145300202100ustar00rootroot00000000000000[tool.poetry] name = "pytest_httpserver" version = "1.1.2" description = "pytest-httpserver is a httpserver for pytest" authors = ["Zsolt Cserna "] license = "MIT" readme = "README.md" documentation = "https://pytest-httpserver.readthedocs.io/en/latest/" classifiers = [ "Development Status :: 3 - Alpha", "Intended Audience :: Developers", "Operating System :: OS Independent", "Topic :: Software Development :: Libraries :: Python Modules", "Framework :: Pytest", ] repository = "https://github.com/csernazs/pytest-httpserver" include = [ { path = "tests", format = "sdist" }, { path = "CHANGES.rst", format = "sdist" }, { path = "CONTRIBUTION.md", format = "sdist" }, { path = "example*.py", format = "sdist" }, { path = "doc", format = "sdist" }, ] [tool.poetry.dependencies] python = ">=3.9" Werkzeug = ">= 2.0.0" [tool.poetry.plugins.pytest11] pytest_httpserver = "pytest_httpserver.pytest_plugin" [tool.poetry.urls] "Bug Tracker" = "https://github.com/csernazs/pytest-httpserver/issues" [tool.poetry.group.develop] optional = true [tool.poetry.group.develop.dependencies] pre-commit = ">=2.20,<4.0" requests = "*" Sphinx = ">=5.1.1,<8.0.0" sphinx-rtd-theme = ">=1,<4" reno = "*" types-requests = "*" pytest = ">=7.1.3,<9.0.0" pytest-cov = ">=3,<6" coverage = ">=6.4.4,<8.0.0" tomli = { version = "*", markers = "python_version < '3.11'"} black = "*" ruff = "*" mypy = "*" [tool.poetry.group.doc] optional = true [tool.poetry.group.doc.dependencies] Sphinx = ">=5.1.1,<8.0.0" sphinx-rtd-theme = ">=1,<4" [tool.poetry.group.test] optional = true [tool.poetry.group.test.dependencies] pytest = "*" pytest-cov = "*" coverage = "*" requests = "*" types-requests = "*" pre-commit = "*" tomli = { version = "*", markers = "python_version < '3.11'"} mypy = "*" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] markers = [ "ssl: set up ssl context", "release: run release tests", ] [tool.mypy] files = ["pytest_httpserver", "scripts", "tests"] implicit_reexport = false [tool.black] line-length = 120 safe = true [tool.ruff] lint.select = ["ALL"] lint.ignore = [ "I", "D", "ANN", "ARG005", "B011", "B904", "C408", "C901", "COM812", "EM101", "EM103", "FBT002", "FIX002", "INP001", "PGH003", "PLR0912", "PLR0913", "PLR2004", "PLW2901", "PT004", "PT012", "PT013", "PTH118", "PTH120", "RET504", "RET505", "RET506", "RUF005", "S101", "S113", "S603", "S607", "SIM108", "T201", "TD002", "TD003", "TRY003", "UP032", ] line-length = 120 target-version = "py39" exclude = ["doc", "example*.py", "tests/examples/*.py"] pytest-httpserver-1.1.2/pytest_httpserver/000077500000000000000000000000001475715145300211015ustar00rootroot00000000000000pytest-httpserver-1.1.2/pytest_httpserver/__init__.py000066400000000000000000000016011475715145300232100ustar00rootroot00000000000000""" This is package provides the main API for the pytest_httpserver package. """ __all__ = [ "METHOD_ALL", "URI_DEFAULT", "BlockingHTTPServer", "BlockingRequestHandler", "Error", "HTTPServer", "HTTPServerError", "HeaderValueMatcher", "NoHandlerError", "RequestHandler", "RequestMatcher", "URIPattern", "WaitingSettings", ] from .blocking_httpserver import BlockingHTTPServer from .blocking_httpserver import BlockingRequestHandler from .httpserver import METHOD_ALL from .httpserver import URI_DEFAULT from .httpserver import Error from .httpserver import HeaderValueMatcher from .httpserver import HTTPServer from .httpserver import HTTPServerError from .httpserver import NoHandlerError from .httpserver import RequestHandler from .httpserver import RequestMatcher from .httpserver import URIPattern from .httpserver import WaitingSettings pytest-httpserver-1.1.2/pytest_httpserver/blocking_httpserver.py000066400000000000000000000161101475715145300255300ustar00rootroot00000000000000from __future__ import annotations from queue import Empty from queue import Queue from typing import TYPE_CHECKING from typing import Any from pytest_httpserver.httpserver import METHOD_ALL from pytest_httpserver.httpserver import UNDEFINED from pytest_httpserver.httpserver import HeaderValueMatcher from pytest_httpserver.httpserver import HTTPServerBase from pytest_httpserver.httpserver import QueryMatcher from pytest_httpserver.httpserver import RequestHandlerBase from pytest_httpserver.httpserver import URIPattern if TYPE_CHECKING: from collections.abc import Mapping from re import Pattern from ssl import SSLContext from werkzeug import Request from werkzeug import Response class BlockingRequestHandler(RequestHandlerBase): """ Provides responding to a request synchronously. This class should only be instantiated inside the implementation of the :py:class:`BlockingHTTPServer`. """ def __init__(self): self.response_queue = Queue() def respond_with_response(self, response: Response): self.response_queue.put_nowait(response) class BlockingHTTPServer(HTTPServerBase): """ Server instance which enables synchronous matching for incoming requests. :param host: the host or IP where the server will listen :param port: the TCP port where the server will listen :param ssl_context: the ssl context object to use for https connections :param timeout: waiting time in seconds for matching and responding to an incoming request. manager .. py:attribute:: no_handler_status_code Attribute containing the http status code (int) which will be the response status when no matcher is found for the request. By default, it is set to *500* but it can be overridden to any valid http status code such as *404* if needed. """ DEFAULT_LISTEN_HOST = "localhost" DEFAULT_LISTEN_PORT = 0 # Use ephemeral port def __init__( self, host=DEFAULT_LISTEN_HOST, port=DEFAULT_LISTEN_PORT, ssl_context: SSLContext | None = None, timeout: int = 30, ): super().__init__(host, port, ssl_context) self.timeout = timeout self.request_queue: Queue[Request] = Queue() self.request_handlers: dict[Request, Queue[BlockingRequestHandler]] = {} def assert_request( self, uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, data: str | bytes | None = None, data_encoding: str = "utf-8", headers: Mapping[str, str] | None = None, query_string: None | QueryMatcher | str | bytes | Mapping = None, header_value_matcher: HeaderValueMatcher | None = None, json: Any = UNDEFINED, timeout: int = 30, ) -> BlockingRequestHandler: """ Wait for an incoming request and check whether it matches according to the given parameters. If the incoming request matches, a request handler is created and registered, otherwise assertion error is raised. The request handler can be used once to respond for the request. If no response is performed in the period given in the timeout parameter of the constructor or no request arrives in the `timeout` period, assertion error is raised. :param uri: URI of the request. This must be an absolute path starting with ``/``, a :py:class:`URIPattern` object, or a regular expression compiled by :py:func:`re.compile`. :param method: HTTP method of the request. If not specified (or `METHOD_ALL` specified), all HTTP requests will match. :param data: payload of the HTTP request. This could be a string (utf-8 encoded by default, see `data_encoding`) or a bytes object. :param data_encoding: the encoding used for data parameter if data is a string. :param headers: dictionary of the headers of the request to be matched :param query_string: the http query string, after ``?``, such as ``username=user``. If string is specified it will be encoded to bytes with the encode method of the string. If dict is specified, it will be matched to the ``key=value`` pairs specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers. :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. :param timeout: waiting time in seconds for an incoming request. :return: Created and registered :py:class:`BlockingRequestHandler`. Parameters `json` and `data` are mutually exclusive. """ matcher = self.create_matcher( uri, method=method.upper(), data=data, data_encoding=data_encoding, headers=headers, query_string=query_string, header_value_matcher=header_value_matcher, json=json, ) try: request = self.request_queue.get(timeout=timeout) except Empty: raise AssertionError(f"Waiting for request {matcher} timed out") # noqa: EM102 diff = matcher.difference(request) request_handler = BlockingRequestHandler() self.request_handlers[request].put_nowait(request_handler) if diff: request_handler.respond_with_response(self.respond_nohandler(request)) raise AssertionError(f"Request {matcher} does not match: {diff}") # noqa: EM102 return request_handler def dispatch(self, request: Request) -> Response: """ Dispatch a request for synchronous matching. This method queues the request for matching and waits for the request handler. If there was no request handler, error is responded, otherwise it waits for the response of request handler. If no response arrives, assertion error is raised, otherwise the response is returned. :param request: the request object from the werkzeug library. :return: the response object what the handler responded, or a response which contains the error. """ self.request_handlers[request] = Queue() try: self.request_queue.put_nowait(request) try: request_handler = self.request_handlers[request].get(timeout=self.timeout) except Empty: return self.respond_nohandler(request) try: return request_handler.response_queue.get(timeout=self.timeout) except Empty: assertion = AssertionError(f"No response for request: {request}") self.add_assertion(assertion) raise assertion finally: del self.request_handlers[request] pytest-httpserver-1.1.2/pytest_httpserver/hooks.py000066400000000000000000000057161475715145300226070ustar00rootroot00000000000000""" Hooks for pytest-httpserver """ import os import time from typing import Callable from werkzeug import Request from werkzeug import Response class Chain: """ Combine multiple hooks into one callable object Hooks specified will be called one by one. Each hook will receive the response object made by the previous hook, similar to reduce. """ def __init__(self, *args: Callable[[Request, Response], Response]): """ :param *args: callable objects specified in the same order they should be called. """ self._hooks = args def __call__(self, request: Request, response: Response) -> Response: """ Calls the callable object one by one. The second and further callable objects receive the response returned by the previous one, while the first one receives the original response object. """ for hook in self._hooks: response = hook(request, response) return response class Delay: """ Delays returning the response """ def __init__(self, seconds: float): """ :param seconds: seconds to sleep before returning the response """ self._seconds = seconds def _sleep(self): """ Sleeps for the seconds specified in the constructor """ time.sleep(self._seconds) def __call__(self, _request: Request, response: Response) -> Response: """ Delays returning the response object for the time specified in the constructor. Returns the original response unmodified. """ self._sleep() return response class Garbage: def __init__(self, prefix_size: int = 0, suffix_size: int = 0): """ Adds random bytes to the beginning or to the end of the response data. :param prefix_size: amount of random bytes to be added to the beginning of the response data :param suffix_size: amount of random bytes to be added to the end of the response data """ assert prefix_size >= 0, "prefix_size should be positive integer" assert suffix_size >= 0, "suffix_size should be positive integer" self._prefix_size = prefix_size self._suffix_size = suffix_size def _get_garbage_bytes(self, size: int) -> bytes: """ Returns the specified amount of random bytes. :param size: amount of bytes to return """ return os.urandom(size) def __call__(self, _request: Request, response: Response) -> Response: """ Adds random bytes to the beginning or to the end of the response data. New random bytes will be generated for every call. Returns the modified response object. """ prefix = self._get_garbage_bytes(self._prefix_size) suffix = self._get_garbage_bytes(self._suffix_size) response.set_data(prefix + response.get_data() + suffix) return response pytest-httpserver-1.1.2/pytest_httpserver/httpserver.py000066400000000000000000001537211475715145300236720ustar00rootroot00000000000000from __future__ import annotations import abc import ipaddress import json import queue import re import threading import time import urllib.parse from collections import defaultdict from collections.abc import Iterable from collections.abc import Mapping from collections.abc import MutableMapping from contextlib import contextmanager from contextlib import suppress from copy import copy from enum import Enum from re import Pattern from typing import TYPE_CHECKING from typing import Any from typing import Callable from typing import ClassVar from typing import Optional from typing import Union import werkzeug.http from werkzeug import Request from werkzeug import Response from werkzeug.datastructures import Authorization from werkzeug.datastructures import MultiDict from werkzeug.serving import make_server if TYPE_CHECKING: from ssl import SSLContext from types import TracebackType from werkzeug.serving import BaseWSGIServer URI_DEFAULT = "" METHOD_ALL = "__ALL" HEADERS_T = Union[ Mapping[str, Union[str, Iterable[str]]], Iterable[tuple[str, str]], ] HVMATCHER_T = Callable[[str, Optional[str], str], bool] class Undefined: def __repr__(self): return "" UNDEFINED = Undefined() class Error(Exception): """ Base class for all exception defined in this package. """ class NoHandlerError(Error): """ Raised when a :py:class:`RequestHandler` has no registered method to serve the request. """ class HTTPServerError(Error): """ Raised when there's a problem with HTTP server. """ class NoMethodFoundForMatchingHeaderValueError(Error): """ Raised when a :py:class:`HeaderValueMatcher` has no registered method to match the header value. """ class WaitingSettings: """Class for providing default settings and storing them in HTTPServer :param raise_assertions: whether raise assertions on unexpected request or timeout or not :param stop_on_nohandler: whether stop on unexpected request or not :param timeout: time (in seconds) until time is out """ def __init__( self, raise_assertions: bool = True, # noqa: FBT001 stop_on_nohandler: bool = True, # noqa: FBT001 timeout: float = 5, ): self.raise_assertions = raise_assertions self.stop_on_nohandler = stop_on_nohandler self.timeout = timeout class Waiting: """Class for HTTPServer.wait context manager This class should not be instantiated directly.""" def __init__(self): self._result = None self._start = time.monotonic() self._stop = None def complete(self, result: bool): # noqa: FBT001 self._result = result self._stop = time.monotonic() @property def result(self) -> bool: return bool(self._result) @property def elapsed_time(self) -> float: """Elapsed time in seconds""" if self._stop is None: raise TypeError("unsupported operand type(s) for -: 'NoneType' and 'float'") return self._stop - self._start class HeaderValueMatcher: """ Matcher object for the header value of incoming request. :param matchers: mapping from header name to comparator function that accepts actual and expected header values and return whether they are equal as bool. """ DEFAULT_MATCHERS: ClassVar[MutableMapping[str, Callable[[str | None, str], bool]]] = {} def __init__(self, matchers: Mapping[str, Callable[[str | None, str], bool]] | None = None): self.matchers = self.DEFAULT_MATCHERS if matchers is None else matchers @staticmethod def authorization_header_value_matcher(actual: str | None, expected: str) -> bool: func = getattr(Authorization, "from_header", None) if func is None: # Werkzeug < 2.3.0 func = werkzeug.http.parse_authorization_header # type: ignore[attr-defined] return func(actual) == func(expected) # type: ignore @staticmethod def default_header_value_matcher(actual: str | None, expected: str) -> bool: return actual == expected def __call__(self, header_name: str, actual: str | None, expected: str) -> bool: try: matcher = self.matchers[header_name] except KeyError: raise NoMethodFoundForMatchingHeaderValueError( "No method found for matching header value: {}".format(header_name) ) return matcher(actual, expected) HeaderValueMatcher.DEFAULT_MATCHERS = defaultdict( lambda: HeaderValueMatcher.default_header_value_matcher, {"Authorization": HeaderValueMatcher.authorization_header_value_matcher}, ) class QueryMatcher(abc.ABC): """ Abstract class for QueryMatchers get_comparing_values should return a 2-element tuple whose elements will be compared. """ def match(self, request_query_string: bytes) -> bool: values = self.get_comparing_values(request_query_string) return values[0] == values[1] @abc.abstractmethod def get_comparing_values(self, request_query_string: bytes) -> tuple[Any, Any]: pass class StringQueryMatcher(QueryMatcher): """ Matches a query for a string or bytes specified """ def __init__(self, query_string: bytes | str): """ :param query_string: the query string will be compared to this string or bytes. If string is specified, it will be encoded by the encode() method. The query must not start with '?' but will be exactly (byte-by-byte) equal the actual query string of the incoming request. """ if not isinstance(query_string, (str, bytes)): raise TypeError("query_string must be a string, or a bytes-like object") self.query_string = query_string def get_comparing_values(self, request_query_string: bytes) -> tuple[bytes, bytes]: if isinstance(self.query_string, str): query_string = self.query_string.encode() elif isinstance(self.query_string, bytes): # type: ignore query_string = self.query_string else: raise TypeError("query_string must be a string, or a bytes-like object") return (request_query_string, query_string) class MappingQueryMatcher(QueryMatcher): """ Matches a query string to a dictionary or MultiDict specified """ def __init__(self, query_dict: Mapping[str, str] | MultiDict[str, str]): """ :param query_dict: if dictionary (Mapping) is specified, it will be used as a key-value mapping where both key and value should be string. If there are multiple values specified for the same key in the request, the first element will be used. If you want to match multiple values, use a MultiDict object from werkzeug, which represents multiple values for one key. """ self.query_dict = query_dict def get_comparing_values(self, request_query_string: bytes) -> tuple[Mapping[str, str], Mapping[str, str]]: query = MultiDict(urllib.parse.parse_qsl(request_query_string.decode("utf-8"))) if isinstance(self.query_dict, MultiDict): return (query, self.query_dict) else: return (query.to_dict(), dict(self.query_dict)) class BooleanQueryMatcher(QueryMatcher): """ Matches the query depending on the boolean value """ def __init__(self, result: bool): # noqa: FBT001 """ :param result: if this parameter is true, the query match will be always successful. Otherwise, no query match will be successful. """ self.result = result def get_comparing_values(self, request_query_string: bytes): # noqa: ARG002 if self.result: return (True, True) else: return (True, False) def _create_query_matcher(query_string: None | QueryMatcher | str | bytes | Mapping[str, str]) -> QueryMatcher: if isinstance(query_string, QueryMatcher): return query_string if query_string is None: return BooleanQueryMatcher(result=True) if isinstance(query_string, (str, bytes)): return StringQueryMatcher(query_string) if isinstance(query_string, Mapping): return MappingQueryMatcher(query_string) raise TypeError("Unable to cast this type to QueryMatcher: {!r}".format(type(query_string))) class URIPattern(abc.ABC): @abc.abstractmethod def match(self, uri: str) -> bool: """ Matches the provided URI. :param uri: URI of the request. This is an absolute path starting with "/" and does not contain the query part. :return: True if there's a match, False otherwise """ class RequestMatcher: """ Matcher object for the incoming request. It defines various parameters to match the incoming request. :param uri: URI of the request. This must be an absolute path starting with ``/``, a :py:class:`URIPattern` object, or a regular expression compiled by :py:func:`re.compile`. :param method: HTTP method of the request. If not specified (or `METHOD_ALL` specified), all HTTP requests will match. :param data: payload of the HTTP request. This could be a string (utf-8 encoded by default, see `data_encoding`) or a bytes object. :param data_encoding: the encoding used for data parameter if data is a string. :param headers: dictionary of the headers of the request to be matched :param query_string: the http query string, after ``?``, such as ``username=user``. If string is specified it will be encoded to bytes with the encode method of the string. If dict is specified, it will be matched to the ``key=value`` pairs specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers, or a ``Callable[[str, Optional[str], str], bool]`` receiving the header key (from `headers`), header value (or `None`) and the expected value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. """ def __init__( self, uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, data: str | bytes | None = None, data_encoding: str = "utf-8", headers: Mapping[str, str] | None = None, query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None, header_value_matcher: HVMATCHER_T | None = None, json: Any = UNDEFINED, ): if json is not UNDEFINED and data is not None: raise ValueError("data and json parameters are mutually exclusive") self.uri = uri self.method = method self.query_string = query_string self.query_matcher = _create_query_matcher(self.query_string) self.json = json self.headers: Mapping[str, str] = {} if headers is not None: self.headers = headers if isinstance(data, str): data = data.encode(data_encoding) self.data = data self.data_encoding = data_encoding self.header_value_matcher: HVMATCHER_T = HeaderValueMatcher() if header_value_matcher is not None: self.header_value_matcher = header_value_matcher def __repr__(self): """ Returns the string representation of the object, with the known parameters. """ class_name = self.__class__.__name__ retval = "<{} ".format(class_name) retval += ( "uri={uri!r} method={method!r} query_string={query_string!r} " "headers={headers!r} data={data!r} json={json!r}>" ).format_map(self.__dict__) return retval def match_data(self, request: Request) -> bool: """ Matches the data part of the request :param request: the HTTP request :return: `True` when the data is matched or no matching is required. `False` otherwise. """ if self.data is None: return True return request.data == self.data def match_uri(self, request: Request) -> bool: path = request.path if isinstance(self.uri, URIPattern): return self.uri.match(path) # this is python version depending # in python 3.7 and above: it is re.Pattern # below python 3.7 it is _sre.SRE_Pattern which cannot be accessed directly elif isinstance(self.uri, re.compile("").__class__): return bool(self.uri.match(path)) else: # there could be a guard isinstance(self.uri, str) been here # but we want to allow any object which provides the __eq__ parameter # (note: in this case it will be not typeing correct) # # also, python will raise TypeError when self.uri is a conflicting type return self.uri in (URI_DEFAULT, path) def match_json(self, request: Request) -> bool: """ Matches the request data as json. Load the request data as json and compare it to self.json which is a json-serializable data structure (eg. a dict or list). :param request: the HTTP request :return: `True` when the data is matched or no matching is required. `False` otherwise. """ if self.json is UNDEFINED: return True try: # do the decoding here as python 3.5 requires string and does not # accept bytes json_received = json.loads(request.data.decode(self.data_encoding)) except json.JSONDecodeError: return False except UnicodeDecodeError: return False return json_received == self.json def difference(self, request: Request) -> list[tuple[str, str, str | URIPattern]]: """ Calculates the difference between the matcher and the request. Returns a list of fields where there's a difference between the request and the matcher. The returned list may have zero or more elements, each element is a three-element tuple containing the field name, the request value, and the matcher value. If zero-length list is returned, this means that there's no difference, so the request matches the fields set in the matcher object. """ retval: list[tuple[str, Any, Any]] = [] if not self.match_uri(request): retval.append(("uri", request.path, self.uri)) if self.method not in (METHOD_ALL, request.method): retval.append(("method", request.method, self.method)) if not self.query_matcher.match(request.query_string): retval.append(("query_string", request.query_string, self.query_string)) request_headers: dict[str, str | None] = {} expected_headers: dict[str, str] = {} for key, value in self.headers.items(): if not self.header_value_matcher(key, request.headers.get(key), value): request_headers[key] = request.headers.get(key) expected_headers[key] = value if request_headers and expected_headers: retval.append(("headers", request_headers, expected_headers)) if not self.match_data(request): retval.append(("data", request.data, self.data)) if not self.match_json(request): retval.append(("json", request.data, self.json)) return retval def match(self, request: Request) -> bool: """ Returns whether the request matches the parameters set in the matcher object or not. `True` value is returned when it matches, `False` otherwise. """ difference = self.difference(request) return not difference class RequestHandlerBase(abc.ABC): """ Represents a :py:class:`RequestHandler` object providing a response for the corresponding request. """ def respond_with_json( self, response_json: Any, status: int = 200, headers: Mapping[str, str] | None = None, content_type: str = "application/json", ): """ Prepares a response with a serialized JSON object. :param response_json: a JSON-serializable python object :param status: the HTTP status of the response :param headers: the HTTP headers to be sent (excluding the Content-Type header) :param content_type: the content type header to be sent """ response_data = json.dumps(response_json, indent=4) self.respond_with_data(response_data, status, headers, content_type=content_type) def respond_with_data( self, response_data: str | bytes = "", status: int = 200, headers: HEADERS_T | None = None, mimetype: str | None = None, content_type: str | None = None, ): """ Prepares a response with raw data. For detailed description please see the :py:class:`werkzeug.Response` object as the parameters are analogue. :param response_data: a string or bytes object representing the body of the response :param status: the HTTP status of the response :param headers: the HTTP headers to be sent (excluding the Content-Type header) :param content_type: the content type header to be sent :param mimetype: the mime type of the request """ self.respond_with_response(Response(response_data, status, headers, mimetype, content_type)) @abc.abstractmethod def respond_with_response(self, response: Response): """ Prepares a response with the specified response object. :param response: the response object which will be responded """ class RequestHandler(RequestHandlerBase): """ Represents a response function and a :py:class:`RequestHandler` object. This class connects the matcher object with the function responsible for the response. The respond handler function can be registered with the `respond_with_` methods. :param matcher: the matcher object """ def __init__(self, matcher: RequestMatcher): self.matcher = matcher self.request_handler: Callable[[Request], Response] | None = None self._hooks: list[Callable[[Request, Response], Response]] = [] def with_post_hook(self, hook: Callable[[Request, Response], Response]): self._hooks.append(hook) return self def respond(self, request: Request) -> Response: """ Calls the request handler registered for this object. If no response was specified previously, it raises :py:class:`NoHandlerError` exception. :param request: the incoming request object :return: the response object """ if self.request_handler is None: raise NoHandlerError( "Matching request handler found but no response defined: {} {}".format(request.method, request.path) ) else: response = self.request_handler(request) for hook in self._hooks: response = hook(request, response) return response def respond_with_handler(self, func: Callable[[Request], Response]): """ Registers the specified function as a responder. The function will receive the request object and must return with the response object. """ self.request_handler = func def respond_with_response(self, response: Response): self.request_handler = lambda request: response def __repr__(self) -> str: class_name = self.__class__.__name__ retval = ( f"<{class_name} uri={self.matcher.uri!r} method={self.matcher.method!r} " f"query_string={self.matcher.query_string!r} headers={self.matcher.headers!r} data={self.matcher.data!r} " f"json={self.matcher.json!r}>" ) return retval class RequestHandlerList(list[RequestHandler]): """ Represents a list of :py:class:`RequestHandler` objects. """ def match(self, request: Request) -> RequestHandler | None: """ Returns the first request handler which matches the specified request. Otherwise, it returns `None`. """ for requesthandler in self: if requesthandler.matcher.match(request): return requesthandler return None class HandlerType(Enum): PERMANENT = "permanent" ONESHOT = "oneshot" ORDERED = "ordered" class HTTPServerBase(abc.ABC): # pylint: disable=too-many-instance-attributes """ Abstract HTTP server with error handling. :param host: the host or IP where the server will listen :param port: the TCP port where the server will listen :param ssl_context: the ssl context object to use for https connections :param threaded: whether to handle concurrent requests in separate threads .. py:attribute:: log Attribute containing the list of two-element tuples. Each tuple contains :py:class:`werkzeug.Request` and :py:class:`werkzeug.Response` object which represents the incoming request and the outgoing response which happened during the lifetime of the server. .. py:attribute:: no_handler_status_code Attribute containing the http status code (int) which will be the response status when no matcher is found for the request. By default, it is set to *500* but it can be overridden to any valid http status code such as *404* if needed. """ def __init__( self, host: str, port: int, ssl_context: SSLContext | None = None, *, threaded: bool = False, ): """ Initializes the instance. """ self.host = host self.port = port self.server: BaseWSGIServer | None = None self.server_thread: threading.Thread | None = None self.assertions: list[str | AssertionError] = [] self.handler_errors: list[Exception] = [] self.log: list[tuple[Request, Response]] = [] self.ssl_context = ssl_context self.threaded = threaded self.no_handler_status_code = 500 def __repr__(self): return f"<{self.__class__.__name__} host={self.host} port={self.port}>" def clear(self): """ Clears and resets the state attributes of the object. This method is useful when the object needs to be re-used but stopping the server is not feasible. """ self.clear_assertions() self.clear_handler_errors() self.clear_log() self.no_handler_status_code = 500 def clear_assertions(self): """ Clears the list of assertions """ self.assertions = [] def clear_handler_errors(self): """ Clears the list of collected errors from handler invocations """ self.handler_errors = [] def clear_log(self): """ Clears the list of log entries """ self.log = [] def url_for(self, suffix: str): """ Return an url for a given suffix. This basically means that it prepends the string ``http://$HOST:$PORT/`` to the `suffix` parameter (where $HOST and $PORT are the parameters given to the constructor). When host is an IPv6 address, the required square brackets will be added to it, forming a valid URL. When SSL or TLS is in use, the protocol of the returned URL will be ``https``. :param suffix: the suffix which will be added to the base url. It can start with ``/`` (slash) or not, the url will be the same. :return: the full url which refers to the server """ if not suffix.startswith("/"): suffix = "/" + suffix if self.ssl_context is None: protocol = "http" else: protocol = "https" host = self.format_host(self.host) return "{}://{}:{}{}".format(protocol, host, self.port, suffix) def create_matcher(self, *args, **kwargs) -> RequestMatcher: """ Creates a :py:class:`.RequestMatcher` instance with the specified parameters. This method can be overridden if you want to use your own matcher. """ return RequestMatcher(*args, **kwargs) def thread_target(self): """ This method serves as a thread target when the server is started. This should not be called directly, but can be overridden to tailor it to your needs. """ assert self.server is not None self.server.serve_forever() def is_running(self) -> bool: """ Returns `True` when the server is running, otherwise `False`. """ return bool(self.server) def start(self) -> None: """ Start the server in a thread. This method returns immediately (e.g. does not block), and it's the caller's responsibility to stop the server (by calling :py:meth:`stop`) when it is no longer needed). If the server is not stopped by the caller and execution reaches the end, the program needs to be terminated by Ctrl+C or by signal as it will not terminate until the thread is stopped. If the server is already running :py:class:`HTTPServerError` will be raised. If you are unsure, call :py:meth:`is_running` first. There's a context interface of this class which stops the server when the context block ends. """ if self.is_running(): raise HTTPServerError("Server is already running") app = Request.application(self.application) self.server = make_server( self.host, self.port, app, ssl_context=self.ssl_context, threaded=self.threaded, ) self.port = self.server.port # Update port (needed if `port` was set to 0) self.server_thread = threading.Thread(target=self.thread_target) self.server_thread.start() def stop(self): """ Stop the running server. Notifies the server thread about the intention of the stopping, and the thread will terminate itself. This needs about 0.5 seconds in worst case. Only a running server can be stopped. If the sever is not running, :py:class`HTTPServerError` will be raised. """ assert self.server is not None assert self.server_thread is not None if not self.is_running(): raise HTTPServerError("Server is not running") self.server.shutdown() self.server_thread.join() self.server = None self.server_thread = None def add_assertion(self, obj: str | AssertionError): """ Add a new assertion Assertions can be added here, and when :py:meth:`check_assertions` is called, it will raise AssertionError for pytest with the object specified here. :param obj: An AssertionError, or an object which will be passed to an AssertionError. """ self.assertions.append(obj) def check(self): """ Raises AssertionError or Errors raised in handlers. Runs both :py:meth:`check_assertions` and :py:meth:`check_handler_errors` """ self.check_assertions() self.check_handler_errors() def check_assertions(self): """ Raise AssertionError when at least one assertion added The first assertion added by :py:meth:`add_assertion` will be raised and it will be removed from the list. This method can be useful to get some insights into the errors happened in the sever, and to have a proper error reporting in pytest. """ if self.assertions: assertion = self.assertions.pop(0) if isinstance(assertion, AssertionError): raise assertion raise AssertionError(assertion) def check_handler_errors(self): """ Re-Raises any errors caused in request handlers The first error raised by a handler will be re-raised here, and then removed from the list. """ if self.handler_errors: raise self.handler_errors.pop(0) def respond_nohandler(self, request: Request, extra_message: str = ""): """ Add a 'no handler' assertion. This method is called when the server wasn't able to find any handler to serve the request. As the result, there's an assertion added (which can be raised by :py:meth:`check_assertions`). """ text = "No handler found for request {!r} with data {!r}.".format(request, request.data) self.add_assertion(text + extra_message) return Response(text + extra_message, self.no_handler_status_code) @abc.abstractmethod def dispatch(self, request: Request) -> Response: """ Dispatch a request to the appropriate request handler. :param request: the request object from the werkzeug library :return: the response object what the handler responded, or a response which contains the error """ def application(self, request: Request) -> Response: """ Entry point of werkzeug. This method is called for each request, and it then calls the undecorated :py:meth:`dispatch` method to serve the request. :param request: the request object from the werkzeug library :return: the response object what the dispatch returned """ request.get_data() response = self.dispatch(request) self.log.append((request, response)) return response def __enter__(self): """ Provide the context API It starts the server in a thread if the server is not already running. """ if not self.is_running(): self.start() return self def __exit__( self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None, ): """ Provide the context API It stops the server if the server is running. Please note that depending on the internal things of werkzeug, it may take 0.5 seconds. """ if self.is_running(): self.stop() @staticmethod def format_host(host: str): """ Formats a hostname so it can be used in a URL. Notably, this adds brackets around IPV6 addresses when they are missing. """ try: ipaddress.IPv6Address(host) is_ipv6 = True except ValueError: is_ipv6 = False if is_ipv6 and not host.startswith("[") and not host.endswith("]"): return f"[{host}]" return host class HTTPServer(HTTPServerBase): # pylint: disable=too-many-instance-attributes """ Server instance which manages handlers to serve pre-defined requests. :param host: the host or IP where the server will listen :param port: the TCP port where the server will listen :param ssl_context: the ssl context object to use for https connections :param default_waiting_settings: the waiting settings object to use as default settings for :py:meth:`wait` context manager :param threaded: whether to handle concurrent requests in separate threads .. py:attribute:: no_handler_status_code Attribute containing the http status code (int) which will be the response status when no matcher is found for the request. By default, it is set to *500* but it can be overridden to any valid http status code such as *404* if needed. """ DEFAULT_LISTEN_HOST = "localhost" DEFAULT_LISTEN_PORT = 0 # Use ephemeral port def __init__( self, host: str = DEFAULT_LISTEN_HOST, port: int = DEFAULT_LISTEN_PORT, ssl_context: SSLContext | None = None, default_waiting_settings: WaitingSettings | None = None, *, threaded: bool = False, ): """ Initializes the instance. """ super().__init__(host, port, ssl_context, threaded=threaded) self.ordered_handlers: list[RequestHandler] = [] self.oneshot_handlers = RequestHandlerList() self.handlers = RequestHandlerList() self.permanently_failed = False if default_waiting_settings is not None: self.default_waiting_settings = default_waiting_settings else: self.default_waiting_settings = WaitingSettings() self._waiting_settings = copy(self.default_waiting_settings) self._waiting_result: queue.LifoQueue[bool] = queue.LifoQueue(maxsize=1) def clear(self): """ Clears and resets the state attributes of the object. This method is useful when the object needs to be re-used but stopping the server is not feasible. """ super().clear() self.clear_all_handlers() self.permanently_failed = False def clear_all_handlers(self): """ Clears all types of the handlers (ordered, oneshot, permanent) """ self.ordered_handlers = [] self.oneshot_handlers = RequestHandlerList() self.handlers = RequestHandlerList() def expect(self, matcher: RequestMatcher, handler_type: HandlerType = HandlerType.PERMANENT) -> RequestHandler: """ Create and register a request handler. :param matcher: :py:class:`RequestMatcher` used to match requests. :param handler_type: type of handler """ request_handler = RequestHandler(matcher) if handler_type == HandlerType.PERMANENT: self.handlers.append(request_handler) elif handler_type == HandlerType.ONESHOT: self.oneshot_handlers.append(request_handler) elif handler_type == HandlerType.ORDERED: self.ordered_handlers.append(request_handler) return request_handler def expect_request( self, uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, data: str | bytes | None = None, data_encoding: str = "utf-8", headers: Mapping[str, str] | None = None, query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None, header_value_matcher: HVMATCHER_T | None = None, handler_type: HandlerType = HandlerType.PERMANENT, json: Any = UNDEFINED, ) -> RequestHandler: """ Create and register a request handler. If `handler_type` is `HandlerType.PERMANENT` a permanent request handler is created. This handler can be used as many times as the request matches it, but ordered handlers have higher priority so if there's one or more ordered handler registered, those must be used first. If `handler_type` is `HandlerType.ONESHOT` a oneshot request handler is created. This handler can be only used once. Once the server serves a response for this handler, the handler will be dropped. If `handler_type` is `HandlerType.ORDERED` an ordered request handler is created. Comparing to oneshot handler, ordered handler also determines the order of the requests to be served. For example if there are two ordered handlers registered, the first request must hit the first handler, and the second request must hit the second one, and not vice versa. If one or more ordered handler defined, those must be exhausted first. .. note:: Once this method is called, the response should also be specified by calling one of the respond methods of the returned :py:class:`RequestHandler` object, otherwise :py:class:`NoHandlerError` will be raised on an incoming request. :param uri: URI of the request. This must be an absolute path starting with ``/``, a :py:class:`URIPattern` object, or a regular expression compiled by :py:func:`re.compile`. :param method: HTTP method of the request. If not specified (or `METHOD_ALL` specified), all HTTP requests will match. Case insensitive. :param data: payload of the HTTP request. This could be a string (utf-8 encoded by default, see `data_encoding`) or a bytes object. :param data_encoding: the encoding used for data parameter if data is a string. :param headers: dictionary of the headers of the request to be matched :param query_string: the http query string, after ``?``, such as ``username=user``. If string is specified it will be encoded to bytes with the encode method of the string. If dict is specified, it will be matched to the ``key=value`` pairs specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers, or a ``Callable[[str, Optional[str], str], bool]`` receiving the header key (from `headers`), header value (or `None`) and the expected value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param handler_type: type of handler :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. :return: Created and register :py:class:`RequestHandler`. Parameters `json` and `data` are mutually exclusive. """ matcher = self.create_matcher( uri, method=method.upper(), data=data, data_encoding=data_encoding, headers=headers, query_string=query_string, header_value_matcher=header_value_matcher, json=json, ) request_handler = RequestHandler(matcher) if handler_type == HandlerType.PERMANENT: self.handlers.append(request_handler) elif handler_type == HandlerType.ONESHOT: self.oneshot_handlers.append(request_handler) elif handler_type == HandlerType.ORDERED: self.ordered_handlers.append(request_handler) return request_handler def expect_oneshot_request( self, uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, data: str | bytes | None = None, data_encoding: str = "utf-8", headers: Mapping[str, str] | None = None, query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None, header_value_matcher: HVMATCHER_T | None = None, json: Any = UNDEFINED, ) -> RequestHandler: """ Create and register a oneshot request handler. This is a method for convenience. See :py:meth:`expect_request` for documentation. :param uri: URI of the request. This must be an absolute path starting with ``/``, a :py:class:`URIPattern` object, or a regular expression compiled by :py:func:`re.compile`. :param method: HTTP method of the request. If not specified (or `METHOD_ALL` specified), all HTTP requests will match. :param data: payload of the HTTP request. This could be a string (utf-8 encoded by default, see `data_encoding`) or a bytes object. :param data_encoding: the encoding used for data parameter if data is a string. :param headers: dictionary of the headers of the request to be matched :param query_string: the http query string, after ``?``, such as ``username=user``. If string is specified it will be encoded to bytes with the encode method of the string. If dict is specified, it will be matched to the ``key=value`` pairs specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers, or a ``Callable[[str, Optional[str], str], bool]`` receiving the header key (from `headers`), header value (or `None`) and the expected value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. :return: Created and register :py:class:`RequestHandler`. Parameters `json` and `data` are mutually exclusive. """ return self.expect_request( uri=uri, method=method, data=data, data_encoding=data_encoding, headers=headers, query_string=query_string, header_value_matcher=header_value_matcher, handler_type=HandlerType.ONESHOT, json=json, ) def expect_ordered_request( self, uri: str | URIPattern | Pattern[str], method: str = METHOD_ALL, data: str | bytes | None = None, data_encoding: str = "utf-8", headers: Mapping[str, str] | None = None, query_string: None | QueryMatcher | str | bytes | Mapping[str, str] = None, header_value_matcher: HVMATCHER_T | None = None, json: Any = UNDEFINED, ) -> RequestHandler: """ Create and register a ordered request handler. This is a method for convenience. See :py:meth:`expect_request` for documentation. :param uri: URI of the request. This must be an absolute path starting with ``/``, a :py:class:`URIPattern` object, or a regular expression compiled by :py:func:`re.compile`. :param method: HTTP method of the request. If not specified (or `METHOD_ALL` specified), all HTTP requests will match. :param data: payload of the HTTP request. This could be a string (utf-8 encoded by default, see `data_encoding`) or a bytes object. :param data_encoding: the encoding used for data parameter if data is a string. :param headers: dictionary of the headers of the request to be matched :param query_string: the http query string, after ``?``, such as ``username=user``. If string is specified it will be encoded to bytes with the encode method of the string. If dict is specified, it will be matched to the ``key=value`` pairs specified in the request. If multiple values specified for a given key, the first value will be used. If multiple values needed to be handled, use ``MultiDict`` object from werkzeug. :param header_value_matcher: :py:class:`HeaderValueMatcher` that matches values of headers, or a ``Callable[[str, Optional[str], str], bool]`` receiving the header key (from `headers`), header value (or `None`) and the expected value (from `headers`) and should return ``True`` if the header matches, ``False`` otherwise. :param json: a python object (eg. a dict) whose value will be compared to the request body after it is loaded as json. If load fails, this matcher will be failed also. *Content-Type* is not checked. If that's desired, add it to the headers parameter. :return: Created and register :py:class:`RequestHandler`. Parameters `json` and `data` are mutually exclusive. """ return self.expect_request( uri=uri, method=method, data=data, data_encoding=data_encoding, headers=headers, query_string=query_string, header_value_matcher=header_value_matcher, handler_type=HandlerType.ORDERED, json=json, ) def format_matchers(self) -> str: """ Return a string representation of the matchers This method returns a human-readable string representation of the matchers registered. You can observe which requests will be served, etc. This method is primarily used when reporting errors. """ def format_handlers(handlers: list[RequestHandler]): if handlers: return [" {!r}".format(handler.matcher) for handler in handlers] else: return [" none"] lines: list[str] = [] lines.append("Ordered matchers:") lines.extend(format_handlers(self.ordered_handlers)) lines.append("") lines.append("Oneshot matchers:") lines.extend(format_handlers(self.oneshot_handlers)) lines.append("") lines.append("Persistent matchers:") lines.extend(format_handlers(self.handlers)) return "\n".join(lines) def respond_nohandler(self, request: Request, extra_message: str = ""): """ Add a 'no handler' assertion. This method is called when the server wasn't able to find any handler to serve the request. As the result, there's an assertion added (which can be raised by :py:meth:`check_assertions`). """ if self._waiting_settings.stop_on_nohandler: self._set_waiting_result(value=False) return super().respond_nohandler(request, self.format_matchers() + extra_message) def respond_permanent_failure(self): """ Add a 'permanent failure' assertion. This assertion means that no further requests will be handled. This is the resuld of missing an ordered matcher. """ self.add_assertion("All requests will be permanently failed due failed ordered handler") return Response("No handler found for this request", 500) def dispatch(self, request: Request) -> Response: """ Dispatch a request to the appropriate request handler. This method tries to find the request handler whose matcher matches the request, and then calls it in order to serve the request. First, the request is checked for the ordered matchers. If there's an ordered matcher, it must match the request, otherwise the server will be put into a `permanent failure` mode in which it makes all request failed - this is the intended way of working of ordered matchers. Then oneshot handlers, and the permanent handlers are looked up. :param request: the request object from the werkzeug library :return: the response object what the handler responded, or a response which contains the error """ if self.permanently_failed: return self.respond_permanent_failure() handler = None if self.ordered_handlers: handler = self.ordered_handlers[0] if not handler.matcher.match(request): self.permanently_failed = True response = self.respond_nohandler(request) return response self.ordered_handlers.pop(0) self._update_waiting_result() if not handler: handler = self.oneshot_handlers.match(request) if handler: self.oneshot_handlers.remove(handler) self._update_waiting_result() else: handler = self.handlers.match(request) if not handler: return self.respond_nohandler(request) try: response = handler.respond(request) except Error: # don't collect package-internal errors raise except AssertionError as e: self.add_assertion(e) raise except Exception as e: self.handler_errors.append(e) raise if response is None: response = Response("") if isinstance(response, str): response = Response(response) return response def _set_waiting_result(self, value: bool) -> None: # noqa: FBT001 """Set waiting_result Setting is implemented as putting value to queue without waiting. If queue is full we simply ignore the exception, because that means that waiting_result was already set, but not read. """ with suppress(queue.Full): self._waiting_result.put_nowait(value) def _update_waiting_result(self) -> None: if not self.oneshot_handlers and not self.ordered_handlers: self._set_waiting_result(value=True) @contextmanager def wait( self, raise_assertions: bool | None = None, stop_on_nohandler: bool | None = None, timeout: float | None = None, ): """Context manager to wait until the first of following event occurs: all ordered and oneshot handlers were executed, unexpected request was received (if `stop_on_nohandler` is set to `True`), or time was out :param raise_assertions: whether raise assertions on unexpected request or timeout or not :param stop_on_nohandler: whether stop on unexpected request or not :param timeout: time (in seconds) until time is out Example: .. code-block:: python def test_wait(httpserver): httpserver.expect_oneshot_request("/").respond_with_data("OK") with httpserver.wait( raise_assertions=False, stop_on_nohandler=False, timeout=1 ) as waiting: requests.get(httpserver.url_for("/")) # `waiting` is :py:class:`Waiting` assert waiting.result print("Elapsed time: {} sec".format(waiting.elapsed_time)) """ if raise_assertions is None: self._waiting_settings.raise_assertions = self.default_waiting_settings.raise_assertions else: self._waiting_settings.raise_assertions = raise_assertions if stop_on_nohandler is None: self._waiting_settings.stop_on_nohandler = self.default_waiting_settings.stop_on_nohandler else: self._waiting_settings.stop_on_nohandler = stop_on_nohandler if timeout is None: self._waiting_settings.timeout = self.default_waiting_settings.timeout else: self._waiting_settings.timeout = timeout # Ensure that waiting_result is empty with suppress(queue.Empty): self._waiting_result.get_nowait() waiting = Waiting() yield waiting try: waiting_result = self._waiting_result.get(timeout=self._waiting_settings.timeout) waiting.complete(result=waiting_result) except queue.Empty: waiting.complete(result=False) if self._waiting_settings.raise_assertions: raise AssertionError( "Wait timeout occurred, but some handlers left:\n{}".format(self.format_matchers()) ) if self._waiting_settings.raise_assertions and not waiting.result: self.check_assertions() def iter_matching_requests(self, matcher: RequestMatcher) -> Iterable[tuple[Request, Response]]: """ Queries log for matching requests. :param matcher: the matcher object to match requests :return: an iterator with request-response pair from the log """ for request, response in self.log: if matcher.match(request): yield (request, response) def get_matching_requests_count(self, matcher: RequestMatcher) -> int: """ Queries the log for matching requests, returning the number of log entries matching for the specified matcher. :param matcher: the matcher object to match requests :return: the number of log entries matching """ return len(list(self.iter_matching_requests(matcher))) def assert_request_made(self, matcher: RequestMatcher, *, count: int = 1): """ Check the amount of log entries matching for the matcher specified. By default it verifies that exactly one request matching for the matcher specified. The expected count can be customized with the count kwarg (including zero, which asserts that no requests made for the given matcher). :param matcher: the matcher object to match requests :param count: the expected number of matches in the log :return: ``None`` if the assert succeeded, raises :py:class:`AssertionError` if not. """ matching_count = self.get_matching_requests_count(matcher) if matching_count != count: similar_requests: list[Request] = [] for request, _ in self.log: if request.path == matcher.uri: similar_requests.append(request) assert_msg_lines = [ f"Matching request found {matching_count} times but expected {count} times.", f"Expected request: {matcher}", ] if similar_requests: assert_msg_lines.append(f"Found {len(similar_requests)} similar request(s):") for request in similar_requests: assert_msg_lines.extend( ( "--- Similar Request Start", f"Path: {request.path}", f"Method: {request.method}", f"Body: {request.get_data()!r}", f"Headers: {request.headers}", f"Query String: {request.query_string.decode('utf-8')!r}", "--- Similar Request End", ) ) else: assert_msg_lines.append("No similar requests found.") assert_msg = "\n".join(assert_msg_lines) + "\n" assert matching_count == count, assert_msg pytest-httpserver-1.1.2/pytest_httpserver/py.typed000066400000000000000000000000001475715145300225660ustar00rootroot00000000000000pytest-httpserver-1.1.2/pytest_httpserver/pytest_plugin.py000066400000000000000000000044151475715145300243650ustar00rootroot00000000000000import os import pytest from .httpserver import HTTPServer class Plugin: SERVER = None class PluginHTTPServer(HTTPServer): def start(self): super().start() Plugin.SERVER = self def stop(self): super().stop() Plugin.SERVER = None def get_httpserver_listen_address(): listen_host = os.environ.get("PYTEST_HTTPSERVER_HOST") listen_port = os.environ.get("PYTEST_HTTPSERVER_PORT") if listen_port: listen_port = int(listen_port) return listen_host, listen_port @pytest.fixture(scope="session") def httpserver_listen_address(): return get_httpserver_listen_address() @pytest.fixture(scope="session") def httpserver_ssl_context(): return None @pytest.fixture(scope="session") def make_httpserver(httpserver_listen_address, httpserver_ssl_context): host, port = httpserver_listen_address if not host: host = HTTPServer.DEFAULT_LISTEN_HOST if not port: port = HTTPServer.DEFAULT_LISTEN_PORT server = HTTPServer(host=host, port=port, ssl_context=httpserver_ssl_context) server.start() yield server server.clear() if server.is_running(): server.stop() def pytest_sessionfinish(session, exitstatus): # noqa: ARG001 if Plugin.SERVER is not None: Plugin.SERVER.clear() if Plugin.SERVER.is_running(): Plugin.SERVER.stop() @pytest.fixture def httpserver(make_httpserver): server = make_httpserver server.clear() return server @pytest.fixture(scope="session") def make_httpserver_ipv4(httpserver_ssl_context): server = HTTPServer(host="127.0.0.1", port=0, ssl_context=httpserver_ssl_context) server.start() yield server server.clear() if server.is_running(): server.stop() @pytest.fixture def httpserver_ipv4(make_httpserver_ipv4): server = make_httpserver_ipv4 server.clear() return server @pytest.fixture(scope="session") def make_httpserver_ipv6(httpserver_ssl_context): server = HTTPServer(host="::1", port=0, ssl_context=httpserver_ssl_context) server.start() yield server server.clear() if server.is_running(): server.stop() @pytest.fixture def httpserver_ipv6(make_httpserver_ipv6): server = make_httpserver_ipv6 server.clear() return server pytest-httpserver-1.1.2/release_checklist.md000066400000000000000000000012721475715145300212600ustar00rootroot00000000000000 Sounds like a python script for me, but here's the manual checklist: - [ ] check the latest commits on master - [ ] every major change reflected in the release notes - [ ] CHANGES.rst is generated by `make changes`: check formatting - [ ] check the latest doc build at rtd.org - [ ] check that master is green at travis - [ ] version bump (sphinx/conf.py, setup.py) - [ ] tag the HEAD - [ ] generate documentation, check version at release notes - [ ] build the dist: setup.py sdist bdist_wheel - [ ] install package in a local venv - [ ] run the examples in this venv - [ ] CHANGES.rst is generated by `make changes`: commit - [ ] push to github (version bump, tags) - [ ] upload dist/* to pypi pytest-httpserver-1.1.2/releasenotes/000077500000000000000000000000001475715145300177545ustar00rootroot00000000000000pytest-httpserver-1.1.2/releasenotes/notes/000077500000000000000000000000001475715145300211045ustar00rootroot00000000000000pytest-httpserver-1.1.2/releasenotes/notes/additional-type-hints-improvements-b595896ca71b3537.yaml000066400000000000000000000003051475715145300330170ustar00rootroot00000000000000--- features: - | Additional type hints improvements to make the library more mypy compliant. Imports in `__init__.py` have been updated to indicate that this is a namespace package. pytest-httpserver-1.1.2/releasenotes/notes/blocking-httpserver-3b58e2a8464b4d97.yaml000066400000000000000000000003111475715145300301260ustar00rootroot00000000000000--- features: - | Add a new way of running tests with the blocking mode. In this mode, the http server is synchronized to the main thread and the client code is run in a separate thread. pytest-httpserver-1.1.2/releasenotes/notes/build-minor-fixes-32955967c5948adc.yaml000066400000000000000000000001521475715145300274230ustar00rootroot00000000000000prelude: > Minor fixes in setup.py and build environment. No actual code change in library .py files. pytest-httpserver-1.1.2/releasenotes/notes/deprecation-warnings-for-1.0.0-b2b3909e5ad97594.yaml000066400000000000000000000002531475715145300315020ustar00rootroot00000000000000--- deprecations: - | Deprecation warnings were added to prepare changes to 1.0.0. More details: https://pytest-httpserver.readthedocs.io/en/latest/upgrade.html pytest-httpserver-1.1.2/releasenotes/notes/environment-variables-8717eb8f545a8d3d.yaml000066400000000000000000000004051475715145300305340ustar00rootroot00000000000000--- features: - | When using pytest plugin, specifying the bind address and bind port can also be possible via environment variables. Setting PYTEST_HTTPSERVER_HOST and PYTEST_HTTPSERVER_PORT will change the bind host and bind port, respectively. pytest-httpserver-1.1.2/releasenotes/notes/ephemeral-port-support-adea94cb8f6088c6.yaml000066400000000000000000000007341475715145300310210ustar00rootroot00000000000000--- features: - | Support ephemeral port. This can be used by specify 0 as the port number to the HTTPServer instance. In such case, an unused port will be picked up and the server will start listening on that port. Querying the port attribute after server start reveals the real port where the server is actually listening. upgrade: - | The default port has been changed to 0, which results that the server will be staring on an ephemeral port. pytest-httpserver-1.1.2/releasenotes/notes/fix-16-query-string-c989bee73733a325.yaml000066400000000000000000000003071475715145300276210ustar00rootroot00000000000000--- fixes: - | Fixed issue \#16 by converting string object passed as query_string to bytes which is the type of the query string in werkzeug, and also allowing bytes as the parameter. pytest-httpserver-1.1.2/releasenotes/notes/fix-log-leak-ee94a826d75173f1.yaml000066400000000000000000000003361475715145300264200ustar00rootroot00000000000000--- fixes: - | Fixed an issue related to the leak of httpserver state between the tests when httpserver is destructed before the other fixtures. `#352 `_ pytest-httpserver-1.1.2/releasenotes/notes/fix-packaging-include-673c0b51ebdcd9a2.yaml000066400000000000000000000001761475715145300304730ustar00rootroot00000000000000--- fixes: - | Packaging of sdist and the wheel fixed by adding the extra files only to the sdist and not to the wheel. pytest-httpserver-1.1.2/releasenotes/notes/fix-test_verify_assert_msg-af37678f187bb8da.yaml000066400000000000000000000003171475715145300316610ustar00rootroot00000000000000--- fixes: - | Fix pytest-httpserver's own tests related to log querying. No functional changes in pytest-httpserver code itself. `#345 `_ pytest-httpserver-1.1.2/releasenotes/notes/fix-x509-test-assets-8a1b220d085a504a.yaml000066400000000000000000000002571475715145300276530ustar00rootroot00000000000000--- fixes: - | Fix the tests assets created for SSL/TLS tests by extending their expiration time. Also update the Makefile which can be used to update these assets. pytest-httpserver-1.1.2/releasenotes/notes/flake8-version-update-d70b88d9a15a723f.yaml000066400000000000000000000002301475715145300303210ustar00rootroot00000000000000other: - | Version of flake8 library updated to require 4.0.0+ at minimum. This is required to make flake8 working on recent python versions. pytest-httpserver-1.1.2/releasenotes/notes/header-value-matcher-5b32c6640aef71d8.yaml000066400000000000000000000003601475715145300301660ustar00rootroot00000000000000--- features: - | Make it possible to intelligently compare headers. To accomplish that HeaderValueMatcher was added. It already contains logic to compare unknown headers and authorization headers. Patch by Roman Inflianskas. pytest-httpserver-1.1.2/releasenotes/notes/headervaluematcher-type-hinting-fix-f19e7a4d48b0a1d8.yaml000066400000000000000000000004621475715145300333220ustar00rootroot00000000000000fixes: - | Type hinting for header_value_matcher has been fixed. From now, specifying a callable as ``Callable[[str, Optional[str], str], bool]`` will be accepted also. Providing a ``HeaderValueMatcher`` object will be also accepted as before, as it provides the same callable signature. pytest-httpserver-1.1.2/releasenotes/notes/hooks-306915ded3b2771f.yaml000066400000000000000000000000421475715145300252450ustar00rootroot00000000000000--- features: - | Hooks API pytest-httpserver-1.1.2/releasenotes/notes/http-methods-are-case-insensitive-c2a1d49f9809f263.yaml000066400000000000000000000002031475715145300325650ustar00rootroot00000000000000--- features: - | HTTP methods are case insensitive. The HTTP method specified is converted to uppercase in the library. pytest-httpserver-1.1.2/releasenotes/notes/httpserver-listen-address-fixture-87f18b2cdbf47532.yaml000066400000000000000000000003121475715145300330200ustar00rootroot00000000000000--- features: - | Add httpserver_listen_address fixture which is used to set up the bind address and port of the server. Setting bind address and port is possible by overriding this fixture. improved-error-reporting-in-custom-request-handlers-df479afb8eae03d1.yaml000066400000000000000000000007401475715145300365340ustar00rootroot00000000000000pytest-httpserver-1.1.2/releasenotes/notes--- features: - | Improved error handling of custom request handlers. Request handlers added with ``respond_with_handler`` now can use the ``assert`` statement. Those errors will be reported when a further ``check_assertions()`` call is made. Also, unhandled exceptions raised in the request handlers can be re-raised by calling the new ``check_handler_errors()`` method. A new method called ``check()`` has been added which calls these two in sequence. pytest-httpserver-1.1.2/releasenotes/notes/initial-ef17767ee807ab89.yaml000066400000000000000000000000411475715145300256550ustar00rootroot00000000000000--- prelude: > First release pytest-httpserver-1.1.2/releasenotes/notes/json-matcher-1d030e3b4f9b66bd.yaml000066400000000000000000000003621475715145300266510ustar00rootroot00000000000000--- features: - | It is now possible to specify a JSON-serializable python value (such as dict, list, etc) and match the request to it as JSON. The request's body is loaded as JSON and it will be compared to the expected value. pytest-httpserver-1.1.2/releasenotes/notes/log-querying-683219f3587d2139.yaml000066400000000000000000000001251475715145300263420ustar00rootroot00000000000000--- features: - | New methods added to query for matching requests in the log. pytest-httpserver-1.1.2/releasenotes/notes/minor-fixes-496615d00d2b3e44.yaml000066400000000000000000000001561475715145300263020ustar00rootroot00000000000000--- prelude: > Minor fixes in setup.py and build environment. No actual code change in library .py files. pytest-httpserver-1.1.2/releasenotes/notes/mypy-fix-for-headervaluematcher-fba16bfa9dc3e0e4.yaml000066400000000000000000000002621475715145300326740ustar00rootroot00000000000000--- fixes: - | Fixed type hinting of ``HeaderValueMatcher.DEFAULT_MATCHERS``, which did not allow modifications, however it is explicitly allowed in the documentation. pytest-httpserver-1.1.2/releasenotes/notes/new-expect-method-4f8d071c78c9884b.yaml000066400000000000000000000002321475715145300275010ustar00rootroot00000000000000--- features: - | Add a new ``expect`` method to the ``HTTPServer`` object which allows developers to provide their own request matcher object. pytest-httpserver-1.1.2/releasenotes/notes/new-fixture-for-httpserver-making-98afa235d3283831.yaml000066400000000000000000000012211475715145300325550ustar00rootroot00000000000000--- features: - | Added a new session scoped fixture ``make_httpserver`` which creates the object for the ``httpserver`` fixture. It can be overridden to add further customizations and it must yield a ``HTTPServer`` object - see ``pytest_plugin.py`` for an implementation. As this fixture is session scoped, it will be called only once when the first test using httpserver is started. This addition also deprecates the use of ``PluginHTTPServer`` which was used in earlier versions as a way to customize server object creation. ``PluginHTTPServer`` can still be used but it may be subject to deprecation in the future. pytest-httpserver-1.1.2/releasenotes/notes/new-fixture-for-ssl-context-054da072a46e9e62.yaml000066400000000000000000000005171475715145300314470ustar00rootroot00000000000000--- features: - | Added a new session scoped fixture ``httpserver_ssl_context`` which provides the SSL context for the server. By default it returns ``None``, so SSL is not enabled, but can be overridden to return a valid ``ssl.SSLContext`` object which will be used for SSL connections. See test_ssl.py for example. pytest-httpserver-1.1.2/releasenotes/notes/no-handler-status-code-380fa02ebe9b2721.yaml000066400000000000000000000002161475715145300304530ustar00rootroot00000000000000--- features: - | The http response code sent when no handler is found for the request can be changed. It is set to 500 by default. pytest-httpserver-1.1.2/releasenotes/notes/nohandler-response-details-2af020f4763fcea8.yaml000066400000000000000000000002361475715145300315130ustar00rootroot00000000000000--- features: - | When there's no handler for the request, add more details to the response sent by the server about the request to help debugging. pytest-httpserver-1.1.2/releasenotes/notes/poetry-5c16af5ed108ba9c.yaml000066400000000000000000000005671475715145300256730ustar00rootroot00000000000000--- other: - | Package deployment and CI has been migrated to poetry. poetry.lock will be kept up to date. Make target "quick-test" renamed to "test". Also, minor adjustments were made regarding documentation generation. Make targets should be identical. Build results like sdist, and wheel are almost identical to the one which was made by setuptools. pytest-httpserver-listen-address-scope-change-0c1aa457e20cae83.yaml000066400000000000000000000005231475715145300351740ustar00rootroot00000000000000pytest-httpserver-1.1.2/releasenotes/notes--- upgrade: - | **Breaking change**: The scope of ``httpserver_listen_address`` fixture changed from **function** to **session**. This is a requirement to implement the other features listed in this release. See the `upgrade guide `_ for the details. pytest-httpserver-1.1.2/releasenotes/notes/python-34-35-deprecation-a4a3b57d1f2875d7.yaml000066400000000000000000000004671475715145300305040ustar00rootroot00000000000000--- deprecations: - | Python 3.4 and 3.5 versions have been deprecated in order to support type hints in the source code. Users using 3.5 and earlier releases encouraged to upgrade to later versions. Please node that 3.5 reached EOL in September of 2020 and no longer receives security fixes. pytest-httpserver-1.1.2/releasenotes/notes/python-37-deprecation-72029b78e91d6b26.yaml000066400000000000000000000004311475715145300301160ustar00rootroot00000000000000--- deprecations: - | Python versions earlier than 3.8 have been deprecated in order to support the latest werkzeug. Users using 3.7 or earlier python may use pytest-httpserver with earlier werkzeug versions but tests are no longer run for these python versions. pytest-httpserver-1.1.2/releasenotes/notes/python-38-deprecation-48b0c8be245f63d1.yaml000066400000000000000000000002521475715145300302470ustar00rootroot00000000000000--- deprecations: - | Python versions earlier than 3.9 have been deprecated in order to make the code more type safe. Python 3.8 has reached EOL on 2024-10-07. pytest-httpserver-1.1.2/releasenotes/notes/python-classifier-fix-bfe43601d16f27d8.yaml000066400000000000000000000001431475715145300304330ustar00rootroot00000000000000--- fixes: - | Python version classifier updated in pyproject.toml (which updates pypi also) pytest-httpserver-1.1.2/releasenotes/notes/query-matcher-751db32b2ac1fc74.yaml000066400000000000000000000005361475715145300270440ustar00rootroot00000000000000--- features: - | Besides bytes and string, dict and MultiDict objects can be specified as query_string. When these objects are used, the query string gets parsed into a dict (or MultiDict), and comparison is made accordingly. This enables the developer to ignore the order of the keys in the query_string when expecting a request. pytest-httpserver-1.1.2/releasenotes/notes/re-release-107-23f0fd429612b470.yaml000066400000000000000000000001451475715145300263700ustar00rootroot00000000000000--- fixes: - | Version 1.0.7 has been released with incorrect dependencies. This is fixed now. pytest-httpserver-1.1.2/releasenotes/notes/release-tag-fix-8b2dfc26a24598c3.yaml000066400000000000000000000001521475715145300271640ustar00rootroot00000000000000--- fixes: - | Fix release tagging. 0.3.2 was released in a mistake by tagging 3.0.2 to the branch. pytest-httpserver-1.1.2/releasenotes/notes/requesthandler-repr-09f342f19f6250bc.yaml000066400000000000000000000002651475715145300301240ustar00rootroot00000000000000--- features: - | Add ``__repr__`` to ``RequestHandler`` object so when it is compared (eg. with the ``log`` attribute of the server) it will show the matcher parameters. pytest-httpserver-1.1.2/releasenotes/notes/same-as-1.0.0rc1-6356c8b1c488e3cd.yaml000066400000000000000000000002071475715145300266020ustar00rootroot00000000000000--- prelude: > Functionally the same as 1.0.0rc1. For the list of changes between 0.3.8 and 1.0.0 see the changelist for 1.0.0rc1. pytest-httpserver-1.1.2/releasenotes/notes/sdist-new-files-d99db1317673be9c.yaml000066400000000000000000000002011475715145300272260ustar00rootroot00000000000000--- other: - | Add more files to source distribution (sdist). It now contains tests, assets, examples and other files. pytest-httpserver-1.1.2/releasenotes/notes/setup-py-remove-pytest-runner-dd60d3f20ed45f1c.yaml000066400000000000000000000002471475715145300322570ustar00rootroot00000000000000--- other: - | Removed pytest-runner from setup.py as it is deprecated and makes packaging inconvenient as it needs to be installed before running setup.py. pytest-httpserver-1.1.2/releasenotes/notes/ssl-support-13321dd9d636af34.yaml000066400000000000000000000002421475715145300264320ustar00rootroot00000000000000--- features: - | SSL/TLS support added with using the SSL/TLS support provided by werkzeug. This is based on the ssl module from the standard library. pytest-httpserver-1.1.2/releasenotes/notes/threading-support-28c89686025e2184.yaml000066400000000000000000000001101475715145300273710ustar00rootroot00000000000000--- features: - | Threading support to serve requests in parallel pytest-httpserver-1.1.2/releasenotes/notes/type-hints-improvement-02e0efd620644440.yaml000066400000000000000000000002331475715145300304760ustar00rootroot00000000000000--- features: - | Type hints updated to conform to 'mypy' type checking tool. Also, py.typed file is added as package data according to PEP 561. pytest-httpserver-1.1.2/releasenotes/notes/unify-expect-request-functions-bd877c586b62a294.yaml000066400000000000000000000012461475715145300322600ustar00rootroot00000000000000--- features: - | Unify request functions of the HTTPServer class to make the API more straightforward to use. upgrade: - | The following methods of HTTPServer have been changed in a backward-incompatible way: * :py:meth:`pytest_httpserver.HTTPServer.expect_request` becomes a general function accepting handler_type parameter so it can create any kind of request handlers * :py:meth:`pytest_httpserver.HTTPServer.expect_oneshot_request` no longer accepts the ordered parameter, and it creates an unordered oneshot request handler * :py:meth:`pytest_httpserver.HTTPServer.expect_ordered_request` is a new method creating an ordered request handler pytest-httpserver-1.1.2/releasenotes/notes/uri-matching-dba6660cb0689402.yaml000066400000000000000000000004301475715145300265040ustar00rootroot00000000000000--- features: - | Extend URI matching by allowing to specify URIPattern object or a compiled regular expression, which will be matched against the URI. URIPattern class is defined as abstract in the library so the user need to implement a new class based on it. pytest-httpserver-1.1.2/releasenotes/notes/use-ruff-for-linting-a0f446e9df39c719.yaml000066400000000000000000000002231475715145300302010ustar00rootroot00000000000000--- other: - | Use ruff for linting. It includes some source code changes which should not introduce functional changes, or API changes. pytest-httpserver-1.1.2/releasenotes/notes/werkzeug-header-type-follow-up-74a80dd03e6ca6db.yaml000066400000000000000000000002461475715145300323330ustar00rootroot00000000000000--- upgrade: - With werkzeug 2.3.x the headers type has been updated to not allow integers as header values. This restriction followed up in pytest-httpserver. werkzeug-parse_authorization_header-deprecation-fix-8264966b70fddc6d.yaml000066400000000000000000000004101475715145300364600ustar00rootroot00000000000000pytest-httpserver-1.1.2/releasenotes/notes--- fixes: - | Fix Werkzeug deprecation warning about ``parse_authorization_header`` call. Replace ``parse_authorization_header`` with ``Authorization.from_header`` as suggested. This fix should not introduce any functional change for the users. werkzeug-urls-url_decode-deprecation-fix-56fe3c183b53f83b.yaml000066400000000000000000000003721475715145300341450ustar00rootroot00000000000000pytest-httpserver-1.1.2/releasenotes/notes--- fixes: - | Fix Werkzeug deprecation warning about ``werkzeug.urls.url_decode`` call. This call has been changed to ``urllib.parse.parse_qsl`` in the implementation. This fix should not introduce any functional change for the users. pytest-httpserver-1.1.2/scripts/000077500000000000000000000000001475715145300167525ustar00rootroot00000000000000pytest-httpserver-1.1.2/scripts/release.py000077500000000000000000000045101475715145300207470ustar00rootroot00000000000000#!/usr/bin/env python3 import argparse import subprocess import sys from collections.abc import Iterable from pathlib import Path from shutil import which class UsageError(Exception): pass def parse_version() -> str: output = subprocess.check_output(["poetry", "version", "--short"], encoding="utf-8") version = output.strip() return version def bump_version(path: Path, prefix_list: Iterable[str], current_version: str, new_version: str): prefixes = tuple(prefix_list) lines = [] for line in path.open(): if line.startswith(prefixes): line = line.replace(current_version, new_version) lines.append(line) path.write_text("".join(lines)) def git(*args): return subprocess.check_call(["git"] + list(args)) def make(*args): return subprocess.check_call(["make"] + list(args)) def check_changelog(): old_changelog = Path("CHANGES.rst").read_text() make("changes") new_changelog = Path("CHANGES.rst").read_text() if old_changelog == new_changelog: raise UsageError("No new changelog entries") def check_environment(): for binary in ("git", "make", "poetry"): if not which(binary): raise UsageError("No such binary: {}".format(binary)) def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("new_version", help="Version to release") args = parser.parse_args() new_version: str = args.new_version current_version = parse_version() if current_version is None: raise UsageError("Unable to parse version") print(f"Current version: {current_version}") if current_version == new_version: raise UsageError("Current version is the same as new version") check_changelog() bump_version(Path("doc/conf.py"), ["version"], current_version, new_version) subprocess.check_call(["poetry", "version", new_version]) git("add", "pyproject.toml", "doc/conf.py") git("commit", "-m", "Version bump to {}".format(new_version)) git("tag", new_version) make("changes") git("add", "CHANGES.rst") git("commit", "-m", "CHANGES.rst: add release notes for {}".format(new_version)) git("tag", "-f", new_version) if __name__ == "__main__": try: main() except UsageError as err: print(f"ERROR: {err}", file=sys.stderr) sys.exit(1) pytest-httpserver-1.1.2/shell.nix000066400000000000000000000011721475715145300171130ustar00rootroot00000000000000{ pkgs ? import { } }: let unstable = import { config = { allowUnfree = true; }; }; in pkgs.mkShell { buildInputs = with pkgs; [ virtualenv python3Packages.tox python3Packages.poetry-core pre-commit python3Packages.requests python3Packages.sphinx python3Packages.sphinx-rtd-theme reno python3Packages.mypy python3Packages.types-requests python3Packages.pytest python3Packages.pytest-cov python3Packages.coverage python3Packages.ipdb python3Packages.types-toml python3Packages.toml python3Packages.black bashInteractive ]; } pytest-httpserver-1.1.2/tests/000077500000000000000000000000001475715145300164255ustar00rootroot00000000000000pytest-httpserver-1.1.2/tests/assets/000077500000000000000000000000001475715145300177275ustar00rootroot00000000000000pytest-httpserver-1.1.2/tests/assets/Makefile000066400000000000000000000011331475715145300213650ustar00rootroot00000000000000 all: server.crt rootCA.key: openssl genrsa -out rootCA.key 2048 rootCA.crt: rootCA.key rootCA.cnf openssl req -x509 -new -nodes -key rootCA.key -sha256 -days 11083 -out rootCA.crt -config rootCA.cnf server.csr server.key: server.cnf openssl req -new -sha256 -nodes -out server.csr -newkey rsa:2048 -keyout server.key -config server.cnf server.crt: server.csr rootCA.crt rootCA.key v3.ext openssl x509 -req -in server.csr -CA rootCA.crt -CAkey rootCA.key -CAcreateserial -out server.crt -days 11083 -sha256 -extfile v3.ext clean: rm -f rootCA.key rootCA.crt server.csr server.key server.crt pytest-httpserver-1.1.2/tests/assets/README000066400000000000000000000002631475715145300206100ustar00rootroot00000000000000 !!! WARNING !!! This directory contains a certificate and a root CA for testing. Never use these certs in production as these are (including the private keys) public on github. pytest-httpserver-1.1.2/tests/assets/rootCA.cnf000066400000000000000000000003021475715145300216010ustar00rootroot00000000000000[req] default_bits = 4096 prompt = no default_md = sha256 distinguished_name = dn [dn] C=US ST=SomeState L=SomeCity O=Test CA OU=SomeOrganizationUnit emailAddress=test@example.com CN = Test CA pytest-httpserver-1.1.2/tests/assets/rootCA.crt000066400000000000000000000025061475715145300216330ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIDuTCCAqECFA89mdXXMCnDsJ4Ur/KBUaNZ2AAYMA0GCSqGSIb3DQEBCwUAMIGY MQswCQYDVQQGEwJVUzESMBAGA1UECAwJU29tZVN0YXRlMREwDwYDVQQHDAhTb21l Q2l0eTEQMA4GA1UECgwHVGVzdCBDQTEdMBsGA1UECwwUU29tZU9yZ2FuaXphdGlv blVuaXQxHzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20xEDAOBgNVBAMM B1Rlc3QgQ0EwHhcNMTkwODI4MTk0NzIwWhcNNDkxMjMxMTk0NzIwWjCBmDELMAkG A1UEBhMCVVMxEjAQBgNVBAgMCVNvbWVTdGF0ZTERMA8GA1UEBwwIU29tZUNpdHkx EDAOBgNVBAoMB1Rlc3QgQ0ExHTAbBgNVBAsMFFNvbWVPcmdhbml6YXRpb25Vbml0 MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1wbGUuY29tMRAwDgYDVQQDDAdUZXN0 IENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1B6TAFqG5tNXXH4v uc7lqMc9FUDgjAeAlxH5pb2b5pmKdZoNFE4R5UiJ90Hb1P2kUmWywUIYiQ1QKOtr 8/qPzenkd6Y6myOBtHlW8ktodnfXKzhmxzdNMWqrikHOSiZXxOJfwmNMCOuLd9lo 12NI00ZqiS6XATW7vFZd3fw/IojwoN+RyKFmRMy27d3jtxTSqx37+jVerETmL4zA G01VfVrIm2Mx0ZHq4OHoQfsc2X1UrBjU68evjc2pJ+gJ2GN0NlvQ8lpwD8rguDQf HCF/VjGOsZyxQWHfmYKK5F/UXi/k9GwqNQ/adNV8drWm3OqqFVKNRLAviQNm+507 kNfYIwIDAQABMA0GCSqGSIb3DQEBCwUAA4IBAQAajndlgIF/rjP7aeSZ6h033BVW JEbpb5Ke1Arvj5UypH/hlhpfi0KIeoYR82qdPoDTYoFDp62yGQmkCBDJDQ1mgFhl 8UcVY6zOuMH6J94JgvXneFTAsCFNsLF4g1EeKvLZ7EB+339AFXI+1jRrJHnEJD7+ CNiLYdLTqpGHie3AAPf+9ImJsvyhOL3eWq5Z9t+/5rrF2bqoy9sJTYo+pVstLbVU QGoA+PVTlOGI8N2KmhsYU+tKKpKgItUttTPqzzdZY8nw33BYfkHRp2tSIz8XHa+F wAVAzi5DRKKMWWROfHOy5WwEOwksIGul7z9/RE1tGonpxRTp6CCBy5hl4qs8 -----END CERTIFICATE----- pytest-httpserver-1.1.2/tests/assets/rootCA.key000066400000000000000000000032171475715145300216330ustar00rootroot00000000000000-----BEGIN RSA PRIVATE KEY----- MIIEpQIBAAKCAQEA1B6TAFqG5tNXXH4vuc7lqMc9FUDgjAeAlxH5pb2b5pmKdZoN FE4R5UiJ90Hb1P2kUmWywUIYiQ1QKOtr8/qPzenkd6Y6myOBtHlW8ktodnfXKzhm xzdNMWqrikHOSiZXxOJfwmNMCOuLd9lo12NI00ZqiS6XATW7vFZd3fw/IojwoN+R yKFmRMy27d3jtxTSqx37+jVerETmL4zAG01VfVrIm2Mx0ZHq4OHoQfsc2X1UrBjU 68evjc2pJ+gJ2GN0NlvQ8lpwD8rguDQfHCF/VjGOsZyxQWHfmYKK5F/UXi/k9Gwq NQ/adNV8drWm3OqqFVKNRLAviQNm+507kNfYIwIDAQABAoIBAQCEbU4TE3FTHDBX Q0RieUOx2ilNYoKNiYWDSyqTicuR7NufTfzKZ28OuPnBTvGCiJOVCX7O3ofY0GHy Gf/8grpSsKn89N9zyXgJhxN8Ew31oB3KEt0/lEvyBQlxOGIksslq1nU4M6vdicg5 m5azmX12wWhzmo2VqMk67KSPmRKFaNu20WJcIa/cNA3fJGkkiLmZBDmsrcVMH62s FfqpHrMr6BXQl90RDSIi6UoVzZ8u8kjZOczz8SJx23borr7UGlKJX2cRx37KJ6w3 r3NScu+FPKtjY7WbiS7coA/105BF8kvhXWrMBC9IrdRRvze/pBoo2/09vbRrBAXR /fFRaRLJAoGBAPKBOM4OHZ2gOtkdFhUEAvQmPIf+Ule+SpUYMUmvKF3Umyu+DJpW F6tH7zP28yY/zIhfEHXq+O1YHe9u8QFQ/GOiT6sf3O4mW/QJvTvP3DlilVjdj9+r IBwX06rwZQ0uVrPsYh+egOtfBjeHhZ+Ikl9W/n55jILMYAWiHOBBTKCFAoGBAN/s edBm2pREaijtl3VyDjITMcMQrpA2W4MnaiL0gr5eEJjjIlYhO79RmSfpzRYQB7t6 r19JTJtix8DavEjJt9/iCi4pWDtRVpulgBkTjoKZMOA9e+pwDcEK5ZZneNJ6NTGV iVyJtozybSLv517mzOvdTiHEG2Imgg0Jw+kDogqHAoGBANzPYQPuwnS2Yx8yZtr3 7iCVeGRz5FJUyOB9SNPJE75sSmZIBH27io3BUENGxxu61+gMd5aHP+YNaCSOJhCG x2mJb3Vn6+lFMFFDVPVTTTlLVKW4CLsmvHQYFfn+LmUUHopx4N7dmpG20phZZAhh eLYrJkvPLWwj4AMBG73ud3FpAoGBAK3DK6P0rJlxnY+1D1sr7qgdDPh58XwnMkxM QahuZSakh+ycFQEROPP1tguq+mKsfdOWGJCwqKnLtYaKNqGlJvKszYmUu8sMC1Es 1IKhEm11wt+/1nDOE15BvndARBnQi4a2q6kLlIU7ekUqNTkHkO1XBlJdg4Jer3y+ nzAqiYvxAoGABzVsHeI9Hlm2pcMbMIRHMXHXXJpl6kmB6OI4T+FFfbvR1yy16i9A wTwes3Dqjtoxz+ykdJAh8w1MF2jCfC0jDb3TxMJqs04HwvelBWe2rN9s5akdh1Ft 8YbziRMOfxFZ9ab8upBHYdT7Q9OSmv7qJfkc8YcpKjjOh+vzqbD2ajI= -----END RSA PRIVATE KEY----- pytest-httpserver-1.1.2/tests/assets/rootCA.srl000066400000000000000000000000211475715145300216310ustar00rootroot00000000000000D647D966F3C4188D pytest-httpserver-1.1.2/tests/assets/server.cnf000066400000000000000000000003061475715145300217240ustar00rootroot00000000000000[req] default_bits = 4096 prompt = no default_md = sha256 distinguished_name = dn [dn] C=US ST=SomeState L=SomeCity O=Test cert OU=SomeOrganizationUnit emailAddress=test@example.com CN = localhost pytest-httpserver-1.1.2/tests/assets/server.crt000066400000000000000000000032301475715145300217450ustar00rootroot00000000000000-----BEGIN CERTIFICATE----- MIIEsDCCA5igAwIBAgIJANZH2WbzxBiNMA0GCSqGSIb3DQEBCwUAMIGYMQswCQYD VQQGEwJVUzESMBAGA1UECAwJU29tZVN0YXRlMREwDwYDVQQHDAhTb21lQ2l0eTEQ MA4GA1UECgwHVGVzdCBDQTEdMBsGA1UECwwUU29tZU9yZ2FuaXphdGlvblVuaXQx HzAdBgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20xEDAOBgNVBAMMB1Rlc3Qg Q0EwHhcNMTkwODI4MTk0NzIwWhcNNDkxMjMxMTk0NzIwWjCBnDELMAkGA1UEBhMC VVMxEjAQBgNVBAgMCVNvbWVTdGF0ZTERMA8GA1UEBwwIU29tZUNpdHkxEjAQBgNV BAoMCVRlc3QgY2VydDEdMBsGA1UECwwUU29tZU9yZ2FuaXphdGlvblVuaXQxHzAd BgkqhkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20xEjAQBgNVBAMMCWxvY2FsaG9z dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMtWmnFZdWPRN0v2bQSW /Plmi/qO9VQWcc+5g1Bmpm/KlfjBqB0NcNWXi8bxIPlDyv6B+1OsrcfCAnUKkQsT HHYFsnyJByC9Hz7OGQdrdPYsyugvRbTA/JTgR9xr9w7xENYNV+ZKg+bEe8b/MNor xuteuM3EklTze0V3RrDDuq33Vuunkqaz5varResyMfVGLxDcvqcRi2OkysdaKoc8 z3DmvkZggwe1IarbWXVHmAGT45BAK9dM3/UPTFQyjL88igZtwVEU37x9x7RV8zSn 5MO2KeRLDfVthyxfYQjSk/D+CxM3v0L/SXJS3/ZJv0zd3/GdHJhFRcatEnRsIeAM g9cCAwEAAaOB9jCB8zCBwgYDVR0jBIG6MIG3oYGepIGbMIGYMQswCQYDVQQGEwJV UzESMBAGA1UECAwJU29tZVN0YXRlMREwDwYDVQQHDAhTb21lQ2l0eTEQMA4GA1UE CgwHVGVzdCBDQTEdMBsGA1UECwwUU29tZU9yZ2FuaXphdGlvblVuaXQxHzAdBgkq hkiG9w0BCQEWEHRlc3RAZXhhbXBsZS5jb20xEDAOBgNVBAMMB1Rlc3QgQ0GCFA89 mdXXMCnDsJ4Ur/KBUaNZ2AAYMAkGA1UdEwQCMAAwCwYDVR0PBAQDAgTwMBQGA1Ud EQQNMAuCCWxvY2FsaG9zdDANBgkqhkiG9w0BAQsFAAOCAQEAx5LINgzj+89wkjy5 5TpzgV6rcgAK8av1NLXztWwnbGtq9WDYc/3o1HIkEuHY6kkfsqhby3Vg1NKUS1MZ WiB1vvT4TyCwz0FoldVXRbIevUVtitFNigbfOOWQ3F/+pK7gfHM+psLDzvh2qAxO 6zUSfmCr3QFUjs7Bc0o5qMyaLL/SUdCps534yWoRemLmo89Pf9OgdYsbNWKhSRM2 uq5rPmLkIQMWbAJFGc4KejusBswzDNP/+yaLnnhluI90j07CZBfOiMnQcSzLVYrd LzJ+YJKL3ZuwaK1Yg0J5xgaBc3fr3+XoF7rQV4HRfk4kvT46xmrlropBakS4T9g7 YGvslw== -----END CERTIFICATE----- pytest-httpserver-1.1.2/tests/assets/server.csr000066400000000000000000000020661475715145300217520ustar00rootroot00000000000000-----BEGIN CERTIFICATE REQUEST----- MIIC4jCCAcoCAQAwgZwxCzAJBgNVBAYTAlVTMRIwEAYDVQQIDAlTb21lU3RhdGUx ETAPBgNVBAcMCFNvbWVDaXR5MRIwEAYDVQQKDAlUZXN0IGNlcnQxHTAbBgNVBAsM FFNvbWVPcmdhbml6YXRpb25Vbml0MR8wHQYJKoZIhvcNAQkBFhB0ZXN0QGV4YW1w bGUuY29tMRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEBAQUAA4IB DwAwggEKAoIBAQDLVppxWXVj0TdL9m0Elvz5Zov6jvVUFnHPuYNQZqZvypX4wagd DXDVl4vG8SD5Q8r+gftTrK3HwgJ1CpELExx2BbJ8iQcgvR8+zhkHa3T2LMroL0W0 wPyU4Efca/cO8RDWDVfmSoPmxHvG/zDaK8brXrjNxJJU83tFd0aww7qt91brp5Km s+b2q0XrMjH1Ri8Q3L6nEYtjpMrHWiqHPM9w5r5GYIMHtSGq21l1R5gBk+OQQCvX TN/1D0xUMoy/PIoGbcFRFN+8fce0VfM0p+TDtinkSw31bYcsX2EI0pPw/gsTN79C /0lyUt/2Sb9M3d/xnRyYRUXGrRJ0bCHgDIPXAgMBAAGgADANBgkqhkiG9w0BAQsF AAOCAQEAGxaH8686Uqlb6puKCf51Kk3Fabut1iNq0e0+zrHZ+Nkyk8GGfF+u/2s+ 8Ga8uxvdWHwhANvTWdnHhM6F+4qv12PUVOIQN+phZubR+chsbqR4OvIFDLyZ4Ot5 +i+H5jUR25omV01n7l0HBDyK7aKaMB9upChzqsKJ9rN/sI8k253PAESbXkS3CtFv qx9yNYetQqZ8fA+pbwt0qHiyC0I8Nm1aJ20qjHXev9guB+hlHNxPJB3n8WzBlml1 +03fsuimckEVMiDVY4DSUAl2x09SWIwKdK9mXvUEyY7njf1iYzgY7NUMqlm8eHEh hwXfn88GVfDjLCM/4SaZYDHYyMIUPg== -----END CERTIFICATE REQUEST----- pytest-httpserver-1.1.2/tests/assets/server.key000066400000000000000000000032501475715145300217470ustar00rootroot00000000000000-----BEGIN PRIVATE KEY----- MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDLVppxWXVj0TdL 9m0Elvz5Zov6jvVUFnHPuYNQZqZvypX4wagdDXDVl4vG8SD5Q8r+gftTrK3HwgJ1 CpELExx2BbJ8iQcgvR8+zhkHa3T2LMroL0W0wPyU4Efca/cO8RDWDVfmSoPmxHvG /zDaK8brXrjNxJJU83tFd0aww7qt91brp5Kms+b2q0XrMjH1Ri8Q3L6nEYtjpMrH WiqHPM9w5r5GYIMHtSGq21l1R5gBk+OQQCvXTN/1D0xUMoy/PIoGbcFRFN+8fce0 VfM0p+TDtinkSw31bYcsX2EI0pPw/gsTN79C/0lyUt/2Sb9M3d/xnRyYRUXGrRJ0 bCHgDIPXAgMBAAECggEBAILvVGoy+dV5zkq69v5in6mVcAO69JzeCxGA2t72Cqyn 3iSdxzxWAFd5y4cilGIOVnPGjWkXI5wjAOQPFtDz0HrrNkDdF9rsDWeq3REbD+jJ dStH7XH0Z3ESbxOv7hoP/xBm7TgcuYzq7u14PCPw1pmPmB7gOn47fyB/KuT+VIJa QYVi7kHW4dYkV1nqgZ8Xak4HSqJ8EAWbqcIWYs0tZ70BrACIPZPrUxEIK1Eq1o9M yJFdcCru1tNDaG49JAiuQOAsLAg9bKFStoJPjgdPf+3E4A4vvOWe+PkPEWZe9X9D pJSSg+uZ2UXEpqBSpqP3SriZXYPt0DmEKTBWxzhnEEkCgYEA86ZTy3GXcIVttWKe 1dixY2os54aeUJb63GyoqgzDAXjW1sX5sa5D9yr/y64Sv2ouEFOotSSu9UAImpuE l67o1kfDKWm4gArr8pc5kHxkw/Jex/6/LEYwYMAvvYz4KI4pBAeSVJrpzAyd/H34 Wuqx42QJtQBKK7X5FifBgYdk6ZUCgYEA1aUuxsWn6iMLxrcdzjtINpuYH56Jvo86 JyXKApCXiRpYiPJE2KoM52F6BOwnngiRG7unhhRRs9Tj+R3ieIUCOsamM0AxDLvU 46wm0tFqYyHDIaYq/dqTnsnAQqDJxL76Guk4SFbe97AzOJKuE/KpyIrCs8JZ8KGM s9wgXjetVLsCgYB2mnqjx/GuBmV3LEChXFBNUnv1YCRkmnoXHpWj4X5zkv0Ro2F+ ypOvF7FY1q2tm+Q8clzngKniHH+TsyyCIdSZqmkoGZyER8y/VDnjSYpLkAnvVOR+ itQ15JfYr2yFYV455e2nXZl9iI7HQBLLPv/E7weCold4m6Za0JNzmBLZWQKBgE7P 0KGi6H6GzyFPC7+4PrtmSoffhBC35UvrtMmdbUk2XtKmDJ+gm4H/g3Otai/yGRWR 9AqSFFGyhyauz4yGBHyKK2VcmLuJzs7uAqRifEx1d/ZBxjo/F5XL9xCdH9FkYf7r acfFxBq69So5cd4J9nf0OD73wxXxgmYXHhmjkF1hAoGBAPFz3EL5syk0aVWclIiZ R5HDlf39dQAhbfjGUBRRBEcyQzvCO37q2esO0deeLh8o5kaHZZ5ymoBQ+rPJEXx3 6HY4oJLVp9BP50GY5955KTKGDhRScTmcn5b/zLQoQsGIFH8xv/HNx3zbnB8HzmXo YV96kc0NhvB0XSGg9OfnbzTS -----END PRIVATE KEY----- pytest-httpserver-1.1.2/tests/assets/v3.ext000066400000000000000000000003101475715145300207730ustar00rootroot00000000000000authorityKeyIdentifier=keyid,issuer basicConstraints=CA:FALSE keyUsage = digitalSignature, nonRepudiation, keyEncipherment, dataEncipherment subjectAltName = @alt_names [alt_names] DNS.1 = localhost pytest-httpserver-1.1.2/tests/conftest.py000066400000000000000000000011211475715145300206170ustar00rootroot00000000000000import pytest def pytest_addoption(parser): parser.addoption("--ssl", action="store_true", default=False, help="run ssl tests") parser.addoption("--release", action="store_true", default=False, help="run release tests") def pytest_runtest_setup(item): markers = [marker.name for marker in item.iter_markers()] if not item.config.getoption("--ssl") and "ssl" in markers: pytest.skip() if item.config.getoption("--ssl") and "ssl" not in markers: pytest.skip() if not item.config.getoption("--release") and "release" in markers: pytest.skip() pytest-httpserver-1.1.2/tests/examples/000077500000000000000000000000001475715145300202435ustar00rootroot00000000000000pytest-httpserver-1.1.2/tests/examples/test_example_blocking_httpserver.py000066400000000000000000000034461475715145300274540ustar00rootroot00000000000000import threading from queue import Queue import pytest import requests from pytest_httpserver import BlockingHTTPServer # override httpserver fixture @pytest.fixture def httpserver(): server = BlockingHTTPServer(timeout=1) server.start() yield server if server.is_running(): server.stop() # this is to check if the client has made any request where no # `assert_request` was called on it from the test server.check_assertions() server.clear() def test_simplified(httpserver: BlockingHTTPServer): def client(response_queue: Queue): response = requests.get(httpserver.url_for("/foobar"), timeout=10) response_queue.put(response) # start the client, server is not yet configured # it will block until we add a request handler to the server # (see the timeout parameter of the http server) response_queue: Queue[requests.models.Response] = Queue(maxsize=1) thread = threading.Thread(target=client, args=(response_queue,)) thread.start() try: # check that the request is for /foobar and it is a GET method # if this does not match, it will raise AssertionError and test will fail client_connection = httpserver.assert_request(uri="/foobar", method="GET") # with the received client_connection, we now need to send back the response # this makes the request.get() call in client() to return client_connection.respond_with_json({"foo": "bar"}) finally: # wait for the client thread to complete thread.join(timeout=1) assert not thread.is_alive() # check if join() has not timed out # check the response the client received response = response_queue.get(timeout=1) assert response.status_code == 200 assert response.json() == {"foo": "bar"} pytest-httpserver-1.1.2/tests/examples/test_example_query_params1.py000066400000000000000000000002011475715145300261510ustar00rootroot00000000000000def test_query_params(httpserver): httpserver.expect_request("/foo", query_string={"user": "user1"}).respond_with_data("OK") pytest-httpserver-1.1.2/tests/examples/test_example_query_params2.py000066400000000000000000000002451475715145300261620ustar00rootroot00000000000000def test_query_params(httpserver): expected_query = {"user": "user1"} httpserver.expect_request("/foo", query_string=expected_query).respond_with_data("OK") pytest-httpserver-1.1.2/tests/examples/test_howto_authorization_headers.py000066400000000000000000000027271475715145300274770ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_authorization_headers(httpserver: HTTPServer): headers_with_values_in_direct_order = { "Authorization": ( 'Digest username="Mufasa",' 'realm="testrealm@host.com",' 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' 'uri="/dir/index.html",' "qop=auth," "nc=00000001," 'cnonce="0a4f113b",' 'response="6629fae49393a05397450978507c4ef1",' 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' ) } httpserver.expect_request(uri="/", headers=headers_with_values_in_direct_order).respond_with_data("OK") response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_direct_order) assert response.status_code == 200 assert response.text == "OK" headers_with_values_in_modified_order = { "Authorization": ( "Digest qop=auth," 'username="Mufasa",' 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' 'uri="/dir/index.html",' "nc=00000001," 'realm="testrealm@host.com",' 'response="6629fae49393a05397450978507c4ef1",' 'cnonce="0a4f113b",' 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' ) } response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_modified_order) assert response.status_code == 200 assert response.text == "OK" pytest-httpserver-1.1.2/tests/examples/test_howto_case_insensitive_matcher.py000066400000000000000000000013561475715145300301370ustar00rootroot00000000000000from typing import Optional import requests from pytest_httpserver import HTTPServer def case_insensitive_matcher(header_name: str, actual: Optional[str], expected: str) -> bool: if actual is None: return False if header_name == "X-Foo": return actual.lower() == expected.lower() else: return actual == expected def test_case_insensitive_matching(httpserver: HTTPServer): httpserver.expect_request( "/", header_value_matcher=case_insensitive_matcher, headers={"X-Foo": "bar"} ).respond_with_data("OK") assert requests.get(httpserver.url_for("/"), headers={"X-Foo": "bar"}).status_code == 200 assert requests.get(httpserver.url_for("/"), headers={"X-Foo": "BAR"}).status_code == 200 pytest-httpserver-1.1.2/tests/examples/test_howto_check.py000066400000000000000000000005611475715145300241530ustar00rootroot00000000000000import pytest import requests from pytest_httpserver import HTTPServer @pytest.mark.xfail def test_check_assertions(httpserver: HTTPServer): def handler(_): assert 1 == 2 httpserver.expect_request("/foobar").respond_with_handler(handler) requests.get(httpserver.url_for("/foobar")) # this will raise AssertionError: httpserver.check() pytest-httpserver-1.1.2/tests/examples/test_howto_check_handler_errors.py000066400000000000000000000022101475715145300272350ustar00rootroot00000000000000import pytest import requests from pytest_httpserver import HTTPServer def test_check_assertions_raises_handler_assertions(httpserver: HTTPServer): def handler(_): assert 1 == 2 httpserver.expect_request("/foobar").respond_with_handler(handler) requests.get(httpserver.url_for("/foobar")) # if you leave this "with" statement out, check_assertions() will break # the test by re-raising the assertion error caused by the handler # pytest will pick this exception as it was happened in the main thread with pytest.raises(AssertionError): httpserver.check_assertions() httpserver.check_handler_errors() def test_check_handler_errors_raises_handler_error(httpserver: HTTPServer): def handler(_): raise ValueError("should be propagated") httpserver.expect_request("/foobar").respond_with_handler(handler) requests.get(httpserver.url_for("/foobar")) httpserver.check_assertions() # if you leave this "with" statement out, check_handler_errors() will # break the test with the original exception with pytest.raises(ValueError): httpserver.check_handler_errors() pytest-httpserver-1.1.2/tests/examples/test_howto_custom_handler.py000066400000000000000000000005261475715145300261060ustar00rootroot00000000000000from random import randint from werkzeug import Request from werkzeug import Response from pytest_httpserver import HTTPServer def test_expected_request_handler(httpserver: HTTPServer): def handler(request: Request): return Response(str(randint(1, 10))) httpserver.expect_request("/foobar").respond_with_handler(handler) pytest-httpserver-1.1.2/tests/examples/test_howto_custom_hooks.py000066400000000000000000000010041475715145300256040ustar00rootroot00000000000000import requests from werkzeug import Request from werkzeug import Response from pytest_httpserver import HTTPServer def my_hook(_request: Request, response: Response) -> Response: # add a new header value to the response response.headers["X-Example"] = "Example" return response def test_custom_hook(httpserver: HTTPServer): httpserver.expect_request("/foo").with_post_hook(my_hook).respond_with_data(b"OK") assert requests.get(httpserver.url_for("/foo")).headers["X-Example"] == "Example" pytest-httpserver-1.1.2/tests/examples/test_howto_custom_request_matcher.py000066400000000000000000000023221475715145300276600ustar00rootroot00000000000000import requests from werkzeug import Request from pytest_httpserver import HTTPServer from pytest_httpserver import RequestMatcher class MyMatcher(RequestMatcher): def match(self, request: Request) -> bool: match = super().match(request) if not match: # existing parameters didn't match -> return with False return match # match the json's "value" key: if it is an integer and it is an even # number, it returns True json = request.json if isinstance(json, dict) and isinstance(json.get("value"), int): return json["value"] % 2 == 0 return False def test_custom_request_matcher(httpserver: HTTPServer): httpserver.expect(MyMatcher("/foo")).respond_with_data("OK") # with even number it matches the request resp = requests.post(httpserver.url_for("/foo"), json={"value": 42}) resp.raise_for_status() assert resp.text == "OK" resp = requests.post(httpserver.url_for("/foo"), json={"value": 198}) resp.raise_for_status() assert resp.text == "OK" # with an odd number, it does not match the request resp = requests.post(httpserver.url_for("/foo"), json={"value": 43}) assert resp.status_code == 500 pytest-httpserver-1.1.2/tests/examples/test_howto_header_value_matcher.py000066400000000000000000000014141475715145300272230ustar00rootroot00000000000000from typing import Optional import requests from pytest_httpserver import HeaderValueMatcher from pytest_httpserver import HTTPServer def case_insensitive_compare(actual: Optional[str], expected: str) -> bool: # actual is `None` if it is not specified if actual is None: return False return actual.lower() == expected.lower() def test_own_matcher_object(httpserver: HTTPServer): matcher = HeaderValueMatcher({"X-Bar": case_insensitive_compare}) httpserver.expect_request("/", headers={"X-Bar": "bar"}, header_value_matcher=matcher).respond_with_data("OK") assert requests.get(httpserver.url_for("/"), headers={"X-Bar": "bar"}).status_code == 200 assert requests.get(httpserver.url_for("/"), headers={"X-Bar": "BAR"}).status_code == 200 pytest-httpserver-1.1.2/tests/examples/test_howto_hooks.py000066400000000000000000000006001475715145300242130ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer from pytest_httpserver.hooks import Delay def test_delay(httpserver: HTTPServer): # this adds 0.5 seconds delay to the server response httpserver.expect_request("/foo").with_post_hook(Delay(0.5)).respond_with_json({"example": "foo"}) assert requests.get(httpserver.url_for("/foo")).json() == {"example": "foo"} pytest-httpserver-1.1.2/tests/examples/test_howto_json_matcher.py000066400000000000000000000005331475715145300255510ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_json_matcher(httpserver: HTTPServer): httpserver.expect_request("/foo", json={"foo": "bar"}).respond_with_data("Hello world!") resp = requests.get(httpserver.url_for("/foo"), json={"foo": "bar"}) assert resp.status_code == 200 assert resp.text == "Hello world!" pytest-httpserver-1.1.2/tests/examples/test_howto_log_querying.py000066400000000000000000000043371475715145300256070ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer from pytest_httpserver import RequestMatcher def test_log_querying_example(httpserver: HTTPServer): # set up the handler for the request httpserver.expect_request("/foo").respond_with_data("OK") # make a request matching the handler assert requests.get(httpserver.url_for("/foo")).text == "OK", "Response should be 'OK'" # make another request non-matching and handler assert ( requests.get(httpserver.url_for("/no_match")).status_code == 500 ), "Response code should be 500 for non-matched requests" # you can query the log directly # log will contain all request-response pair, including non-matching # requests and their response as well assert len(httpserver.log) == 2, "2 request-response pairs should be in the log" # there are the following methods to query the log # # each one uses the matcher we created for the handler in the very beginning # of this test, RequestMatcher accepts the same parameters what you were # specifying to the `expect_request` (and similar) methods. # 1. get counts # (returns 0 for non-matches) httpserver.get_matching_requests_count( RequestMatcher("/foo") ) == 1, "There should be one request matching the the /foo request" # 2. assert for matching request counts # by default it asserts for exactly 1 matches # it is roughly the same as: # ``` # assert httpserver.get_matching_requests_count(...) == 1 # ``` # assertion text will be a fully-detailed explanation about the error, including # the similar handlers (which might have been inproperly configured) httpserver.assert_request_made(RequestMatcher("/foo")) # you can also specify the counts # if you want, you can specify 0 to check for non-matching requests # there should have been 0 requests for /bar httpserver.assert_request_made(RequestMatcher("/bar"), count=0) # 3. iterate over the matching request-response pairs # this provides you greater flexibility for request, response in httpserver.iter_matching_requests(RequestMatcher("/foo")): assert request.url == httpserver.url_for("/foo") assert response.get_data() == b"OK" pytest-httpserver-1.1.2/tests/examples/test_howto_query_params_dict.py000066400000000000000000000005451475715145300266130ustar00rootroot00000000000000import requests def test_query_params(httpserver): httpserver.expect_request("/foo", query_string={"user": "user1", "group": "group1"}).respond_with_data("OK") assert requests.get(httpserver.url_for("/foo?user=user1&group=group1")).status_code == 200 assert requests.get(httpserver.url_for("/foo?group=group1&user=user1")).status_code == 200 pytest-httpserver-1.1.2/tests/examples/test_howto_query_params_never_do_this.py000066400000000000000000000001431475715145300305120ustar00rootroot00000000000000def test_query_params(httpserver): httpserver.expect_request("/foo?user=bar") # never do this pytest-httpserver-1.1.2/tests/examples/test_howto_query_params_proper_use.py000066400000000000000000000001421475715145300300440ustar00rootroot00000000000000def test_query_params(httpserver): httpserver.expect_request("/foo", query_string="user=bar") pytest-httpserver-1.1.2/tests/examples/test_howto_regexp.py000066400000000000000000000002731475715145300243700ustar00rootroot00000000000000import re import requests def test_httpserver_with_regexp(httpserver): httpserver.expect_request(re.compile("^/foo"), method="GET") requests.get(httpserver.url_for("/foobar")) pytest-httpserver-1.1.2/tests/examples/test_howto_timeout_requests.py000066400000000000000000000003531475715145300265160ustar00rootroot00000000000000import pytest import requests def test_connection_refused(): # assumes that there's no server listening at localhost:1234 with pytest.raises(requests.exceptions.ConnectionError): requests.get("http://localhost:1234") pytest-httpserver-1.1.2/tests/examples/test_howto_url_matcher.py000066400000000000000000000006061475715145300254030ustar00rootroot00000000000000from pytest_httpserver import HTTPServer from pytest_httpserver import URIPattern class PrefixMatch(URIPattern): def __init__(self, prefix: str): self.prefix = prefix def match(self, uri): return uri.startswith(self.prefix) def test_uripattern_object(httpserver: HTTPServer): httpserver.expect_request(PrefixMatch("/foo")).respond_with_json({"foo": "bar"}) pytest-httpserver-1.1.2/tests/examples/test_howto_wait_success.py000066400000000000000000000014241475715145300255710ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_wait_success(httpserver: HTTPServer): waiting_timeout = 0.1 with httpserver.wait(stop_on_nohandler=False, timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobar")) httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") requests.get(httpserver.url_for("/foobar")) assert waiting.result httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") httpserver.expect_oneshot_request("/foobaz").respond_with_data("OK foobaz") with httpserver.wait(timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobar")) requests.get(httpserver.url_for("/foobaz")) assert waiting.result pytest-httpserver-1.1.2/tests/test_blocking_httpserver.py000066400000000000000000000070201475715145300241130ustar00rootroot00000000000000from contextlib import contextmanager from copy import deepcopy from multiprocessing.pool import ThreadPool from urllib.parse import urlparse import pytest import requests from pytest_httpserver import BlockingHTTPServer @contextmanager def when_a_request_is_being_sent_to_the_server(request): with ThreadPool(1) as pool: yield pool.apply_async(requests.request, kwds=request) def then_the_server_gets_the_request(server, request): request = deepcopy(request) replace_url_with_uri(request) return server.assert_request(**request) def replace_url_with_uri(request): request["uri"] = get_uri(request["url"]) del request["url"] def get_uri(url): url = urlparse(url) return "?".join(item for item in [url.path, url.query] if item) def when_the_server_responds_to(client_connection, response): client_connection.respond_with_json(response) def then_the_response_is_got_from(server_connection, response): assert server_connection.get(timeout=9).json() == response @pytest.fixture def httpserver(): server = BlockingHTTPServer(timeout=1) server.start() yield server server.clear() if server.is_running(): server.stop() def test_behave_workflow(httpserver: BlockingHTTPServer): request = dict( method="GET", url=httpserver.url_for("/my/path"), ) with when_a_request_is_being_sent_to_the_server(request) as server_connection: client_connection = then_the_server_gets_the_request(httpserver, request) response = {"foo": "bar"} when_the_server_responds_to(client_connection, response) then_the_response_is_got_from(server_connection, response) def test_raises_assertion_error_when_request_does_not_match(httpserver: BlockingHTTPServer): request = dict( method="GET", url=httpserver.url_for("/my/path"), ) with when_a_request_is_being_sent_to_the_server(request): with pytest.raises(AssertionError) as exc: httpserver.assert_request(uri="/not/my/path/") assert "/not/my/path/" in str(exc) assert "does not match" in str(exc) def test_raises_assertion_error_when_request_was_not_sent(httpserver: BlockingHTTPServer): with pytest.raises(AssertionError) as exc: httpserver.assert_request(uri="/my/path/", timeout=1) assert "/my/path/" in str(exc) assert "timed out" in str(exc) def test_ignores_when_request_is_not_asserted(httpserver: BlockingHTTPServer): request = dict( method="GET", url=httpserver.url_for("/my/path"), ) with when_a_request_is_being_sent_to_the_server(request) as server_connection: assert ( server_connection.get(timeout=9).text == "No handler found for request " f" with data b''." ) def test_raises_assertion_error_when_request_was_not_responded(httpserver: BlockingHTTPServer): request = dict( method="GET", url=httpserver.url_for("/my/path"), ) with when_a_request_is_being_sent_to_the_server(request): then_the_server_gets_the_request(httpserver, request) httpserver.stop() # waiting for timeout of waiting for the response with pytest.raises(AssertionError) as exc: httpserver.check_assertions() assert "/my/path" in str(exc) assert "no response" in str(exc).lower() def test_repr(httpserver: BlockingHTTPServer): assert repr(httpserver) == f"" pytest-httpserver-1.1.2/tests/test_handler_errors.py000066400000000000000000000051251475715145300230520ustar00rootroot00000000000000import pytest import requests import werkzeug from pytest_httpserver import HTTPServer def test_check_assertions_raises_handler_assertions(httpserver: HTTPServer): def handler(_): assert False # noqa: PT015 httpserver.expect_request("/foobar").respond_with_handler(handler) requests.get(httpserver.url_for("/foobar")) with pytest.raises(AssertionError): httpserver.check_assertions() httpserver.check_handler_errors() def test_check_handler_errors_raises_handler_error(httpserver: HTTPServer): def handler(_) -> werkzeug.Response: raise ValueError("should be propagated") httpserver.expect_request("/foobar").respond_with_handler(handler) requests.get(httpserver.url_for("/foobar")) httpserver.check_assertions() with pytest.raises(ValueError): # noqa: PT011 httpserver.check_handler_errors() def test_check_handler_errors_correct_order(httpserver: HTTPServer): def handler1(_) -> werkzeug.Response: raise ValueError("should be propagated") def handler2(_) -> werkzeug.Response: raise OSError("should be propagated") httpserver.expect_request("/foobar1").respond_with_handler(handler1) httpserver.expect_request("/foobar2").respond_with_handler(handler2) requests.get(httpserver.url_for("/foobar1")) requests.get(httpserver.url_for("/foobar2")) httpserver.check_assertions() with pytest.raises(ValueError): # noqa: PT011 httpserver.check_handler_errors() with pytest.raises(OSError): # noqa: PT011 httpserver.check_handler_errors() httpserver.check_handler_errors() def test_missing_matcher_raises_exception(httpserver): requests.get(httpserver.url_for("/foobar")) # missing handlers should not raise handler exception here httpserver.check_handler_errors() with pytest.raises(AssertionError): httpserver.check_assertions() def test_check_raises_errors_in_order(httpserver): def handler1(_): assert False # noqa: PT015 def handler2(_): pass # does nothing def handler3(_): raise ValueError httpserver.expect_request("/foobar1").respond_with_handler(handler1) httpserver.expect_request("/foobar2").respond_with_handler(handler2) httpserver.expect_request("/foobar3").respond_with_handler(handler3) requests.get(httpserver.url_for("/foobar1")) requests.get(httpserver.url_for("/foobar2")) requests.get(httpserver.url_for("/foobar3")) with pytest.raises(AssertionError): httpserver.check() with pytest.raises(ValueError): # noqa: PT011 httpserver.check() pytest-httpserver-1.1.2/tests/test_headers.py000066400000000000000000000102701475715145300214510ustar00rootroot00000000000000import http.client import requests from werkzeug.datastructures import Headers from werkzeug.http import parse_dict_header from pytest_httpserver import HTTPServer from pytest_httpserver.httpserver import HeaderValueMatcher def test_custom_headers(httpserver: HTTPServer): headers_with_values_in_direct_order = {"Custom": 'Scheme key0="value0", key1="value1"'} httpserver.expect_request(uri="/", headers=headers_with_values_in_direct_order).respond_with_data("OK") response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_direct_order) assert response.status_code == 200 assert response.text == "OK" # By default different order of items in header value dicts means different header values headers_with_values_in_modified_order = {"Custom": 'Scheme key1="value1", key0="value0"'} response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_modified_order) assert response.status_code == 500 # Define header_value_matcher that ignores the order of items in header value dict def custom_header_value_matcher(actual: str, expected: str) -> bool: actual_scheme, _, actual_dict_str = actual.partition(" ") expected_scheme, _, expected_dict_str = expected.partition(" ") actual_dict = parse_dict_header(actual_dict_str) expected_dict = parse_dict_header(expected_dict_str) return actual_scheme == expected_scheme and actual_dict == expected_dict matchers = HeaderValueMatcher.DEFAULT_MATCHERS.copy() # type: ignore matchers["Custom"] = custom_header_value_matcher header_value_matcher = HeaderValueMatcher(matchers) httpserver.handlers.clear() httpserver.expect_request( uri="/", headers=headers_with_values_in_direct_order, header_value_matcher=header_value_matcher ).respond_with_data("OK") response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_modified_order) assert response.status_code == 200 assert response.text == "OK" # See https://en.wikipedia.org/wiki/Digest_access_authentication def test_authorization_headers(httpserver: HTTPServer): headers_with_values_in_direct_order = { "Authorization": ( 'Digest username="Mufasa",' 'realm="testrealm@host.com",' 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' 'uri="/dir/index.html",' "qop=auth," "nc=00000001," 'cnonce="0a4f113b",' 'response="6629fae49393a05397450978507c4ef1",' 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' ) } httpserver.expect_request(uri="/", headers=headers_with_values_in_direct_order).respond_with_data("OK") response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_direct_order) assert response.status_code == 200 assert response.text == "OK" headers_with_values_in_modified_order = { "Authorization": ( "Digest qop=auth," 'username="Mufasa",' 'nonce="dcd98b7102dd2f0e8b11d0f600bfb0c093",' 'uri="/dir/index.html",' "nc=00000001," 'realm="testrealm@host.com",' 'response="6629fae49393a05397450978507c4ef1",' 'cnonce="0a4f113b",' 'opaque="5ccc069c403ebaf9f0171e9517f40e41"' ) } response = requests.get(httpserver.url_for("/"), headers=headers_with_values_in_modified_order) assert response.status_code == 200 assert response.text == "OK" def test_header_one_key_multiple_values(httpserver: HTTPServer): httpserver.expect_request(uri="/t1").respond_with_data(headers=[("X-Foo", "123"), ("X-Foo", "456")]) httpserver.expect_request(uri="/t2").respond_with_data(headers={"X-Foo": ["123", "456"]}) headers = Headers() headers.add("X-Foo", "123") headers.add("X-Foo", "456") httpserver.expect_request(uri="/t3").respond_with_data(headers=headers) for uri in ("/t1", "/t2", "/t3"): conn = http.client.HTTPConnection("localhost:{}".format(httpserver.port)) conn.request("GET", uri) response = conn.getresponse() conn.close() assert response.status == 200 assert response.headers.get_all("X-Foo") == ["123", "456"] pytest-httpserver-1.1.2/tests/test_hooks.py000066400000000000000000000070261475715145300211660ustar00rootroot00000000000000from __future__ import annotations from typing import TYPE_CHECKING import pytest import requests from pytest_httpserver.hooks import Chain from pytest_httpserver.hooks import Delay from pytest_httpserver.hooks import Garbage if TYPE_CHECKING: from werkzeug import Request from werkzeug import Response from pytest_httpserver import HTTPServer class MyDelay(Delay): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.evidence: int | None = None def _sleep(self): assert self.evidence is None, "_sleep should be called only once" self.evidence = self._seconds def suffix_hook_factory(suffix: bytes): def hook(_request: Request, response: Response) -> Response: response.set_data(response.get_data() + suffix) return response return hook def test_hook(httpserver: HTTPServer): my_hook = suffix_hook_factory(b"-SUFFIX") httpserver.expect_request("/foo").with_post_hook(my_hook).respond_with_data("OK") assert requests.get(httpserver.url_for("/foo")).text == "OK-SUFFIX" def test_delay_hook(httpserver: HTTPServer): delay = MyDelay(10) httpserver.expect_request("/foo").with_post_hook(delay).respond_with_data("OK") assert requests.get(httpserver.url_for("/foo")).text == "OK" assert delay.evidence == 10 def test_garbage_hook(httpserver: HTTPServer): httpserver.expect_request("/prefix").with_post_hook(Garbage(prefix_size=128)).respond_with_data("OK") httpserver.expect_request("/suffix").with_post_hook(Garbage(suffix_size=128)).respond_with_data("OK") httpserver.expect_request("/both").with_post_hook(Garbage(prefix_size=128, suffix_size=128)).respond_with_data("OK") httpserver.expect_request("/large_prefix").with_post_hook(Garbage(prefix_size=10 * 1024 * 1024)).respond_with_data( "OK" ) resp_content = requests.get(httpserver.url_for("/prefix")).content assert len(resp_content) == 130 assert resp_content[128:] == b"OK" resp_content = requests.get(httpserver.url_for("/large_prefix")).content assert len(resp_content) == 10 * 1024 * 1024 + 2 assert resp_content[10 * 1024 * 1024 :] == b"OK" resp_content = requests.get(httpserver.url_for("/suffix")).content assert len(resp_content) == 130 assert resp_content[:2] == b"OK" resp_content = requests.get(httpserver.url_for("/both")).content assert len(resp_content) == 258 assert resp_content[128:130] == b"OK" with pytest.raises(AssertionError, match="prefix_size should be positive integer"): Garbage(-10) with pytest.raises(AssertionError, match="suffix_size should be positive integer"): Garbage(10, -10) def test_chain(httpserver: HTTPServer): delay = MyDelay(10) httpserver.expect_request("/foo").with_post_hook(Chain(delay, Garbage(128))).respond_with_data("OK") assert len(requests.get(httpserver.url_for("/foo")).content) == 130 assert delay.evidence == 10 def test_multiple_hooks(httpserver: HTTPServer): delay = MyDelay(10) httpserver.expect_request("/foo").with_post_hook(delay).with_post_hook(Garbage(128)).respond_with_data("OK") assert len(requests.get(httpserver.url_for("/foo")).content) == 130 assert delay.evidence == 10 def test_multiple_hooks_correct_order(httpserver: HTTPServer): hook1 = suffix_hook_factory(b"-S1") hook2 = suffix_hook_factory(b"-S2") httpserver.expect_request("/foo").with_post_hook(hook1).with_post_hook(hook2).respond_with_data("OK") assert requests.get(httpserver.url_for("/foo")).text == "OK-S1-S2" pytest-httpserver-1.1.2/tests/test_ip_protocols.py000066400000000000000000000010671475715145300225560ustar00rootroot00000000000000import requests def test_ipv4(httpserver_ipv4): httpserver_ipv4.expect_request("/").respond_with_data("OK") assert httpserver_ipv4.host == "127.0.0.1" response = requests.get(httpserver_ipv4.url_for("/")) assert response.text == "OK" def test_ipv6(httpserver_ipv6): httpserver_ipv6.expect_request("/").respond_with_data("OK") assert httpserver_ipv6.host == "::1" assert httpserver_ipv6.url_for("/") == f"http://[::1]:{httpserver_ipv6.port}/" response = requests.get(httpserver_ipv6.url_for("/")) assert response.text == "OK" pytest-httpserver-1.1.2/tests/test_json_matcher.py000066400000000000000000000035271475715145300225210ustar00rootroot00000000000000import json import pytest import requests from pytest_httpserver import HTTPServer def test_json_matcher(httpserver: HTTPServer): httpserver.expect_request("/foo", json={"foo": "bar"}).respond_with_data("Hello world!") assert requests.get(httpserver.url_for("/foo")).status_code == 500 resp = requests.get(httpserver.url_for("/foo"), json={"foo": "bar"}) assert resp.status_code == 200 assert resp.text == "Hello world!" assert requests.get(httpserver.url_for("/foo"), json={"foo": "bar", "foo2": "bar2"}).status_code == 500 def test_json_matcher_with_none(httpserver: HTTPServer): httpserver.expect_request("/foo", json=None).respond_with_data("Hello world!") resp = requests.get(httpserver.url_for("/foo"), data=json.dumps(None), headers={"content-type": "application/json"}) assert resp.status_code == 200 assert resp.text == "Hello world!" def test_json_matcher_without_content_type(httpserver: HTTPServer): httpserver.expect_request("/foo", json={"foo": "bar"}).respond_with_data("Hello world!") assert requests.get(httpserver.url_for("/foo"), json={"foo": "bar"}).status_code == 200 assert requests.get(httpserver.url_for("/foo"), data=json.dumps({"foo": "bar"})).status_code == 200 def test_json_matcher_with_invalid_json(httpserver: HTTPServer): httpserver.expect_request("/foo", json={"foo": "bar"}).respond_with_data("Hello world!") assert requests.get(httpserver.url_for("/foo"), data="invalid-json").status_code == 500 assert requests.get(httpserver.url_for("/foo"), data='{"invalid": "json"').status_code == 500 assert requests.get(httpserver.url_for("/foo"), data=b"non-text\x1f\x8b").status_code == 500 def test_data_and_json_mutually_exclusive(httpserver: HTTPServer): with pytest.raises(ValueError): # noqa: PT011 httpserver.expect_request("/foo", json={}, data="foo") pytest-httpserver-1.1.2/tests/test_log_leak.py000066400000000000000000000010621475715145300216120ustar00rootroot00000000000000import pytest import requests from pytest_httpserver import HTTPServer class Client: def __init__(self) -> None: self.url: str | None = None def get(self): if self.url: requests.get(self.url) @pytest.fixture def my_fixture(): client = Client() yield client client.get() def test_1(my_fixture: Client, httpserver: HTTPServer): httpserver.expect_request("/foo").respond_with_data("OK") my_fixture.url = httpserver.url_for("/foo") def test_2(httpserver: HTTPServer): assert httpserver.log == [] pytest-httpserver-1.1.2/tests/test_log_querying.py000066400000000000000000000063501475715145300225460ustar00rootroot00000000000000import pytest import requests from pytest_httpserver import HTTPServer from pytest_httpserver import RequestMatcher def test_verify(httpserver: HTTPServer): httpserver.expect_request("/foo").respond_with_data("OK") httpserver.expect_request("/bar").respond_with_data("OKOK") assert list(httpserver.iter_matching_requests(httpserver.create_matcher("/foo"))) == [] assert requests.get(httpserver.url_for("/foo")).text == "OK" assert requests.get(httpserver.url_for("/bar")).text == "OKOK" matching_log = list(httpserver.iter_matching_requests(httpserver.create_matcher("/foo"))) assert len(matching_log) == 1 request, response = matching_log[0] assert request.url == httpserver.url_for("/foo") assert response.get_data() == b"OK" assert httpserver.get_matching_requests_count(httpserver.create_matcher("/foo")) == 1 httpserver.assert_request_made(httpserver.create_matcher("/foo")) httpserver.assert_request_made(httpserver.create_matcher("/no_match"), count=0) with pytest.raises(AssertionError): assert httpserver.assert_request_made(httpserver.create_matcher("/no_match")) with pytest.raises(AssertionError): assert httpserver.assert_request_made(httpserver.create_matcher("/foo"), count=2) def test_verify_assert_msg(httpserver: HTTPServer): httpserver.no_handler_status_code = 404 httpserver.expect_request("/foo", json={"foo": "bar"}, method="POST").respond_with_data("OK") headers = {"User-Agent": "requests", "Accept-Encoding": "gzip, deflate"} assert requests.get(httpserver.url_for("/foo"), headers=headers).status_code == 404 expected_lines = [ "Matching request found 0 times but expected 1 times.", "Expected request: ", "Found 1 similar request(s):", "--- Similar Request Start", "Path: /foo", "Method: GET", "Body: b''", f"Headers: Host: {httpserver.host}:{httpserver.port}", "User-Agent: requests", "Accept-Encoding: gzip, deflate", "Accept: */*", "Connection: keep-alive", "", "", "Query String: ''", "--- Similar Request End", ] with pytest.raises(AssertionError) as err: httpserver.assert_request_made(RequestMatcher("/foo", json={"foo": "bar"}, method="POST")) actual_lines = [x.strip() for x in str(err.value).splitlines()][: len(expected_lines)] assert actual_lines == expected_lines def test_verify_assert_msg_no_similar_requests(httpserver: HTTPServer): httpserver.expect_request("/foo", json={"foo": "bar"}, method="POST").respond_with_data("OK") expected_lines = [ "Matching request found 0 times but expected 1 times.", "Expected request: ", "No similar requests found.", ] with pytest.raises(AssertionError) as err: httpserver.assert_request_made(RequestMatcher("/foo", json={"foo": "bar"}, method="POST")) actual_lines = [x.strip() for x in str(err.value).splitlines()][: len(expected_lines)] assert actual_lines == expected_lines pytest-httpserver-1.1.2/tests/test_matcher.py000066400000000000000000000006161475715145300214640ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_expect_method(httpserver: HTTPServer): expected_response = "OK" matcher = httpserver.create_matcher(uri="/test", method="POST") httpserver.expect(matcher).respond_with_data(expected_response) resp = requests.post(httpserver.url_for("/test"), json={"list": [1, 2, 3, 4]}) assert resp.text == expected_response pytest-httpserver-1.1.2/tests/test_mixed.py000066400000000000000000000063651475715145300211560ustar00rootroot00000000000000import pytest import requests from pytest_httpserver import HTTPServer def _setup_oneshot(server: HTTPServer): server.expect_request("/permanent").respond_with_data("OK permanent") server.expect_oneshot_request("/oneshot1").respond_with_data("OK oneshot1") server.expect_oneshot_request("/oneshot2").respond_with_data("OK oneshot2") def _setup_ordered(server: HTTPServer): server.expect_ordered_request("/ordered1").respond_with_data("OK ordered1") server.expect_ordered_request("/ordered2").respond_with_data("OK ordered2") def _setup_all(server: HTTPServer): _setup_oneshot(server) _setup_ordered(server) def test_oneshot_and_permanent_happy_path1(httpserver: HTTPServer): _setup_oneshot(httpserver) assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert requests.get(httpserver.url_for("/oneshot1")).text == "OK oneshot1" assert requests.get(httpserver.url_for("/oneshot2")).text == "OK oneshot2" assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert len(httpserver.oneshot_handlers) == 0 def test_oneshot_and_permanent_happy_path2(httpserver: HTTPServer): _setup_oneshot(httpserver) assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert requests.get(httpserver.url_for("/oneshot2")).text == "OK oneshot2" assert requests.get(httpserver.url_for("/oneshot1")).text == "OK oneshot1" assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert len(httpserver.oneshot_handlers) == 0 def test_all_happy_path1(httpserver: HTTPServer): _setup_all(httpserver) # ordered must go first assert requests.get(httpserver.url_for("/ordered1")).text == "OK ordered1" assert requests.get(httpserver.url_for("/ordered2")).text == "OK ordered2" assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert requests.get(httpserver.url_for("/oneshot2")).text == "OK oneshot2" assert requests.get(httpserver.url_for("/oneshot1")).text == "OK oneshot1" assert requests.get(httpserver.url_for("/permanent")).text == "OK permanent" assert len(httpserver.oneshot_handlers) == 0 assert len(httpserver.ordered_handlers) == 0 def test_all_ordered_missing(httpserver: HTTPServer): _setup_all(httpserver) # ordered is missing so everything must fail # a.k.a. permanently fail requests.get(httpserver.url_for("/permanent")) with pytest.raises(AssertionError): httpserver.check_assertions() requests.get(httpserver.url_for("/oneshot2")) with pytest.raises(AssertionError): httpserver.check_assertions() requests.get(httpserver.url_for("/oneshot1")) with pytest.raises(AssertionError): httpserver.check_assertions() requests.get(httpserver.url_for("/permanent")) with pytest.raises(AssertionError): httpserver.check_assertions() # handlers must be still intact but as the ordered are failed # everything will fail assert len(httpserver.ordered_handlers) == 2 assert len(httpserver.oneshot_handlers) == 2 assert len(httpserver.handlers) == 1 def test_repr(httpserver: HTTPServer): assert repr(httpserver) == f"" pytest-httpserver-1.1.2/tests/test_oneshot.py000066400000000000000000000034311475715145300215160ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_oneshot(httpserver: HTTPServer): httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") httpserver.expect_oneshot_request("/foobaz").respond_with_data("OK foobaz") assert len(httpserver.oneshot_handlers) == 2 # first requests should pass response = requests.get(httpserver.url_for("/foobaz")) httpserver.check_assertions() assert response.status_code == 200 assert response.text == "OK foobaz" response = requests.get(httpserver.url_for("/foobar")) httpserver.check_assertions() assert response.status_code == 200 assert response.text == "OK foobar" assert len(httpserver.oneshot_handlers) == 0 # second requests should fail due to 'oneshot' type assert requests.get(httpserver.url_for("/foobar")).status_code == 500 assert requests.get(httpserver.url_for("/foobaz")).status_code == 500 def test_oneshot_any_method(httpserver: HTTPServer): for _ in range(5): httpserver.expect_oneshot_request("/foobar").respond_with_data("OK") response = requests.post(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.get(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.delete(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.put(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.patch(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 assert len(httpserver.oneshot_handlers) == 0 pytest-httpserver-1.1.2/tests/test_ordered.py000066400000000000000000000032131475715145300214610ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_ordered_ok(httpserver: HTTPServer): httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") assert len(httpserver.ordered_handlers) == 2 # first requests should pass response = requests.get(httpserver.url_for("/foobar")) httpserver.check_assertions() assert response.status_code == 200 assert response.text == "OK foobar" response = requests.get(httpserver.url_for("/foobaz")) httpserver.check_assertions() assert response.status_code == 200 assert response.text == "OK foobaz" assert len(httpserver.ordered_handlers) == 0 # second requests should fail due to 'oneshot' type assert requests.get(httpserver.url_for("/foobar")).status_code == 500 assert requests.get(httpserver.url_for("/foobaz")).status_code == 500 def test_ordered_invalid_order(httpserver: HTTPServer): httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") assert len(httpserver.ordered_handlers) == 2 # these would not pass as the order is different # this mark the whole thing 'permanently failed' so no further requests must pass response = requests.get(httpserver.url_for("/foobaz")) assert response.status_code == 500 response = requests.get(httpserver.url_for("/foobar")) assert response.status_code == 500 # as no ordered handlers are triggered yet, these must be intact.. assert len(httpserver.ordered_handlers) == 2 pytest-httpserver-1.1.2/tests/test_parse_qs.py000066400000000000000000000013421475715145300216530ustar00rootroot00000000000000from __future__ import annotations import urllib.parse import pytest parse_qsl_semicolon_cases = [ ("&", []), ("&&", []), ("&a=b", [("a", "b")]), ("a=a+b&b=b+c", [("a", "a b"), ("b", "b c")]), ("a=1&a=2", [("a", "1"), ("a", "2")]), ("a=", [("a", "")]), ("a=foo bar&b=bar foo", [("a", "foo bar"), ("b", "bar foo")]), ("a=foo%20bar&b=bar%20foo", [("a", "foo bar"), ("b", "bar foo")]), ("a=%20%21%22%23%24%25%26%27%28%29%2A%2B%2C%2F%3A%3B%3D%3F%40%5B%5D", [("a", " !\"#$%&'()*+,/:;=?@[]")]), ] @pytest.mark.parametrize(("qs", "expected"), parse_qsl_semicolon_cases) def test_qsl(qs: str, expected: list[tuple[bytes, bytes]]): assert urllib.parse.parse_qsl(qs, keep_blank_values=True) == expected pytest-httpserver-1.1.2/tests/test_permanent.py000066400000000000000000000110251475715145300220260ustar00rootroot00000000000000import pytest import requests from werkzeug import Response from pytest_httpserver import HTTPServer JSON_STRING = '{"foo": "bar"}' def test_expected_request_json(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} def test_expected_request_data(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_data(JSON_STRING) assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} def test_expected_request_handler(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_handler(lambda request: JSON_STRING) # type: ignore assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} def test_expected_request_response(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_response(Response(JSON_STRING)) assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} def test_expected_request_response_as_string(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_response(JSON_STRING) # type: ignore assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} def test_request_post(httpserver: HTTPServer): httpserver.expect_request("/foobar", data='{"request": "example"}', method="POST").respond_with_data( "example_response" ) response = requests.post(httpserver.url_for("/foobar"), json={"request": "example"}) httpserver.check_assertions() assert response.text == "example_response" assert response.status_code == 200 def test_request_post_case_insensitive_method(httpserver: HTTPServer): httpserver.expect_request("/foobar", data='{"request": "example"}', method="post").respond_with_data( "example_response" ) response = requests.post(httpserver.url_for("/foobar"), json={"request": "example"}) httpserver.check_assertions() assert response.text == "example_response" assert response.status_code == 200 def test_request_any_method(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_data("OK") response = requests.post(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.delete(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.put(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.patch(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 response = requests.get(httpserver.url_for("/foobar")) assert response.text == "OK" assert response.status_code == 200 def test_unexpected_request(httpserver: HTTPServer): httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) requests.get(httpserver.url_for("/nonexists")) with pytest.raises(AssertionError): httpserver.check_assertions() def test_no_handler_status_code(httpserver: HTTPServer): httpserver.no_handler_status_code = 404 assert requests.get(httpserver.url_for("/foobar")).status_code == 404 def test_server_cleared_for_each_test(httpserver: HTTPServer): assert httpserver.log == [] assert httpserver.assertions == [] assert httpserver.ordered_handlers == [] assert httpserver.oneshot_handlers == [] assert httpserver.handlers == [] def test_response_handler_replaced(httpserver: HTTPServer): # https://github.com/csernazs/pytest-httpserver/issues/229 handler = httpserver.expect_request("/foobar") handler.respond_with_data("FOO") response = requests.get(httpserver.url_for("/foobar")) assert response.text == "FOO" assert response.status_code == 200 handler.respond_with_json({"foo": "bar"}) response = requests.get(httpserver.url_for("/foobar")) assert response.json() == {"foo": "bar"} assert response.status_code == 200 def test_request_handler_repr(httpserver: HTTPServer): handler = httpserver.expect_request("/foo", method="POST") assert ( repr(handler) == ">" ) handler = httpserver.expect_request("/query", query_string={"a": "123"}) assert ( repr(handler) == ">" ) pytest-httpserver-1.1.2/tests/test_port_changing.py000066400000000000000000000022721475715145300226630ustar00rootroot00000000000000import os import pytest from pytest_httpserver import HTTPServer from pytest_httpserver.pytest_plugin import get_httpserver_listen_address PORT_KEY = "PYTEST_HTTPSERVER_PORT" HOST_KEY = "PYTEST_HTTPSERVER_HOST" @pytest.fixture def tmpenv(): old_vars = {} for key in (HOST_KEY, PORT_KEY): old_vars[key] = os.environ.get(key) os.environ[HOST_KEY] = "5.5.5.5" os.environ[PORT_KEY] = "12345" yield for key, value in old_vars.items(): if value: os.environ[key] = value else: del os.environ[key] @pytest.mark.skipif(HOST_KEY not in os.environ, reason="requires {} environment variable".format(HOST_KEY)) def test_host_changing_by_environment(httpserver: HTTPServer): assert httpserver.host == os.environ[HOST_KEY] @pytest.mark.skipif(PORT_KEY not in os.environ, reason="requires {} environment variable".format(PORT_KEY)) def test_port_changing_by_environment(httpserver: HTTPServer): assert httpserver.port == int(os.environ[PORT_KEY]) def test_get_httpserver_listen_address_with_env(tmpenv): # noqa: ARG001 address = get_httpserver_listen_address() assert address[0] == "5.5.5.5" assert address[1] == 12345 pytest-httpserver-1.1.2/tests/test_querymatcher.py000066400000000000000000000027271475715145300225570ustar00rootroot00000000000000from werkzeug.datastructures import MultiDict from pytest_httpserver.httpserver import BooleanQueryMatcher from pytest_httpserver.httpserver import MappingQueryMatcher from pytest_httpserver.httpserver import StringQueryMatcher def assert_match(qm, query_string): values = qm.get_comparing_values(query_string) assert values[0] == values[1] def assert_not_match(qm, query_string): values = qm.get_comparing_values(query_string) assert values[0] != values[1] def test_qm_string(): qm = StringQueryMatcher("k1=v1&k2=v2") assert_match(qm, b"k1=v1&k2=v2") assert_not_match(qm, b"k2=v2&k1=v1") def test_qm_bytes(): qm = StringQueryMatcher(b"k1=v1&k2=v2") assert_match(qm, b"k1=v1&k2=v2") assert_not_match(qm, b"k2=v2&k1=v1") def test_qm_boolean(): qm = BooleanQueryMatcher(result=True) assert_match(qm, b"k1=v1") def test_qm_mapping_string(): qm = MappingQueryMatcher({"k1": "v1"}) assert_match(qm, b"k1=v1") def test_qm_mapping_unordered(): qm = MappingQueryMatcher({"k1": "v1", "k2": "v2"}) assert_match(qm, b"k1=v1&k2=v2") assert_match(qm, b"k2=v2&k1=v1") def test_qm_mapping_first_value(): qm = MappingQueryMatcher({"k1": "v1"}) assert_match(qm, b"k1=v1&k1=v2") qm = MappingQueryMatcher({"k1": "v2"}) assert_match(qm, b"k1=v2&k1=v1") def test_qm_mapping_multiple_values(): md = MultiDict([("k1", "v1"), ("k1", "v2")]) qm = MappingQueryMatcher(md) assert_match(qm, b"k1=v1&k1=v2") pytest-httpserver-1.1.2/tests/test_querystring.py000066400000000000000000000025131475715145300224330ustar00rootroot00000000000000import requests from pytest_httpserver import HTTPServer def test_querystring_str(httpserver: HTTPServer): httpserver.expect_request("/foobar", query_string="foo=bar", method="GET").respond_with_data("example_response") response = requests.get(httpserver.url_for("/foobar?foo=bar")) httpserver.check_assertions() assert response.text == "example_response" assert response.status_code == 200 def test_querystring_bytes(httpserver: HTTPServer): httpserver.expect_request("/foobar", query_string=b"foo=bar", method="GET").respond_with_data("example_response") response = requests.get(httpserver.url_for("/foobar?foo=bar")) httpserver.check_assertions() assert response.text == "example_response" assert response.status_code == 200 def test_querystring_dict(httpserver: HTTPServer): httpserver.expect_request("/foobar", query_string={"k1": "v1", "k2": "v2"}, method="GET").respond_with_data( "example_response" ) response = requests.get(httpserver.url_for("/foobar?k1=v1&k2=v2")) httpserver.check_assertions() assert response.text == "example_response" assert response.status_code == 200 response = requests.get(httpserver.url_for("/foobar?k2=v2&k1=v1")) httpserver.check_assertions() assert response.text == "example_response" assert response.status_code == 200 pytest-httpserver-1.1.2/tests/test_release.py000066400000000000000000000156131475715145300214640ustar00rootroot00000000000000from __future__ import annotations import email import re import shutil import subprocess import tarfile import zipfile from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING import pytest if TYPE_CHECKING: from collections.abc import Iterable try: import tomllib except ImportError: # Unfortunately mypy cannot handle this try/expect pattern, and "type: ignore" # is the simplest work-around. See: https://github.com/python/mypy/issues/1153 import tomli as tomllib # type: ignore # TODO: skip if poetry is not available or add mark to test it explicitly pytestmark = pytest.mark.release NAME = "pytest-httpserver" NAME_UNDERSCORE = NAME.replace("-", "_") PY_MAX_VERSION = (3, 13) @pytest.fixture(scope="session") def pyproject_path() -> Path: return Path("pyproject.toml") @pytest.fixture(scope="session") def pyproject(pyproject_path: Path): assert pyproject_path.is_file() with pyproject_path.open("rb") as infile: pyproject = tomllib.load(infile) return pyproject class Wheel: def __init__(self, path: Path): self.path = path @property def wheel_out_dir(self) -> Path: return self.path.parent.joinpath("wheel") def extract(self): with zipfile.ZipFile(self.path) as zf: zf.extractall(self.wheel_out_dir) # noqa: S202 def get_meta(self, version: str, name: str = NAME_UNDERSCORE) -> email.message.Message: metadata_path = self.wheel_out_dir.joinpath(f"{name}-{version}.dist-info", "METADATA") with metadata_path.open() as metadata_file: msg = email.message_from_file(metadata_file) return msg class Sdist: def __init__(self, path: Path): self.path = path @property def sdist_out_dir(self) -> Path: return self.path.parent.joinpath("sdist") def extract(self): with tarfile.open(self.path, mode="r:gz") as tf: tf.extractall(self.sdist_out_dir) # noqa: S202 @dataclass class Build: wheel: Wheel sdist: Sdist def extract(self): self.wheel.extract() self.sdist.extract() @pytest.fixture(scope="session") def build() -> Iterable[Build]: dist_path = Path("dist").resolve() if dist_path.is_dir(): shutil.rmtree(dist_path) try: subprocess.run(["poetry", "build"], check=True) assert dist_path.is_dir() wheels = list(dist_path.glob("*.whl")) sdists = list(dist_path.glob("*.tar.gz")) assert len(wheels) == 1 assert len(sdists) == 1 build = Build(wheel=Wheel(wheels[0]), sdist=Sdist(sdists[0])) build.extract() yield build finally: shutil.rmtree(dist_path) @pytest.fixture(scope="session") def version(pyproject) -> str: return pyproject["tool"]["poetry"]["version"] def version_to_tuple(version: str) -> tuple: return tuple([int(x) for x in version.split(".")]) def test_no_duplicate_classifiers(build: Build, pyproject): pyproject_meta = pyproject["tool"]["poetry"] wheel_meta = build.wheel.get_meta(version=pyproject_meta["version"]) classifiers = wheel_meta.get_all("Classifier") assert classifiers is not None sorted_classifiers = sorted(classifiers) unique_classifiers = sorted(set(classifiers)) assert sorted_classifiers == unique_classifiers def test_python_version(build: Build, pyproject): pyproject_meta = pyproject["tool"]["poetry"] wheel_meta = build.wheel.get_meta(version=pyproject_meta["version"]) python_dependency = pyproject_meta["dependencies"]["python"] m = re.match(r">=(\d+\.\d+)", python_dependency) if m: min_version, *_ = m.groups() else: raise ValueError(python_dependency) min_version_tuple = version_to_tuple(min_version) classifiers = wheel_meta.get_all("Classifier") assert classifiers is not None for classifier in classifiers: if classifier.startswith("Programming Language :: Python ::"): version_tuple = version_to_tuple(classifier.split("::")[-1].strip()) if len(version_tuple) > 1: assert version_tuple >= min_version_tuple assert version_tuple <= PY_MAX_VERSION def test_wheel_no_extra_contents(build: Build, version: str): wheel_dir = build.wheel.wheel_out_dir wheel_contents = list(wheel_dir.iterdir()) assert len(wheel_contents) == 2 assert wheel_dir.joinpath(NAME_UNDERSCORE).is_dir() assert wheel_dir.joinpath(f"{NAME_UNDERSCORE}-{version}.dist-info").is_dir() package_contents = {path.name for path in wheel_dir.joinpath(NAME_UNDERSCORE).iterdir()} assert package_contents == { "__init__.py", "blocking_httpserver.py", "hooks.py", "httpserver.py", "py.typed", "pytest_plugin.py", } def test_sdist_contents(build: Build, version: str): sdist_base = build.sdist.sdist_out_dir.joinpath(f"pytest_httpserver-{version}") subdir_contents = { ".": { "CHANGES.rst", "CONTRIBUTION.md", "doc", "example_pytest.py", "example.py", "LICENSE", "PKG-INFO", "pyproject.toml", "pytest_httpserver", "README.md", "tests", }, "doc": { "_static", "api.rst", "background.rst", "changes.rst", "conf.py", "fixtures.rst", "guide.rst", "howto.rst", "index.rst", "Makefile", "patch.py", "tutorial.rst", "upgrade.rst", }, "pytest_httpserver": { "__init__.py", "blocking_httpserver.py", "hooks.py", "httpserver.py", "py.typed", "pytest_plugin.py", }, "tests": { "assets", "conftest.py", "examples", "test_blocking_httpserver.py", "test_handler_errors.py", "test_headers.py", "test_hooks.py", "test_ip_protocols.py", "test_json_matcher.py", "test_log_leak.py", "test_log_querying.py", "test_mixed.py", "test_oneshot.py", "test_ordered.py", "test_permanent.py", "test_parse_qs.py", "test_port_changing.py", "test_querymatcher.py", "test_querystring.py", "test_release.py", "test_ssl.py", "test_threaded.py", "test_urimatch.py", "test_wait.py", "test_with_statement.py", "test_matcher.py", }, } for subdir, subdir_content in subdir_contents.items(): contents = {path.name for path in sdist_base.joinpath(subdir).iterdir()} assert contents == subdir_content def test_poetry_check(): subprocess.run(["poetry", "check"], check=True) pytest-httpserver-1.1.2/tests/test_ssl.py000066400000000000000000000026011475715145300206360ustar00rootroot00000000000000import os import ssl from os.path import join as pjoin import pytest import requests from pytest_httpserver import HTTPServer pytestmark = pytest.mark.ssl test_dir = os.path.dirname(os.path.realpath(__file__)) assets_dir = pjoin(test_dir, "assets") @pytest.fixture(scope="session") def httpserver_ssl_context(): protocol = None for name in ("PROTOCOL_TLS_SERVER", "PROTOCOL_TLS", "PROTOCOL_TLSv1_2"): if hasattr(ssl, name): protocol = getattr(ssl, name) break assert protocol is not None, "Unable to obtain TLS protocol" return ssl.SSLContext(protocol) def test_ssl(httpserver: HTTPServer): server_crt = pjoin(assets_dir, "server.crt") server_key = pjoin(assets_dir, "server.key") root_ca = pjoin(assets_dir, "rootCA.crt") assert ( httpserver.ssl_context is not None ), "SSLContext not set. The session was probably started with a test that did not define an SSLContext." httpserver.ssl_context.load_cert_chain(server_crt, server_key) httpserver.expect_request("/foobar").respond_with_json({"foo": "bar"}) assert httpserver.is_running() assert httpserver.url_for("/").startswith("https://") # ensure we are using "localhost" and not "127.0.0.1" to pass cert verification url = f"https://localhost:{httpserver.port}/foobar" assert requests.get(url, verify=root_ca).json() == {"foo": "bar"} pytest-httpserver-1.1.2/tests/test_threaded.py000066400000000000000000000025031475715145300216160ustar00rootroot00000000000000import http.client import threading import time from collections.abc import Iterable import pytest from werkzeug import Request from werkzeug import Response from pytest_httpserver import HTTPServer @pytest.fixture def threaded() -> Iterable[HTTPServer]: server = HTTPServer(threaded=True) server.start() yield server server.clear() if server.is_running(): server.stop() def test_threaded(threaded: HTTPServer): sleep_time = 0.5 def handler(_request: Request): # allow some time to the client to have multiple pending request # handlers running in parallel time.sleep(sleep_time) # send back thread id return Response(f"{threading.get_ident()}") threaded.expect_request("/foo").respond_with_handler(handler) number_of_connections = 5 conns = [http.client.HTTPConnection(threaded.host, threaded.port) for _ in range(number_of_connections)] for conn in conns: conn.request("GET", "/foo", headers={"Host": threaded.host}) thread_ids: list[int] = [] for conn in conns: response = conn.getresponse() assert response.status == 200 thread_ids.append(int(response.read())) for conn in conns: conn.close() assert len(thread_ids) == len(set(thread_ids)), "thread ids returned should be unique" pytest-httpserver-1.1.2/tests/test_urimatch.py000066400000000000000000000035231475715145300216550ustar00rootroot00000000000000import re import requests from pytest_httpserver import HTTPServer from pytest_httpserver import URIPattern class PrefixMatch(URIPattern): def __init__(self, prefix: str): self.prefix = prefix def match(self, uri): return uri.startswith(self.prefix) class PrefixMatchEq: def __init__(self, prefix: str): self.prefix = prefix def __eq__(self, uri): return uri.startswith(self.prefix) def test_uripattern_object(httpserver: HTTPServer): httpserver.expect_request(PrefixMatch("/foo")).respond_with_json({"foo": "bar"}) assert requests.get(httpserver.url_for("/foo")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/foobaz")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/barfoo")).status_code == 500 assert len(httpserver.assertions) == 1 def test_regexp(httpserver: HTTPServer): httpserver.expect_request(re.compile(r"/foo/\d+/bar/")).respond_with_json({"foo": "bar"}) assert requests.get(httpserver.url_for("/foo/123/bar/")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/foo/9999/bar/")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/foo/bar/")).status_code == 500 assert len(httpserver.assertions) == 1 def test_object_with_eq(httpserver: HTTPServer): httpserver.expect_request(PrefixMatchEq("/foo")).respond_with_json({"foo": "bar"}) # type: ignore assert requests.get(httpserver.url_for("/foo")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/foobar")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/foobaz")).json() == {"foo": "bar"} assert requests.get(httpserver.url_for("/barfoo")).status_code == 500 assert len(httpserver.assertions) == 1 pytest-httpserver-1.1.2/tests/test_wait.py000066400000000000000000000060621475715145300210060ustar00rootroot00000000000000import requests from pytest import approx from pytest import raises from pytest_httpserver import HTTPServer def test_wait_success(httpserver: HTTPServer): waiting_timeout = 0.1 with httpserver.wait(stop_on_nohandler=False, timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobar")) httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") requests.get(httpserver.url_for("/foobar")) assert waiting.result httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") httpserver.expect_oneshot_request("/foobaz").respond_with_data("OK foobaz") with httpserver.wait(timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobar")) requests.get(httpserver.url_for("/foobaz")) assert waiting.result def test_wait_unexpected_request(httpserver: HTTPServer): def make_unexpected_request_and_wait() -> None: with raises(AssertionError) as error: waiting_timeout = 0.1 with httpserver.wait(raise_assertions=True, stop_on_nohandler=True, timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobaz")) assert not waiting.result no_handler_text = "No handler found for request" assert no_handler_text in str(error.value) make_unexpected_request_and_wait() httpserver.expect_ordered_request("/foobar").respond_with_data("OK foobar") httpserver.expect_ordered_request("/foobaz").respond_with_data("OK foobaz") make_unexpected_request_and_wait() def test_wait_timeout(httpserver: HTTPServer): httpserver.expect_oneshot_request("/foobar").respond_with_data("OK foobar") httpserver.expect_oneshot_request("/foobaz").respond_with_data("OK foobaz") waiting_timeout = 1 with raises(AssertionError) as error: with httpserver.wait(raise_assertions=True, timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobar")) assert not waiting.result waiting_time_error = 0.1 assert waiting.elapsed_time == approx(waiting_timeout, abs=waiting_time_error) assert "Wait timeout occurred, but some handlers left" in str(error.value) def test_wait_raise_assertion_false(httpserver: HTTPServer): waiting_timeout = 0.1 try: with httpserver.wait(raise_assertions=False, stop_on_nohandler=True, timeout=waiting_timeout) as waiting: requests.get(httpserver.url_for("/foobaz")) except AssertionError as error: raise AssertionError("raise_assertions was set to False, but assertion was raised: {}".format(error)) assert not waiting.result try: with httpserver.wait(raise_assertions=False, stop_on_nohandler=True, timeout=waiting_timeout) as waiting: pass except AssertionError as error: raise AssertionError("raise_assertions was set to False, but assertion was raised: {}".format(error)) assert not waiting.result waiting_time_error = 0.1 assert waiting.elapsed_time == approx(waiting_timeout, abs=waiting_time_error) pytest-httpserver-1.1.2/tests/test_with_statement.py000066400000000000000000000001721475715145300230750ustar00rootroot00000000000000from pytest_httpserver import HTTPServer def test_server_with_statement(): with HTTPServer(port=4001): pass pytest-httpserver-1.1.2/tox.ini000066400000000000000000000003611475715145300165760ustar00rootroot00000000000000[tox] envlist = py38 py39 py310 py311 base_python = py311 isolated_build = true [testenv] whitelist_externals = poetry commands = poetry install -v --with test poetry run pytest -vv poetry run pytest -vv --ssl pytest-httpserver-1.1.2/tox.nix000066400000000000000000000006361475715145300166220ustar00rootroot00000000000000{ pkgs ? import {} }: let unstable = import { config = { allowUnfree = true; }; }; toxPython = p: p.withPackages ( p: [ p.pip ] ); basePython = p: (p.withPackages ( p: [ p.virtualenv p.pip p.tox ] )); in pkgs.mkShell { buildInputs = with pkgs; [ (basePython python311) (toxPython python310) (toxPython python39) (toxPython python38) bashInteractive ]; }