├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .pre-commit-config.yaml ├── LICENSE ├── README.md ├── images ├── multiple_browsers.gif ├── options_marker.gif └── quickstart.gif ├── poetry.lock ├── pyproject.toml ├── src └── pytest_pyppeteer │ ├── __init__.py │ ├── page.py │ └── plugin.py └── tests ├── conftest.py ├── pytest.ini └── test_pytest_pyppeteer.py /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - "v*.*.*" 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Build and publish to pypi 14 | uses: JRubics/poetry-publish@v1.10 15 | with: 16 | pypi_token: ${{ secrets.PYPI_TOKEN }} 17 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | branches: 7 | - dev 8 | paths: 9 | - "**.py" 10 | 11 | jobs: 12 | build-and-deploy: 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [macos-latest, windows-latest] 17 | python-version: ["3.7", "3.8", "3.9", "3.10"] 18 | name: Testing with python ${{ matrix.python-version }} on ${{ matrix.os }} 19 | steps: 20 | - name: Checkout 🛎️ 21 | uses: actions/checkout@v2.3.1 22 | with: 23 | persist-credentials: false 24 | 25 | - name: Set up Python 26 | uses: actions/setup-python@v2 27 | with: 28 | python-version: ${{ matrix.python-version }} 29 | 30 | - name: Install dependencies 31 | run: | 32 | python -m pip install --upgrade pip setuptools wheel 33 | python -m pip install poetry 34 | poetry install 35 | 36 | - name: Execute tests 37 | run: | 38 | poetry run pytest tests -sv 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### Windows template 2 | # Windows thumbnail cache files 3 | Thumbs.db 4 | Thumbs.db:encryptable 5 | ehthumbs.db 6 | ehthumbs_vista.db 7 | 8 | # Dump file 9 | *.stackdump 10 | 11 | # Folder config file 12 | [Dd]esktop.ini 13 | 14 | # Recycle Bin used on file shares 15 | $RECYCLE.BIN/ 16 | 17 | # Windows Installer files 18 | *.cab 19 | *.msi 20 | *.msix 21 | *.msm 22 | *.msp 23 | 24 | # Windows shortcuts 25 | *.lnk 26 | 27 | ### JetBrains template 28 | .idea 29 | 30 | ### Python template 31 | # Byte-compiled / optimized / DLL files 32 | __pycache__/ 33 | *.py[cod] 34 | *$py.class 35 | 36 | # C extensions 37 | *.so 38 | 39 | # Distribution / packaging 40 | .Python 41 | build/ 42 | develop-eggs/ 43 | dist/ 44 | downloads/ 45 | eggs/ 46 | .eggs/ 47 | lib/ 48 | lib64/ 49 | parts/ 50 | sdist/ 51 | var/ 52 | wheels/ 53 | pip-wheel-metadata/ 54 | share/python-wheels/ 55 | *.egg-info/ 56 | .installed.cfg 57 | *.egg 58 | MANIFEST 59 | 60 | # PyInstaller 61 | # Usually these files are written by a python script from a template 62 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 63 | *.manifest 64 | *.spec 65 | 66 | # Installer logs 67 | pip-log.txt 68 | pip-delete-this-directory.txt 69 | 70 | # Unit test / coverage reports 71 | htmlcov/ 72 | .tox/ 73 | .nox/ 74 | .coverage 75 | .coverage.* 76 | .cache 77 | nosetests.xml 78 | coverage.xml 79 | *.cover 80 | *.py,cover 81 | .hypothesis/ 82 | .pytest_cache/ 83 | cover/ 84 | 85 | # Translations 86 | *.mo 87 | *.pot 88 | 89 | # Django stuff: 90 | *.log 91 | local_settings.py 92 | db.sqlite3 93 | db.sqlite3-journal 94 | 95 | # Flask stuff: 96 | instance/ 97 | .webassets-cache 98 | 99 | # Scrapy stuff: 100 | .scrapy 101 | 102 | # Sphinx documentation 103 | docs/_build/ 104 | 105 | # PyBuilder 106 | .pybuilder/ 107 | target/ 108 | 109 | # Jupyter Notebook 110 | .ipynb_checkpoints 111 | 112 | # IPython 113 | profile_default/ 114 | ipython_config.py 115 | 116 | # pyenv 117 | # For a library or package, you might want to ignore these files since the code is 118 | # intended to run in multiple environments; otherwise, check them in: 119 | # .python-version 120 | 121 | # pipenv 122 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 123 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 124 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 125 | # install all needed dependencies. 126 | #Pipfile.lock 127 | 128 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 129 | __pypackages__/ 130 | 131 | # Celery stuff 132 | celerybeat-schedule 133 | celerybeat.pid 134 | 135 | # SageMath parsed files 136 | *.sage.py 137 | 138 | # Environments 139 | .env 140 | .venv 141 | env/ 142 | venv/ 143 | ENV/ 144 | env.bak/ 145 | venv.bak/ 146 | 147 | # Spyder project settings 148 | .spyderproject 149 | .spyproject 150 | 151 | # Rope project settings 152 | .ropeproject 153 | 154 | # mkdocs documentation 155 | /site 156 | 157 | # mypy 158 | .mypy_cache/ 159 | .dmypy.json 160 | dmypy.json 161 | 162 | # Pyre type checker 163 | .pyre/ 164 | 165 | # pytype static type analyzer 166 | .pytype/ 167 | 168 | # Cython debug symbols 169 | cython_debug/ 170 | 171 | ### VisualStudioCode template 172 | .vscode/* 173 | .vscode/settings.json 174 | !.vscode/tasks.json 175 | !.vscode/launch.json 176 | !.vscode/extensions.json 177 | *.code-workspace 178 | 179 | # Local History for Visual Studio Code 180 | .history/ 181 | 182 | ### VirtualEnv template 183 | # Virtualenv 184 | # http://iamzed.com/2009/05/07/a-primer-on-virtualenv/ 185 | .Python 186 | [Bb]in 187 | [Ii]nclude 188 | [Ll]ib 189 | [Ll]ib64 190 | [Ll]ocal 191 | [Ss]cripts 192 | pyvenv.cfg 193 | .venv 194 | pip-selfcheck.json 195 | 196 | ### macOS template 197 | # General 198 | .DS_Store 199 | .AppleDouble 200 | .LSOverride 201 | 202 | # Icon must end with two \r 203 | Icon 204 | 205 | # Thumbnails 206 | ._* 207 | 208 | # Files that might appear in the root of a volume 209 | .DocumentRevisions-V100 210 | .fseventsd 211 | .Spotlight-V100 212 | .TemporaryItems 213 | .Trashes 214 | .VolumeIcon.icns 215 | .com.apple.timemachine.donotpresent 216 | 217 | # Directories potentially created on remote AFP share 218 | .AppleDB 219 | .AppleDesktop 220 | Network Trash Folder 221 | Temporary Items 222 | .apdisk 223 | 224 | .python-version 225 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | fail_fast: true 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v4.0.1 5 | hooks: 6 | - id: trailing-whitespace 7 | - id: end-of-file-fixer 8 | exclude_types: [json] 9 | - id: check-docstring-first 10 | - id: check-json 11 | - id: check-yaml 12 | - id: check-toml 13 | - id: debug-statements 14 | - repo: https://github.com/psf/black 15 | rev: 21.6b0 16 | hooks: 17 | - id: black 18 | args: ["--line-length", "120"] 19 | language_version: python3 20 | - repo: https://github.com/pycqa/isort 21 | rev: 5.8.0 22 | hooks: 23 | - id: isort 24 | args: ["--profile", "black"] 25 | - repo: https://github.com/pycqa/flake8 26 | rev: 3.9.2 27 | hooks: 28 | - id: flake8 29 | exclude: docs 30 | args: 31 | [ 32 | "--max-line-length=120", 33 | "--docstring-convention=google", 34 | "--exclude=tests/*", 35 | ] 36 | additional_dependencies: 37 | [ 38 | "flake8-bugbear==21.4.3", 39 | "flake8-docstrings==1.6.0", 40 | "flake8-print==4.0.0", 41 | "flake8-comprehensions==3.5.0", 42 | ] 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020-2022 Yao Meng 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pytest-pyppeteer 2 | 3 | A plugin to run [pyppeteer](https://github.com/pyppeteer/pyppeteer) in pytest. 4 | 5 | ![PyPI - Python Version](https://img.shields.io/pypi/pyversions/pytest-pyppeteer) 6 | [![GitHub issues](https://img.shields.io/github/issues-raw/luizyao/pytest-pyppeteer)](https://github.com/luizyao/pytest-pyppeteer/issues) 7 | [![PyPI](https://img.shields.io/pypi/v/pytest-pyppeteer)](https://pypi.org/project/pytest-pyppeteer/) 8 | [![Downloads](https://pepy.tech/badge/pytest-pyppeteer)](https://pepy.tech/project/pytest-pyppeteer) 9 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 10 | 11 | # Installation 12 | 13 | You can install pytest-pyppeteer via [pip](https://pypi.org/project/pip/): 14 | 15 | ```bash 16 | $ pip install pytest-pyppeteer 17 | ``` 18 | 19 | or install the latest one on Github: 20 | 21 | ```bash 22 | pip install git+https://github.com/luizyao/pytest-pyppeteer.git 23 | ``` 24 | 25 | # Quickstart 26 | 27 | For example, **The Shawshank Redemption** deserves a 9.0 or higher rating on [douban.com](https://movie.douban.com). 28 | 29 | ```python 30 | from dataclasses import dataclass 31 | 32 | 33 | @dataclass(init=False) 34 | class Elements: 35 | """Collect locators of page objects, no matter XPath or CSS Selector.""" 36 | 37 | # query input 38 | query = "#inp-query" 39 | 40 | # search button 41 | apply = ".inp-btn > input:nth-child(1)" 42 | 43 | # the first result 44 | first_result = "#root > div > div > div > div > div:nth-child(1) > div.item-root a.cover-link" 45 | 46 | # rating 47 | rating = "#interest_sectl > div.rating_wrap.clearbox > div.rating_self.clearfix > strong" 48 | 49 | 50 | async def test_lifetimes(browser): 51 | page = await browser.new_page() 52 | await page.goto("https://movie.douban.com/") 53 | 54 | await page.type(Elements.query, "The Shawshank Redemption") 55 | await page.click(Elements.apply) 56 | 57 | await page.waitfor(Elements.first_result) 58 | await page.click(Elements.first_result) 59 | 60 | await page.waitfor(Elements.rating) 61 | rating = await page.get_value(Elements.rating) 62 | 63 | assert float(rating) >= 9.0 64 | ``` 65 | 66 | ![quickstart](images/quickstart.gif) 67 | 68 | # Usage 69 | 70 | ## Fixtures 71 | 72 | ### `browser` fixture 73 | 74 | Provide an `pyppeteer.browser.Browser` instance with a new method `new_page()`, like `pyppeteer.browser.Browser.newPage()`, `new_page()` could create a `pyppeteer.page.Page` instance. 75 | 76 | But the `pyppeteer.page.Page` instance created by `new_page()` has some new methods: 77 | 78 | | Method | Type | 79 | | ------------- | -------- | 80 | | query_locator | New | 81 | | waitfor | New | 82 | | click | Override | 83 | | type | Override | 84 | | get_value | New | 85 | 86 | For example, you can query an element by css or xpath in the same method `query_locator` instead of original `querySelector` and `xpath`. 87 | 88 | > More details check with [page.py](src/pytest_pyppeteer/page.py) in the source code. 89 | 90 | ### `browser_factory` fixture 91 | 92 | Provide to create an `pyppeteer.browser.Browser` instance. 93 | 94 | For example, query the **The Shawshank Redemption**’s movie and book rating on [douban.com](https://movie.douban.com/) at the same time, then compare them. 95 | 96 | ```python 97 | import asyncio 98 | from dataclasses import dataclass 99 | 100 | 101 | @dataclass 102 | class Elements: 103 | query = "#inp-query" 104 | apply = ".inp-btn > input:nth-child(1)" 105 | 106 | 107 | @dataclass 108 | class BookElements(Elements): 109 | url = "https://book.douban.com/" 110 | 111 | result = '(//*[@class="item-root"])[1]/a' 112 | rating = "#interest_sectl > div > div.rating_self.clearfix > strong" 113 | 114 | 115 | @dataclass 116 | class MovieElements(Elements): 117 | url = "https://movie.douban.com/" 118 | 119 | result = "#root > div > div > div > div > div:nth-child(1) > div.item-root a.cover-link" 120 | rating = "#interest_sectl > div.rating_wrap.clearbox > div.rating_self.clearfix > strong" 121 | 122 | 123 | async def query_rating(browser, name: str, elements: "Elements"): 124 | page = await browser.new_page() 125 | 126 | await page.goto(elements.url) 127 | 128 | await page.type(elements.query, name) 129 | await page.click(elements.apply) 130 | 131 | await page.waitfor(elements.result) 132 | await page.click(elements.result) 133 | 134 | await page.waitfor(elements.rating) 135 | rating = await page.get_value(elements.rating) 136 | return rating 137 | 138 | 139 | async def test_multiple_browsers(browser_factory): 140 | browser1 = await browser_factory() 141 | browser2 = await browser_factory() 142 | 143 | movie_rating, book_rating = await asyncio.gather( 144 | query_rating(browser1, "The Shawshank Redemption", MovieElements), 145 | query_rating(browser2, "The Shawshank Redemption", BookElements), 146 | ) 147 | 148 | assert movie_rating == book_rating 149 | ``` 150 | 151 | ![multiple_browsers](images/multiple_browsers.gif) 152 | 153 | ## Command line options 154 | 155 | ### `--executable-path` 156 | 157 | You can specify the Chromium or Chrome executable path. Otherwise I will use the default install path of Chrome in current platform. 158 | 159 | For other platforms, pyppeteer will downloads the recent version of Chromium when called first time. If you don’t prefer this behavior, you can specify an exact path by override this fixture: 160 | 161 | ```python 162 | @pytest.fixture(scope="session") 163 | def executable_path(executable_path): 164 | return executable_path or "path/to/Chrome/or/Chromium" 165 | ``` 166 | 167 | ### `--headless` 168 | 169 | Run browser in headless mode. 170 | 171 | ### `--args` 172 | 173 | Additional args to pass to the browser instance. 174 | 175 | For example, specify a proxy: 176 | 177 | ```bash 178 | $ pytest --args proxy-server "localhost:5555,direct://" --args proxy-bypass-list "192.0.0.1/8;10.0.0.1/8" 179 | ``` 180 | 181 | Or by override the `args` fixture: 182 | 183 | ```python 184 | @pytest.fixture(scope="session") 185 | def args(args) -> List[str]: 186 | return args + [ 187 | "--proxy-server=localhost:5555,direct://", 188 | "--proxy-bypass-list=192.0.0.1/8;10.0.0.1/8", 189 | ] 190 | ``` 191 | 192 | ### `--window-size` 193 | 194 | The default browser size is 800\*600, you can use this option to change this behavior: 195 | 196 | ```bash 197 | $ pytest --window-size 1200 800 198 | ``` 199 | 200 | `--window-size 0 0` means to starts the browser maximized. 201 | 202 | ### `--slow` 203 | 204 | Slow down the pyppeteer operate in milliseconds. Defaults to `0.0`. 205 | 206 | ## Markers 207 | 208 | ### `options` 209 | 210 | You can override some command line options in the specified test. 211 | 212 | For example, auto-open a DevTools panel: 213 | 214 | ```python 215 | import asyncio 216 | 217 | import pytest 218 | 219 | 220 | @pytest.mark.options(devtools=True) 221 | async def test_marker(browser): 222 | await browser.new_page() 223 | await asyncio.sleep(2) 224 | ``` 225 | 226 | ![options marker](images/options_marker.gif) 227 | 228 | # License 229 | 230 | Distributed under the terms of the [MIT](http://opensource.org/licenses/MIT) license, pytest-pyppeteer is free and open source software. 231 | 232 | # Issues 233 | 234 | If you encounter any problems, please [file an issue](https://github.com/luizyao/pytest-pyppeteer/issues) along with a detailed description. 235 | -------------------------------------------------------------------------------- /images/multiple_browsers.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizyao/pytest-pyppeteer/18ad08e1fe81e189bb8276a8d5dd514f714692f1/images/multiple_browsers.gif -------------------------------------------------------------------------------- /images/options_marker.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizyao/pytest-pyppeteer/18ad08e1fe81e189bb8276a8d5dd514f714692f1/images/options_marker.gif -------------------------------------------------------------------------------- /images/quickstart.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/luizyao/pytest-pyppeteer/18ad08e1fe81e189bb8276a8d5dd514f714692f1/images/quickstart.gif -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "appdirs" 5 | version = "1.4.4" 6 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 7 | optional = false 8 | python-versions = "*" 9 | files = [ 10 | {file = "appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128"}, 11 | {file = "appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41"}, 12 | ] 13 | 14 | [[package]] 15 | name = "atomicwrites" 16 | version = "1.4.0" 17 | description = "Atomic file writes." 18 | optional = false 19 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 20 | files = [ 21 | {file = "atomicwrites-1.4.0-py2.py3-none-any.whl", hash = "sha256:6d1784dea7c0c8d4a5172b6c620f40b6e4cbfdf96d783691f2e1302a7b88e197"}, 22 | {file = "atomicwrites-1.4.0.tar.gz", hash = "sha256:ae70396ad1a434f9c7046fd2dd196fc04b12f9e91ffb859164193be8b6168a7a"}, 23 | ] 24 | 25 | [[package]] 26 | name = "attrs" 27 | version = "21.4.0" 28 | description = "Classes Without Boilerplate" 29 | optional = false 30 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 31 | files = [ 32 | {file = "attrs-21.4.0-py2.py3-none-any.whl", hash = "sha256:2d27e3784d7a565d36ab851fe94887c5eccd6a463168875832a1be79c82828b4"}, 33 | {file = "attrs-21.4.0.tar.gz", hash = "sha256:626ba8234211db98e869df76230a137c4c40a12d72445c45d5f5b716f076e2fd"}, 34 | ] 35 | 36 | [package.extras] 37 | dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "sphinx", "sphinx-notfound-page", "zope.interface"] 38 | docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] 39 | tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six", "zope.interface"] 40 | tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "six"] 41 | 42 | [[package]] 43 | name = "certifi" 44 | version = "2023.7.22" 45 | description = "Python package for providing Mozilla's CA Bundle." 46 | optional = false 47 | python-versions = ">=3.6" 48 | files = [ 49 | {file = "certifi-2023.7.22-py3-none-any.whl", hash = "sha256:92d6037539857d8206b8f6ae472e8b77db8058fec5937a1ef3f54304089edbb9"}, 50 | {file = "certifi-2023.7.22.tar.gz", hash = "sha256:539cc1d13202e33ca466e88b2807e29f4c13049d6d87031a3c110744495cb082"}, 51 | ] 52 | 53 | [[package]] 54 | name = "cfgv" 55 | version = "3.3.1" 56 | description = "Validate configuration and produce human readable error messages." 57 | optional = false 58 | python-versions = ">=3.6.1" 59 | files = [ 60 | {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, 61 | {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, 62 | ] 63 | 64 | [[package]] 65 | name = "colorama" 66 | version = "0.4.4" 67 | description = "Cross-platform colored terminal text." 68 | optional = false 69 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 70 | files = [ 71 | {file = "colorama-0.4.4-py2.py3-none-any.whl", hash = "sha256:9f47eda37229f68eee03b24b9748937c7dc3868f906e8ba69fbcbdd3bc5dc3e2"}, 72 | {file = "colorama-0.4.4.tar.gz", hash = "sha256:5941b2b48a20143d2267e95b1c2a7603ce057ee39fd88e7329b0c292aa16869b"}, 73 | ] 74 | 75 | [[package]] 76 | name = "coverage" 77 | version = "6.4.1" 78 | description = "Code coverage measurement for Python" 79 | optional = false 80 | python-versions = ">=3.7" 81 | files = [ 82 | {file = "coverage-6.4.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f1d5aa2703e1dab4ae6cf416eb0095304f49d004c39e9db1d86f57924f43006b"}, 83 | {file = "coverage-6.4.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ce1b258493cbf8aec43e9b50d89982346b98e9ffdfaae8ae5793bc112fb0068"}, 84 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:83c4e737f60c6936460c5be330d296dd5b48b3963f48634c53b3f7deb0f34ec4"}, 85 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:84e65ef149028516c6d64461b95a8dbcfce95cfd5b9eb634320596173332ea84"}, 86 | {file = "coverage-6.4.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f69718750eaae75efe506406c490d6fc5a6161d047206cc63ce25527e8a3adad"}, 87 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e57816f8ffe46b1df8f12e1b348f06d164fd5219beba7d9433ba79608ef011cc"}, 88 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:01c5615d13f3dd3aa8543afc069e5319cfa0c7d712f6e04b920431e5c564a749"}, 89 | {file = "coverage-6.4.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:75ab269400706fab15981fd4bd5080c56bd5cc07c3bccb86aab5e1d5a88dc8f4"}, 90 | {file = "coverage-6.4.1-cp310-cp310-win32.whl", hash = "sha256:a7f3049243783df2e6cc6deafc49ea123522b59f464831476d3d1448e30d72df"}, 91 | {file = "coverage-6.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:ee2ddcac99b2d2aec413e36d7a429ae9ebcadf912946b13ffa88e7d4c9b712d6"}, 92 | {file = "coverage-6.4.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:fb73e0011b8793c053bfa85e53129ba5f0250fdc0392c1591fd35d915ec75c46"}, 93 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106c16dfe494de3193ec55cac9640dd039b66e196e4641fa8ac396181578b982"}, 94 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:87f4f3df85aa39da00fd3ec4b5abeb7407e82b68c7c5ad181308b0e2526da5d4"}, 95 | {file = "coverage-6.4.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:961e2fb0680b4f5ad63234e0bf55dfb90d302740ae9c7ed0120677a94a1590cb"}, 96 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:cec3a0f75c8f1031825e19cd86ee787e87cf03e4fd2865c79c057092e69e3a3b"}, 97 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:129cd05ba6f0d08a766d942a9ed4b29283aff7b2cccf5b7ce279d50796860bb3"}, 98 | {file = "coverage-6.4.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:bf5601c33213d3cb19d17a796f8a14a9eaa5e87629a53979a5981e3e3ae166f6"}, 99 | {file = "coverage-6.4.1-cp37-cp37m-win32.whl", hash = "sha256:269eaa2c20a13a5bf17558d4dc91a8d078c4fa1872f25303dddcbba3a813085e"}, 100 | {file = "coverage-6.4.1-cp37-cp37m-win_amd64.whl", hash = "sha256:f02cbbf8119db68455b9d763f2f8737bb7db7e43720afa07d8eb1604e5c5ae28"}, 101 | {file = "coverage-6.4.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:ffa9297c3a453fba4717d06df579af42ab9a28022444cae7fa605af4df612d54"}, 102 | {file = "coverage-6.4.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:145f296d00441ca703a659e8f3eb48ae39fb083baba2d7ce4482fb2723e050d9"}, 103 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d44996140af8b84284e5e7d398e589574b376fb4de8ccd28d82ad8e3bea13"}, 104 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2bd9a6fc18aab8d2e18f89b7ff91c0f34ff4d5e0ba0b33e989b3cd4194c81fd9"}, 105 | {file = "coverage-6.4.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3384f2a3652cef289e38100f2d037956194a837221edd520a7ee5b42d00cc605"}, 106 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:9b3e07152b4563722be523e8cd0b209e0d1a373022cfbde395ebb6575bf6790d"}, 107 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:1480ff858b4113db2718848d7b2d1b75bc79895a9c22e76a221b9d8d62496428"}, 108 | {file = "coverage-6.4.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:865d69ae811a392f4d06bde506d531f6a28a00af36f5c8649684a9e5e4a85c83"}, 109 | {file = "coverage-6.4.1-cp38-cp38-win32.whl", hash = "sha256:664a47ce62fe4bef9e2d2c430306e1428ecea207ffd68649e3b942fa8ea83b0b"}, 110 | {file = "coverage-6.4.1-cp38-cp38-win_amd64.whl", hash = "sha256:26dff09fb0d82693ba9e6231248641d60ba606150d02ed45110f9ec26404ed1c"}, 111 | {file = "coverage-6.4.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:d9c80df769f5ec05ad21ea34be7458d1dc51ff1fb4b2219e77fe24edf462d6df"}, 112 | {file = "coverage-6.4.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:39ee53946bf009788108b4dd2894bf1349b4e0ca18c2016ffa7d26ce46b8f10d"}, 113 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f5b66caa62922531059bc5ac04f836860412f7f88d38a476eda0a6f11d4724f4"}, 114 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd180ed867e289964404051a958f7cccabdeed423f91a899829264bb7974d3d3"}, 115 | {file = "coverage-6.4.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84631e81dd053e8a0d4967cedab6db94345f1c36107c71698f746cb2636c63e3"}, 116 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:8c08da0bd238f2970230c2a0d28ff0e99961598cb2e810245d7fc5afcf1254e8"}, 117 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:d42c549a8f41dc103a8004b9f0c433e2086add8a719da00e246e17cbe4056f72"}, 118 | {file = "coverage-6.4.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:309ce4a522ed5fca432af4ebe0f32b21d6d7ccbb0f5fcc99290e71feba67c264"}, 119 | {file = "coverage-6.4.1-cp39-cp39-win32.whl", hash = "sha256:fdb6f7bd51c2d1714cea40718f6149ad9be6a2ee7d93b19e9f00934c0f2a74d9"}, 120 | {file = "coverage-6.4.1-cp39-cp39-win_amd64.whl", hash = "sha256:342d4aefd1c3e7f620a13f4fe563154d808b69cccef415415aece4c786665397"}, 121 | {file = "coverage-6.4.1-pp36.pp37.pp38-none-any.whl", hash = "sha256:4803e7ccf93230accb928f3a68f00ffa80a88213af98ed338a57ad021ef06815"}, 122 | {file = "coverage-6.4.1.tar.gz", hash = "sha256:4321f075095a096e70aff1d002030ee612b65a205a0a0f5b815280d5dc58100c"}, 123 | ] 124 | 125 | [package.dependencies] 126 | tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} 127 | 128 | [package.extras] 129 | toml = ["tomli"] 130 | 131 | [[package]] 132 | name = "cssselect" 133 | version = "1.1.0" 134 | description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" 135 | optional = false 136 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" 137 | files = [ 138 | {file = "cssselect-1.1.0-py2.py3-none-any.whl", hash = "sha256:f612ee47b749c877ebae5bb77035d8f4202c6ad0f0fc1271b3c18ad6c4468ecf"}, 139 | {file = "cssselect-1.1.0.tar.gz", hash = "sha256:f95f8dedd925fd8f54edb3d2dfb44c190d9d18512377d3c1e2388d16126879bc"}, 140 | ] 141 | 142 | [[package]] 143 | name = "distlib" 144 | version = "0.3.4" 145 | description = "Distribution utilities" 146 | optional = false 147 | python-versions = "*" 148 | files = [ 149 | {file = "distlib-0.3.4-py2.py3-none-any.whl", hash = "sha256:6564fe0a8f51e734df6333d08b8b94d4ea8ee6b99b5ed50613f731fd4089f34b"}, 150 | {file = "distlib-0.3.4.zip", hash = "sha256:e4b58818180336dc9c529bfb9a0b58728ffc09ad92027a3f30b7cd91e3458579"}, 151 | ] 152 | 153 | [[package]] 154 | name = "filelock" 155 | version = "3.4.2" 156 | description = "A platform independent file lock." 157 | optional = false 158 | python-versions = ">=3.7" 159 | files = [ 160 | {file = "filelock-3.4.2-py3-none-any.whl", hash = "sha256:cf0fc6a2f8d26bd900f19bf33915ca70ba4dd8c56903eeb14e1e7a2fd7590146"}, 161 | {file = "filelock-3.4.2.tar.gz", hash = "sha256:38b4f4c989f9d06d44524df1b24bd19e167d851f19b50bf3e3559952dddc5b80"}, 162 | ] 163 | 164 | [package.extras] 165 | docs = ["furo (>=2021.8.17b43)", "sphinx (>=4.1)", "sphinx-autodoc-typehints (>=1.12)"] 166 | testing = ["covdefaults (>=1.2.0)", "coverage (>=4)", "pytest (>=4)", "pytest-cov", "pytest-timeout (>=1.4.2)"] 167 | 168 | [[package]] 169 | name = "identify" 170 | version = "2.4.5" 171 | description = "File identification library for Python" 172 | optional = false 173 | python-versions = ">=3.7" 174 | files = [ 175 | {file = "identify-2.4.5-py2.py3-none-any.whl", hash = "sha256:d27d10099844741c277b45d809bd452db0d70a9b41ea3cd93799ebbbcc6dcb29"}, 176 | {file = "identify-2.4.5.tar.gz", hash = "sha256:d11469ff952a4d7fd7f9be520d335dc450f585d474b39b5dfb86a500831ab6c7"}, 177 | ] 178 | 179 | [package.extras] 180 | license = ["ukkonen"] 181 | 182 | [[package]] 183 | name = "importlib-metadata" 184 | version = "4.10.1" 185 | description = "Read metadata from Python packages" 186 | optional = false 187 | python-versions = ">=3.7" 188 | files = [ 189 | {file = "importlib_metadata-4.10.1-py3-none-any.whl", hash = "sha256:899e2a40a8c4a1aec681feef45733de8a6c58f3f6a0dbed2eb6574b4387a77b6"}, 190 | {file = "importlib_metadata-4.10.1.tar.gz", hash = "sha256:951f0d8a5b7260e9db5e41d429285b5f451e928479f19d80818878527d36e95e"}, 191 | ] 192 | 193 | [package.dependencies] 194 | typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} 195 | zipp = ">=0.5" 196 | 197 | [package.extras] 198 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 199 | perf = ["ipython"] 200 | testing = ["flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy", "pytest-perf (>=0.9.2)"] 201 | 202 | [[package]] 203 | name = "iniconfig" 204 | version = "1.1.1" 205 | description = "iniconfig: brain-dead simple config-ini parsing" 206 | optional = false 207 | python-versions = "*" 208 | files = [ 209 | {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, 210 | {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, 211 | ] 212 | 213 | [[package]] 214 | name = "lxml" 215 | version = "4.9.1" 216 | description = "Powerful and Pythonic XML processing library combining libxml2/libxslt with the ElementTree API." 217 | optional = false 218 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, != 3.4.*" 219 | files = [ 220 | {file = "lxml-4.9.1-cp27-cp27m-macosx_10_15_x86_64.whl", hash = "sha256:98cafc618614d72b02185ac583c6f7796202062c41d2eeecdf07820bad3295ed"}, 221 | {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c62e8dd9754b7debda0c5ba59d34509c4688f853588d75b53c3791983faa96fc"}, 222 | {file = "lxml-4.9.1-cp27-cp27m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:21fb3d24ab430fc538a96e9fbb9b150029914805d551deeac7d7822f64631dfc"}, 223 | {file = "lxml-4.9.1-cp27-cp27m-win32.whl", hash = "sha256:86e92728ef3fc842c50a5cb1d5ba2bc66db7da08a7af53fb3da79e202d1b2cd3"}, 224 | {file = "lxml-4.9.1-cp27-cp27m-win_amd64.whl", hash = "sha256:4cfbe42c686f33944e12f45a27d25a492cc0e43e1dc1da5d6a87cbcaf2e95627"}, 225 | {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:dad7b164905d3e534883281c050180afcf1e230c3d4a54e8038aa5cfcf312b84"}, 226 | {file = "lxml-4.9.1-cp27-cp27mu-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:a614e4afed58c14254e67862456d212c4dcceebab2eaa44d627c2ca04bf86837"}, 227 | {file = "lxml-4.9.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:f9ced82717c7ec65a67667bb05865ffe38af0e835cdd78728f1209c8fffe0cad"}, 228 | {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:d9fc0bf3ff86c17348dfc5d322f627d78273eba545db865c3cd14b3f19e57fa5"}, 229 | {file = "lxml-4.9.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:e5f66bdf0976ec667fc4594d2812a00b07ed14d1b44259d19a41ae3fff99f2b8"}, 230 | {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fe17d10b97fdf58155f858606bddb4e037b805a60ae023c009f760d8361a4eb8"}, 231 | {file = "lxml-4.9.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:8caf4d16b31961e964c62194ea3e26a0e9561cdf72eecb1781458b67ec83423d"}, 232 | {file = "lxml-4.9.1-cp310-cp310-win32.whl", hash = "sha256:4780677767dd52b99f0af1f123bc2c22873d30b474aa0e2fc3fe5e02217687c7"}, 233 | {file = "lxml-4.9.1-cp310-cp310-win_amd64.whl", hash = "sha256:b122a188cd292c4d2fcd78d04f863b789ef43aa129b233d7c9004de08693728b"}, 234 | {file = "lxml-4.9.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:be9eb06489bc975c38706902cbc6888f39e946b81383abc2838d186f0e8b6a9d"}, 235 | {file = "lxml-4.9.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:f1be258c4d3dc609e654a1dc59d37b17d7fef05df912c01fc2e15eb43a9735f3"}, 236 | {file = "lxml-4.9.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:927a9dd016d6033bc12e0bf5dee1dde140235fc8d0d51099353c76081c03dc29"}, 237 | {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9232b09f5efee6a495a99ae6824881940d6447debe272ea400c02e3b68aad85d"}, 238 | {file = "lxml-4.9.1-cp35-cp35m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:04da965dfebb5dac2619cb90fcf93efdb35b3c6994fea58a157a834f2f94b318"}, 239 | {file = "lxml-4.9.1-cp35-cp35m-win32.whl", hash = "sha256:4d5bae0a37af799207140652a700f21a85946f107a199bcb06720b13a4f1f0b7"}, 240 | {file = "lxml-4.9.1-cp35-cp35m-win_amd64.whl", hash = "sha256:4878e667ebabe9b65e785ac8da4d48886fe81193a84bbe49f12acff8f7a383a4"}, 241 | {file = "lxml-4.9.1-cp36-cp36m-macosx_10_15_x86_64.whl", hash = "sha256:1355755b62c28950f9ce123c7a41460ed9743c699905cbe664a5bcc5c9c7c7fb"}, 242 | {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:bcaa1c495ce623966d9fc8a187da80082334236a2a1c7e141763ffaf7a405067"}, 243 | {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6eafc048ea3f1b3c136c71a86db393be36b5b3d9c87b1c25204e7d397cee9536"}, 244 | {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:13c90064b224e10c14dcdf8086688d3f0e612db53766e7478d7754703295c7c8"}, 245 | {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:206a51077773c6c5d2ce1991327cda719063a47adc02bd703c56a662cdb6c58b"}, 246 | {file = "lxml-4.9.1-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:e8f0c9d65da595cfe91713bc1222af9ecabd37971762cb830dea2fc3b3bb2acf"}, 247 | {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_aarch64.whl", hash = "sha256:8f0a4d179c9a941eb80c3a63cdb495e539e064f8054230844dcf2fcb812b71d3"}, 248 | {file = "lxml-4.9.1-cp36-cp36m-musllinux_1_1_x86_64.whl", hash = "sha256:830c88747dce8a3e7525defa68afd742b4580df6aa2fdd6f0855481e3994d391"}, 249 | {file = "lxml-4.9.1-cp36-cp36m-win32.whl", hash = "sha256:1e1cf47774373777936c5aabad489fef7b1c087dcd1f426b621fda9dcc12994e"}, 250 | {file = "lxml-4.9.1-cp36-cp36m-win_amd64.whl", hash = "sha256:5974895115737a74a00b321e339b9c3f45c20275d226398ae79ac008d908bff7"}, 251 | {file = "lxml-4.9.1-cp37-cp37m-macosx_10_15_x86_64.whl", hash = "sha256:1423631e3d51008871299525b541413c9b6c6423593e89f9c4cfbe8460afc0a2"}, 252 | {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:2aaf6a0a6465d39b5ca69688fce82d20088c1838534982996ec46633dc7ad6cc"}, 253 | {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:9f36de4cd0c262dd9927886cc2305aa3f2210db437aa4fed3fb4940b8bf4592c"}, 254 | {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:ae06c1e4bc60ee076292e582a7512f304abdf6c70db59b56745cca1684f875a4"}, 255 | {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:57e4d637258703d14171b54203fd6822fda218c6c2658a7d30816b10995f29f3"}, 256 | {file = "lxml-4.9.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:6d279033bf614953c3fc4a0aa9ac33a21e8044ca72d4fa8b9273fe75359d5cca"}, 257 | {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:a60f90bba4c37962cbf210f0188ecca87daafdf60271f4c6948606e4dabf8785"}, 258 | {file = "lxml-4.9.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:6ca2264f341dd81e41f3fffecec6e446aa2121e0b8d026fb5130e02de1402785"}, 259 | {file = "lxml-4.9.1-cp37-cp37m-win32.whl", hash = "sha256:27e590352c76156f50f538dbcebd1925317a0f70540f7dc8c97d2931c595783a"}, 260 | {file = "lxml-4.9.1-cp37-cp37m-win_amd64.whl", hash = "sha256:eea5d6443b093e1545ad0210e6cf27f920482bfcf5c77cdc8596aec73523bb7e"}, 261 | {file = "lxml-4.9.1-cp38-cp38-macosx_10_15_x86_64.whl", hash = "sha256:f05251bbc2145349b8d0b77c0d4e5f3b228418807b1ee27cefb11f69ed3d233b"}, 262 | {file = "lxml-4.9.1-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:487c8e61d7acc50b8be82bda8c8d21d20e133c3cbf41bd8ad7eb1aaeb3f07c97"}, 263 | {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:8d1a92d8e90b286d491e5626af53afef2ba04da33e82e30744795c71880eaa21"}, 264 | {file = "lxml-4.9.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:b570da8cd0012f4af9fa76a5635cd31f707473e65a5a335b186069d5c7121ff2"}, 265 | {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5ef87fca280fb15342726bd5f980f6faf8b84a5287fcc2d4962ea8af88b35130"}, 266 | {file = "lxml-4.9.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:93e414e3206779ef41e5ff2448067213febf260ba747fc65389a3ddaa3fb8715"}, 267 | {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6653071f4f9bac46fbc30f3c7838b0e9063ee335908c5d61fb7a4a86c8fd2036"}, 268 | {file = "lxml-4.9.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:32a73c53783becdb7eaf75a2a1525ea8e49379fb7248c3eeefb9412123536387"}, 269 | {file = "lxml-4.9.1-cp38-cp38-win32.whl", hash = "sha256:1a7c59c6ffd6ef5db362b798f350e24ab2cfa5700d53ac6681918f314a4d3b94"}, 270 | {file = "lxml-4.9.1-cp38-cp38-win_amd64.whl", hash = "sha256:1436cf0063bba7888e43f1ba8d58824f085410ea2025befe81150aceb123e345"}, 271 | {file = "lxml-4.9.1-cp39-cp39-macosx_10_15_x86_64.whl", hash = "sha256:4beea0f31491bc086991b97517b9683e5cfb369205dac0148ef685ac12a20a67"}, 272 | {file = "lxml-4.9.1-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:41fb58868b816c202e8881fd0f179a4644ce6e7cbbb248ef0283a34b73ec73bb"}, 273 | {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_24_aarch64.whl", hash = "sha256:bd34f6d1810d9354dc7e35158aa6cc33456be7706df4420819af6ed966e85448"}, 274 | {file = "lxml-4.9.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:edffbe3c510d8f4bf8640e02ca019e48a9b72357318383ca60e3330c23aaffc7"}, 275 | {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6d949f53ad4fc7cf02c44d6678e7ff05ec5f5552b235b9e136bd52e9bf730b91"}, 276 | {file = "lxml-4.9.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.whl", hash = "sha256:079b68f197c796e42aa80b1f739f058dcee796dc725cc9a1be0cdb08fc45b000"}, 277 | {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9c3a88d20e4fe4a2a4a84bf439a5ac9c9aba400b85244c63a1ab7088f85d9d25"}, 278 | {file = "lxml-4.9.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4e285b5f2bf321fc0857b491b5028c5f276ec0c873b985d58d7748ece1d770dd"}, 279 | {file = "lxml-4.9.1-cp39-cp39-win32.whl", hash = "sha256:ef72013e20dd5ba86a8ae1aed7f56f31d3374189aa8b433e7b12ad182c0d2dfb"}, 280 | {file = "lxml-4.9.1-cp39-cp39-win_amd64.whl", hash = "sha256:10d2017f9150248563bb579cd0d07c61c58da85c922b780060dcc9a3aa9f432d"}, 281 | {file = "lxml-4.9.1-pp37-pypy37_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0538747a9d7827ce3e16a8fdd201a99e661c7dee3c96c885d8ecba3c35d1032c"}, 282 | {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:0645e934e940107e2fdbe7c5b6fb8ec6232444260752598bc4d09511bd056c0b"}, 283 | {file = "lxml-4.9.1-pp37-pypy37_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:6daa662aba22ef3258934105be2dd9afa5bb45748f4f702a3b39a5bf53a1f4dc"}, 284 | {file = "lxml-4.9.1-pp38-pypy38_pp73-macosx_10_15_x86_64.whl", hash = "sha256:603a464c2e67d8a546ddaa206d98e3246e5db05594b97db844c2f0a1af37cf5b"}, 285 | {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c4b2e0559b68455c085fb0f6178e9752c4be3bba104d6e881eb5573b399d1eb2"}, 286 | {file = "lxml-4.9.1-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:0f3f0059891d3254c7b5fb935330d6db38d6519ecd238ca4fce93c234b4a0f73"}, 287 | {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_24_i686.whl", hash = "sha256:c852b1530083a620cb0de5f3cd6826f19862bafeaf77586f1aef326e49d95f0c"}, 288 | {file = "lxml-4.9.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_24_x86_64.whl", hash = "sha256:287605bede6bd36e930577c5925fcea17cb30453d96a7b4c63c14a257118dbb9"}, 289 | {file = "lxml-4.9.1.tar.gz", hash = "sha256:fe749b052bb7233fe5d072fcb549221a8cb1a16725c47c37e42b0b9cb3ff2c3f"}, 290 | ] 291 | 292 | [package.extras] 293 | cssselect = ["cssselect (>=0.7)"] 294 | html5 = ["html5lib"] 295 | htmlsoup = ["BeautifulSoup4"] 296 | source = ["Cython (>=0.29.7)"] 297 | 298 | [[package]] 299 | name = "nodeenv" 300 | version = "1.6.0" 301 | description = "Node.js virtual environment builder" 302 | optional = false 303 | python-versions = "*" 304 | files = [ 305 | {file = "nodeenv-1.6.0-py2.py3-none-any.whl", hash = "sha256:621e6b7076565ddcacd2db0294c0381e01fd28945ab36bcf00f41c5daf63bef7"}, 306 | {file = "nodeenv-1.6.0.tar.gz", hash = "sha256:3ef13ff90291ba2a4a7a4ff9a979b63ffdd00a464dbe04acf0ea6471517a4c2b"}, 307 | ] 308 | 309 | [[package]] 310 | name = "packaging" 311 | version = "21.3" 312 | description = "Core utilities for Python packages" 313 | optional = false 314 | python-versions = ">=3.6" 315 | files = [ 316 | {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, 317 | {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, 318 | ] 319 | 320 | [package.dependencies] 321 | pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" 322 | 323 | [[package]] 324 | name = "platformdirs" 325 | version = "2.4.1" 326 | description = "A small Python module for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." 327 | optional = false 328 | python-versions = ">=3.7" 329 | files = [ 330 | {file = "platformdirs-2.4.1-py3-none-any.whl", hash = "sha256:1d7385c7db91728b83efd0ca99a5afb296cab9d0ed8313a45ed8ba17967ecfca"}, 331 | {file = "platformdirs-2.4.1.tar.gz", hash = "sha256:440633ddfebcc36264232365d7840a970e75e1018d15b4327d11f91909045fda"}, 332 | ] 333 | 334 | [package.extras] 335 | docs = ["Sphinx (>=4)", "furo (>=2021.7.5b38)", "proselint (>=0.10.2)", "sphinx-autodoc-typehints (>=1.12)"] 336 | test = ["appdirs (==1.4.4)", "pytest (>=6)", "pytest-cov (>=2.7)", "pytest-mock (>=3.6)"] 337 | 338 | [[package]] 339 | name = "pluggy" 340 | version = "1.0.0" 341 | description = "plugin and hook calling mechanisms for python" 342 | optional = false 343 | python-versions = ">=3.6" 344 | files = [ 345 | {file = "pluggy-1.0.0-py2.py3-none-any.whl", hash = "sha256:74134bbf457f031a36d68416e1509f34bd5ccc019f0bcc952c7b909d06b37bd3"}, 346 | {file = "pluggy-1.0.0.tar.gz", hash = "sha256:4224373bacce55f955a878bf9cfa763c1e360858e330072059e10bad68531159"}, 347 | ] 348 | 349 | [package.dependencies] 350 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 351 | 352 | [package.extras] 353 | dev = ["pre-commit", "tox"] 354 | testing = ["pytest", "pytest-benchmark"] 355 | 356 | [[package]] 357 | name = "pre-commit" 358 | version = "2.17.0" 359 | description = "A framework for managing and maintaining multi-language pre-commit hooks." 360 | optional = false 361 | python-versions = ">=3.6.1" 362 | files = [ 363 | {file = "pre_commit-2.17.0-py2.py3-none-any.whl", hash = "sha256:725fa7459782d7bec5ead072810e47351de01709be838c2ce1726b9591dad616"}, 364 | {file = "pre_commit-2.17.0.tar.gz", hash = "sha256:c1a8040ff15ad3d648c70cc3e55b93e4d2d5b687320955505587fd79bbaed06a"}, 365 | ] 366 | 367 | [package.dependencies] 368 | cfgv = ">=2.0.0" 369 | identify = ">=1.0.0" 370 | importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} 371 | nodeenv = ">=0.11.1" 372 | pyyaml = ">=5.1" 373 | toml = "*" 374 | virtualenv = ">=20.0.8" 375 | 376 | [[package]] 377 | name = "py" 378 | version = "1.11.0" 379 | description = "library with cross-python path, ini-parsing, io, code, log facilities" 380 | optional = false 381 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" 382 | files = [ 383 | {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, 384 | {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, 385 | ] 386 | 387 | [[package]] 388 | name = "pyee" 389 | version = "8.2.2" 390 | description = "A port of node.js's EventEmitter to python." 391 | optional = false 392 | python-versions = "*" 393 | files = [ 394 | {file = "pyee-8.2.2-py2.py3-none-any.whl", hash = "sha256:c09f56e36eb10bf23aa2aacf145f690ded75b990a3d9523fd478b005940303d2"}, 395 | {file = "pyee-8.2.2.tar.gz", hash = "sha256:5c7e60f8df95710dbe17550e16ce0153f83990c00ef744841b43f371ed53ebea"}, 396 | ] 397 | 398 | [[package]] 399 | name = "pyparsing" 400 | version = "3.0.6" 401 | description = "Python parsing module" 402 | optional = false 403 | python-versions = ">=3.6" 404 | files = [ 405 | {file = "pyparsing-3.0.6-py3-none-any.whl", hash = "sha256:04ff808a5b90911829c55c4e26f75fa5ca8a2f5f36aa3a51f68e27033341d3e4"}, 406 | {file = "pyparsing-3.0.6.tar.gz", hash = "sha256:d9bdec0013ef1eb5a84ab39a3b3868911598afa494f5faa038647101504e2b81"}, 407 | ] 408 | 409 | [package.extras] 410 | diagrams = ["jinja2", "railroad-diagrams"] 411 | 412 | [[package]] 413 | name = "pyppeteer" 414 | version = "1.0.2" 415 | description = "Headless chrome/chromium automation library (unofficial port of puppeteer)" 416 | optional = false 417 | python-versions = ">=3.7,<4.0" 418 | files = [ 419 | {file = "pyppeteer-1.0.2-py3-none-any.whl", hash = "sha256:11a734d8f02c6b128035aba8faf32748f2016310a6a1cbc6aa5b1e2580742e8f"}, 420 | {file = "pyppeteer-1.0.2.tar.gz", hash = "sha256:ddb0d15cb644720160d49abb1ad0d97e87a55581febf1b7531be9e983aad7742"}, 421 | ] 422 | 423 | [package.dependencies] 424 | appdirs = ">=1.4.3,<2.0.0" 425 | certifi = ">=2021" 426 | importlib-metadata = ">=1.4" 427 | pyee = ">=8.1.0,<9.0.0" 428 | tqdm = ">=4.42.1,<5.0.0" 429 | urllib3 = ">=1.25.8,<2.0.0" 430 | websockets = ">=10.0,<11.0" 431 | 432 | [[package]] 433 | name = "pytest" 434 | version = "6.2.5" 435 | description = "pytest: simple powerful testing with Python" 436 | optional = false 437 | python-versions = ">=3.6" 438 | files = [ 439 | {file = "pytest-6.2.5-py3-none-any.whl", hash = "sha256:7310f8d27bc79ced999e760ca304d69f6ba6c6649c0b60fb0e04a4a77cacc134"}, 440 | {file = "pytest-6.2.5.tar.gz", hash = "sha256:131b36680866a76e6781d13f101efb86cf674ebb9762eb70d3082b6f29889e89"}, 441 | ] 442 | 443 | [package.dependencies] 444 | atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} 445 | attrs = ">=19.2.0" 446 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 447 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 448 | iniconfig = "*" 449 | packaging = "*" 450 | pluggy = ">=0.12,<2.0" 451 | py = ">=1.8.2" 452 | toml = "*" 453 | 454 | [package.extras] 455 | testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] 456 | 457 | [[package]] 458 | name = "pytest-asyncio" 459 | version = "0.17.2" 460 | description = "Pytest support for asyncio" 461 | optional = false 462 | python-versions = ">=3.7" 463 | files = [ 464 | {file = "pytest-asyncio-0.17.2.tar.gz", hash = "sha256:6d895b02432c028e6957d25fc936494e78c6305736e785d9fee408b1efbc7ff4"}, 465 | {file = "pytest_asyncio-0.17.2-py3-none-any.whl", hash = "sha256:e0fe5dbea40516b661ef1bcfe0bd9461c2847c4ef4bb40012324f2454fb7d56d"}, 466 | ] 467 | 468 | [package.dependencies] 469 | pytest = ">=6.1.0" 470 | typing-extensions = {version = ">=4.0", markers = "python_version < \"3.8\""} 471 | 472 | [package.extras] 473 | testing = ["coverage (==6.2)", "flaky (>=3.5.0)", "hypothesis (>=5.7.1)", "mypy (==0.931)"] 474 | 475 | [[package]] 476 | name = "pytest-cov" 477 | version = "3.0.0" 478 | description = "Pytest plugin for measuring coverage." 479 | optional = false 480 | python-versions = ">=3.6" 481 | files = [ 482 | {file = "pytest-cov-3.0.0.tar.gz", hash = "sha256:e7f0f5b1617d2210a2cabc266dfe2f4c75a8d32fb89eafb7ad9d06f6d076d470"}, 483 | {file = "pytest_cov-3.0.0-py3-none-any.whl", hash = "sha256:578d5d15ac4a25e5f961c938b85a05b09fdaae9deef3bb6de9a6e766622ca7a6"}, 484 | ] 485 | 486 | [package.dependencies] 487 | coverage = {version = ">=5.2.1", extras = ["toml"]} 488 | pytest = ">=4.6" 489 | 490 | [package.extras] 491 | testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] 492 | 493 | [[package]] 494 | name = "pyyaml" 495 | version = "6.0" 496 | description = "YAML parser and emitter for Python" 497 | optional = false 498 | python-versions = ">=3.6" 499 | files = [ 500 | {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, 501 | {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, 502 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, 503 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, 504 | {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, 505 | {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, 506 | {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, 507 | {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, 508 | {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, 509 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, 510 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, 511 | {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, 512 | {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, 513 | {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, 514 | {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, 515 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, 516 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, 517 | {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, 518 | {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, 519 | {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, 520 | {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, 521 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, 522 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, 523 | {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, 524 | {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, 525 | {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, 526 | {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, 527 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, 528 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, 529 | {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, 530 | {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, 531 | {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, 532 | {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, 533 | {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, 534 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, 535 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, 536 | {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, 537 | {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, 538 | {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, 539 | {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, 540 | ] 541 | 542 | [[package]] 543 | name = "six" 544 | version = "1.16.0" 545 | description = "Python 2 and 3 compatibility utilities" 546 | optional = false 547 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" 548 | files = [ 549 | {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, 550 | {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, 551 | ] 552 | 553 | [[package]] 554 | name = "toml" 555 | version = "0.10.2" 556 | description = "Python Library for Tom's Obvious, Minimal Language" 557 | optional = false 558 | python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" 559 | files = [ 560 | {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, 561 | {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, 562 | ] 563 | 564 | [[package]] 565 | name = "tomli" 566 | version = "2.0.0" 567 | description = "A lil' TOML parser" 568 | optional = false 569 | python-versions = ">=3.7" 570 | files = [ 571 | {file = "tomli-2.0.0-py3-none-any.whl", hash = "sha256:b5bde28da1fed24b9bd1d4d2b8cba62300bfb4ec9a6187a957e8ddb9434c5224"}, 572 | {file = "tomli-2.0.0.tar.gz", hash = "sha256:c292c34f58502a1eb2bbb9f5bbc9a5ebc37bee10ffb8c2d6bbdfa8eb13cc14e1"}, 573 | ] 574 | 575 | [[package]] 576 | name = "tqdm" 577 | version = "4.66.3" 578 | description = "Fast, Extensible Progress Meter" 579 | optional = false 580 | python-versions = ">=3.7" 581 | files = [ 582 | {file = "tqdm-4.66.3-py3-none-any.whl", hash = "sha256:4f41d54107ff9a223dca80b53efe4fb654c67efaba7f47bada3ee9d50e05bd53"}, 583 | {file = "tqdm-4.66.3.tar.gz", hash = "sha256:23097a41eba115ba99ecae40d06444c15d1c0c698d527a01c6c8bd1c5d0647e5"}, 584 | ] 585 | 586 | [package.dependencies] 587 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 588 | 589 | [package.extras] 590 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] 591 | notebook = ["ipywidgets (>=6)"] 592 | slack = ["slack-sdk"] 593 | telegram = ["requests"] 594 | 595 | [[package]] 596 | name = "typing-extensions" 597 | version = "4.0.1" 598 | description = "Backported and Experimental Type Hints for Python 3.6+" 599 | optional = false 600 | python-versions = ">=3.6" 601 | files = [ 602 | {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, 603 | {file = "typing_extensions-4.0.1.tar.gz", hash = "sha256:4ca091dea149f945ec56afb48dae714f21e8692ef22a395223bcd328961b6a0e"}, 604 | ] 605 | 606 | [[package]] 607 | name = "urllib3" 608 | version = "1.26.18" 609 | description = "HTTP library with thread-safe connection pooling, file post, and more." 610 | optional = false 611 | python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" 612 | files = [ 613 | {file = "urllib3-1.26.18-py2.py3-none-any.whl", hash = "sha256:34b97092d7e0a3a8cf7cd10e386f401b3737364026c45e622aa02903dffe0f07"}, 614 | {file = "urllib3-1.26.18.tar.gz", hash = "sha256:f8ecc1bba5667413457c529ab955bf8c67b45db799d159066261719e328580a0"}, 615 | ] 616 | 617 | [package.extras] 618 | brotli = ["brotli (==1.0.9)", "brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] 619 | secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] 620 | socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] 621 | 622 | [[package]] 623 | name = "virtualenv" 624 | version = "20.13.0" 625 | description = "Virtual Python Environment builder" 626 | optional = false 627 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,>=2.7" 628 | files = [ 629 | {file = "virtualenv-20.13.0-py2.py3-none-any.whl", hash = "sha256:339f16c4a86b44240ba7223d0f93a7887c3ca04b5f9c8129da7958447d079b09"}, 630 | {file = "virtualenv-20.13.0.tar.gz", hash = "sha256:d8458cf8d59d0ea495ad9b34c2599487f8a7772d796f9910858376d1600dd2dd"}, 631 | ] 632 | 633 | [package.dependencies] 634 | distlib = ">=0.3.1,<1" 635 | filelock = ">=3.2,<4" 636 | importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} 637 | platformdirs = ">=2,<3" 638 | six = ">=1.9.0,<2" 639 | 640 | [package.extras] 641 | docs = ["proselint (>=0.10.2)", "sphinx (>=3)", "sphinx-argparse (>=0.2.5)", "sphinx-rtd-theme (>=0.4.3)", "towncrier (>=21.3)"] 642 | testing = ["coverage (>=4)", "coverage-enable-subprocess (>=1)", "flaky (>=3)", "packaging (>=20.0)", "pytest (>=4)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.1)", "pytest-mock (>=2)", "pytest-randomly (>=1)", "pytest-timeout (>=1)"] 643 | 644 | [[package]] 645 | name = "websockets" 646 | version = "10.1" 647 | description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" 648 | optional = false 649 | python-versions = ">=3.7" 650 | files = [ 651 | {file = "websockets-10.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:38db6e2163b021642d0a43200ee2dec8f4980bdbda96db54fde72b283b54cbfc"}, 652 | {file = "websockets-10.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e1b60fd297adb9fc78375778a5220da7f07bf54d2a33ac781319650413fc6a60"}, 653 | {file = "websockets-10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:3477146d1f87ead8df0f27e8960249f5248dceb7c2741e8bbec9aa5338d0c053"}, 654 | {file = "websockets-10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb01ea7b5f52e7125bdc3c5807aeaa2d08a0553979cf2d96a8b7803ea33e15e7"}, 655 | {file = "websockets-10.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9fd62c6dc83d5d35fb6a84ff82ec69df8f4657fff05f9cd6c7d9bec0dd57f0f6"}, 656 | {file = "websockets-10.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:3bbf080f3892ba1dc8838786ec02899516a9d227abe14a80ef6fd17d4fb57127"}, 657 | {file = "websockets-10.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:5560558b0dace8312c46aa8915da977db02738ac8ecffbc61acfbfe103e10155"}, 658 | {file = "websockets-10.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:667c41351a6d8a34b53857ceb8343a45c85d438ee4fd835c279591db8aeb85be"}, 659 | {file = "websockets-10.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:468f0031fdbf4d643f89403a66383247eb82803430b14fa27ce2d44d2662ca37"}, 660 | {file = "websockets-10.1-cp310-cp310-win32.whl", hash = "sha256:d0d81b46a5c87d443e40ce2272436da8e6092aa91f5fbeb60d1be9f11eff5b4c"}, 661 | {file = "websockets-10.1-cp310-cp310-win_amd64.whl", hash = "sha256:b68b6caecb9a0c6db537aa79750d1b592a841e4f1a380c6196091e65b2ad35f9"}, 662 | {file = "websockets-10.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:a249139abc62ef333e9e85064c27fefb113b16ffc5686cefc315bdaef3eefbc8"}, 663 | {file = "websockets-10.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8877861e3dee38c8d302eee0d5dbefa6663de3b46dc6a888f70cd7e82562d1f7"}, 664 | {file = "websockets-10.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e3872ae57acd4306ecf937d36177854e218e999af410a05c17168cd99676c512"}, 665 | {file = "websockets-10.1-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:b66e6d514f12c28d7a2d80bb2a48ef223342e99c449782d9831b0d29a9e88a17"}, 666 | {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:9f304a22ece735a3da8a51309bc2c010e23961a8f675fae46fdf62541ed62123"}, 667 | {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:189ed478395967d6a98bb293abf04e8815349e17456a0a15511f1088b6cb26e4"}, 668 | {file = "websockets-10.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:08a42856158307e231b199671c4fce52df5786dd3d703f36b5d8ac76b206c485"}, 669 | {file = "websockets-10.1-cp37-cp37m-win32.whl", hash = "sha256:3ef6f73854cded34e78390dbdf40dfdcf0b89b55c0e282468ef92646fce8d13a"}, 670 | {file = "websockets-10.1-cp37-cp37m-win_amd64.whl", hash = "sha256:89e985d40d407545d5f5e2e58e1fdf19a22bd2d8cd54d20a882e29f97e930a0a"}, 671 | {file = "websockets-10.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:002071169d2e44ce8eb9e5ebac9fbce142ba4b5146eef1cfb16b177a27662657"}, 672 | {file = "websockets-10.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:cfae282c2aa7f0c4be45df65c248481f3509f8c40ca8b15ed96c35668ae0ff69"}, 673 | {file = "websockets-10.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:97b4b68a2ddaf5c4707ae79c110bfd874c5be3c6ac49261160fb243fa45d8bbb"}, 674 | {file = "websockets-10.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c9407719f42cb77049975410490c58a705da6af541adb64716573e550e5c9db"}, 675 | {file = "websockets-10.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:1d858fb31e5ac992a2cdf17e874c95f8a5b1e917e1fb6b45ad85da30734b223f"}, 676 | {file = "websockets-10.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7bdd3d26315db0a9cf8a0af30ca95e0aa342eda9c1377b722e71ccd86bc5d1dd"}, 677 | {file = "websockets-10.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e259be0863770cb91b1a6ccf6907f1ac2f07eff0b7f01c249ed751865a70cb0d"}, 678 | {file = "websockets-10.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:6b014875fae19577a392372075e937ebfebf53fd57f613df07b35ab210f31534"}, 679 | {file = "websockets-10.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:98de71f86bdb29430fd7ba9997f47a6b10866800e3ea577598a786a785701bb0"}, 680 | {file = "websockets-10.1-cp38-cp38-win32.whl", hash = "sha256:3a02ab91d84d9056a9ee833c254895421a6333d7ae7fff94b5c68e4fa8095519"}, 681 | {file = "websockets-10.1-cp38-cp38-win_amd64.whl", hash = "sha256:7d6673b2753f9c5377868a53445d0c321ef41ff3c8e3b6d57868e72054bfce5f"}, 682 | {file = "websockets-10.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ddab2dc69ee5ae27c74dbfe9d7bb6fee260826c136dca257faa1a41d1db61a89"}, 683 | {file = "websockets-10.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:14e9cf68a08d1a5d42109549201aefba473b1d925d233ae19035c876dd845da9"}, 684 | {file = "websockets-10.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e4819c6fb4f336fd5388372cb556b1f3a165f3f68e66913d1a2fc1de55dc6f58"}, 685 | {file = "websockets-10.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:05e7f098c76b0a4743716590bb8f9706de19f1ef5148d61d0cf76495ec3edb9c"}, 686 | {file = "websockets-10.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:5bb6256de5a4fb1d42b3747b4e2268706c92965d75d0425be97186615bf2f24f"}, 687 | {file = "websockets-10.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:888a5fa2a677e0c2b944f9826c756475980f1b276b6302e606f5c4ff5635be9e"}, 688 | {file = "websockets-10.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6fdec1a0b3e5630c58e3d8704d2011c678929fce90b40908c97dfc47de8dca72"}, 689 | {file = "websockets-10.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:531d8eb013a9bc6b3ad101588182aa9b6dd994b190c56df07f0d84a02b85d530"}, 690 | {file = "websockets-10.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:0d93b7cadc761347d98da12ec1930b5c71b2096f1ceed213973e3cda23fead9c"}, 691 | {file = "websockets-10.1-cp39-cp39-win32.whl", hash = "sha256:d9b245db5a7e64c95816e27d72830e51411c4609c05673d1ae81eb5d23b0be54"}, 692 | {file = "websockets-10.1-cp39-cp39-win_amd64.whl", hash = "sha256:882c0b8bdff3bf1bd7f024ce17c6b8006042ec4cceba95cf15df57e57efa471c"}, 693 | {file = "websockets-10.1-pp37-pypy37_pp73-macosx_10_9_x86_64.whl", hash = "sha256:10edd9d7d3581cfb9ff544ac09fc98cab7ee8f26778a5a8b2d5fd4b0684c5ba5"}, 694 | {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:baa83174390c0ff4fc1304fbe24393843ac7a08fdd59295759c4b439e06b1536"}, 695 | {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:483edee5abed738a0b6a908025be47f33634c2ad8e737edd03ffa895bd600909"}, 696 | {file = "websockets-10.1-pp37-pypy37_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:816ae7dac2c6522cfa620947ead0ca95ac654916eebf515c94d7c28de5601a6e"}, 697 | {file = "websockets-10.1-pp37-pypy37_pp73-win_amd64.whl", hash = "sha256:1dafe98698ece09b8ccba81b910643ff37198e43521d977be76caf37709cf62b"}, 698 | {file = "websockets-10.1.tar.gz", hash = "sha256:181d2b25de5a437b36aefedaf006ecb6fa3aa1328ec0236cdde15f32f9d3ff6d"}, 699 | ] 700 | 701 | [[package]] 702 | name = "zipp" 703 | version = "3.7.0" 704 | description = "Backport of pathlib-compatible object wrapper for zip files" 705 | optional = false 706 | python-versions = ">=3.7" 707 | files = [ 708 | {file = "zipp-3.7.0-py3-none-any.whl", hash = "sha256:b47250dd24f92b7dd6a0a8fc5244da14608f3ca90a5efcd37a3b1642fac9a375"}, 709 | {file = "zipp-3.7.0.tar.gz", hash = "sha256:9f50f446828eb9d45b267433fd3e9da8d801f614129124863f9c51ebceafb87d"}, 710 | ] 711 | 712 | [package.extras] 713 | docs = ["jaraco.packaging (>=8.2)", "rst.linker (>=1.9)", "sphinx"] 714 | testing = ["func-timeout", "jaraco.itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.0.1)", "pytest-flake8", "pytest-mypy"] 715 | 716 | [metadata] 717 | lock-version = "2.0" 718 | python-versions = "^3.7" 719 | content-hash = "44cbe47f44e31564215b35c6646fd8ae839b66e0a832ec916f3e76a0eab4d925" 720 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "pytest-pyppeteer" 3 | version = "0.3.1" 4 | description = "A plugin to run pyppeteer in pytest" 5 | authors = ["Luiz Yao "] 6 | readme = "README.md" 7 | license = "MIT" 8 | homepage = "https://github.com/luizyao/pytest-pyppeteer/" 9 | 10 | [tool.poetry.urls] 11 | "Bug Tracker" = "https://github.com/luizyao/pytest-pyppeteer/issues" 12 | Documentation = "https://github.com/luizyao/pytest-pyppeteer/blob/dev/README.md" 13 | Repository = "https://github.com/luizyao/pytest-pyppeteer/" 14 | 15 | [tool.poetry.plugins] 16 | 17 | [tool.poetry.plugins.pytest11] 18 | pyppeteer = "pytest_pyppeteer.plugin" 19 | 20 | [tool.poetry.dependencies] 21 | python = "^3.7" 22 | pyppeteer = "^1.0.2" 23 | pytest = "^6.2.5" 24 | cssselect = "^1.1.0" 25 | lxml = "^4.7.1" 26 | 27 | [tool.poetry.dev-dependencies] 28 | pytest-cov = "^3.0.0" 29 | pre-commit = "^2.17.0" 30 | pytest-asyncio = "^0.17.2" 31 | 32 | [build-system] 33 | requires = ["poetry-core>=1.0.0"] 34 | build-backend = "poetry.core.masonry.api" 35 | -------------------------------------------------------------------------------- /src/pytest_pyppeteer/__init__.py: -------------------------------------------------------------------------------- 1 | """The entrypoint.""" 2 | import logging.config 3 | 4 | __version__ = "0.3.1" 5 | 6 | logging.config.dictConfig( 7 | { 8 | "version": 1, 9 | "disable_existing_loggers": False, 10 | "formatters": { 11 | "verbose": { 12 | "format": "%(asctime)s - %(name)-25s - %(levelname)-8s - %(message)s", 13 | "datefmt": "%Y-%m-%d %H:%M:%S", 14 | } 15 | }, 16 | "handlers": { 17 | "console": { 18 | "level": "DEBUG", 19 | "class": "logging.StreamHandler", 20 | "formatter": "verbose", 21 | "stream": "ext://sys.stdout", 22 | } 23 | }, 24 | "loggers": { 25 | "pytest_pyppeteer": { 26 | "handlers": ["console"], 27 | "level": "DEBUG", 28 | }, 29 | "pytest_pyppeteer.page": { 30 | "handlers": ["console"], 31 | "level": "DEBUG", 32 | "propagate": False, 33 | }, 34 | }, 35 | } 36 | ) 37 | -------------------------------------------------------------------------------- /src/pytest_pyppeteer/page.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from __future__ import annotations 3 | 4 | import asyncio 5 | from logging import getLogger 6 | from typing import TYPE_CHECKING 7 | 8 | import cssselect 9 | from lxml import etree 10 | from pyppeteer.errors import ElementHandleError 11 | 12 | try: 13 | from typing import Literal 14 | except ImportError: 15 | from typing_extensions import Literal 16 | 17 | if TYPE_CHECKING: 18 | from typing import Optional 19 | 20 | from pyppeteer.element_handle import ElementHandle 21 | from pyppeteer.page import Page 22 | 23 | logger = getLogger("pytest_pyppeteer.page") 24 | 25 | 26 | def _parse_locator(css_or_xpath: str) -> tuple: 27 | if not isinstance(css_or_xpath, str): 28 | raise TypeError("Locator {!r} is not a string.".format(css_or_xpath)) 29 | 30 | try: 31 | cssselect.parse(css_or_xpath) 32 | except cssselect.SelectorSyntaxError: 33 | pass 34 | else: 35 | return "css", css_or_xpath 36 | 37 | try: 38 | etree.XPath(css_or_xpath) 39 | except etree.XPathSyntaxError: 40 | pass 41 | else: 42 | return "xpath", css_or_xpath 43 | 44 | raise ValueError("Locator {!r} neither a css nor an xpath string.".format(css_or_xpath)) 45 | 46 | 47 | async def query_locator(self: Page, css_or_xpath: str) -> Optional[ElementHandle]: 48 | _type, css_or_xpath = _parse_locator(css_or_xpath) 49 | if _type == "css": 50 | return await self.querySelector(css_or_xpath) 51 | elif _type == "xpath": 52 | elements = await self.xpath(css_or_xpath) 53 | return elements[0] if elements else None 54 | 55 | 56 | async def waitfor( 57 | self: Page, css_or_xpath: str, visible: bool = True, hidden: bool = False, timeout: int = 30000 58 | ) -> None: 59 | """Wait for an element visible or hidden. 60 | 61 | Args: 62 | css_or_xpath (str): CSS or XPath string. 63 | visible (bool, optional): Waiting for visible. Defaults to True. 64 | hidden (bool, optional): Waiting for hidden. Defaults to False. 65 | timeout (int, optional): Timeout in microsecond. Defaults to 30000. 66 | """ 67 | _type, css_or_xpath = _parse_locator(css_or_xpath) 68 | options = {"visible": visible, "hidden": hidden, "timeout": timeout} 69 | logger.debug("Wait for element {!r} {!r}.".format(css_or_xpath, options)) 70 | if _type == "css": 71 | await self.waitForSelector(css_or_xpath, options=options) 72 | elif _type == "xpath": 73 | await self.waitForXPath(css_or_xpath, options=options) 74 | 75 | 76 | async def click( 77 | self: Page, 78 | css_or_xpath: str, 79 | button: Literal["left", "right", "middle"] = "left", 80 | click_count: int = 1, 81 | delay: int = 0, 82 | ): 83 | """Click an element. 84 | 85 | Args: 86 | css_or_xpath (str): CSS or XPath string. 87 | button (Literal["left", "right", "middle"], optional): Which mouse button to click with. Defaults to "left". 88 | click_count (int, optional): How many clicks. Defaults to 1. 89 | delay (int, optional): Time to wait between ``mousedown`` and ``mouseup`` event in milliseconds. Defaults to 0. 90 | """ 91 | element: Optional[ElementHandle] = await self.query_locator(css_or_xpath) 92 | if element is None: 93 | logger.error("Element {!r} does not exist.".format(css_or_xpath)) 94 | else: 95 | for _ in range(10): 96 | try: 97 | await element._clickablePoint() 98 | except ElementHandleError as error: 99 | logger.debug( 100 | "Element {!r} is not clickable because of {}, waiting... and try again.".format(css_or_xpath, error) 101 | ) 102 | await asyncio.sleep(0.5) 103 | else: 104 | logger.debug("Click element {!r}".format(css_or_xpath)) 105 | await element.click(button=button, clickCount=click_count, delay=delay) 106 | await element.dispose() 107 | break 108 | 109 | 110 | async def type(self: Page, css_or_xpath: str, text: str, delay: int = 0, clear: bool = False): 111 | """Type some text in the element input. 112 | 113 | Args: 114 | css_or_xpath (str): CSS or XPath string. 115 | text (str): Type what. 116 | delay (int, optional): Time to wait between each input event in milliseconds. Defaults to 0. 117 | clear (bool, optional): Whether to clear original content. Defaults to False. 118 | """ 119 | element: Optional[ElementHandle] = await self.query_locator(css_or_xpath) 120 | if element is None: 121 | logger.error("Element {!r} does not exist.".format(css_or_xpath)) 122 | else: 123 | logger.debug("Type text {!r} into element {!r}.".format(text, css_or_xpath)) 124 | if clear: 125 | value = await element.executionContext.evaluate("(node => node.value || node.innerText)", element) 126 | if value: 127 | await element.click() 128 | for _ in value: 129 | await self.keyboard.press("ArrowRight") 130 | await self.keyboard.down("Shift") 131 | for _ in value: 132 | await self.keyboard.press("ArrowLeft") 133 | await asyncio.sleep(delay / 1000.0) 134 | await self.keyboard.up("Shift") 135 | await self.keyboard.press("Backspace") 136 | await element.type(text, delay=delay) 137 | await element.dispose() 138 | 139 | 140 | async def get_value(self: Page, css_or_xpath: str) -> Optional[str]: 141 | """Get an element value. 142 | 143 | Args: 144 | css_or_xpath (str): CSS or XPath string. 145 | 146 | Returns: 147 | Optional[str]: The element value. 148 | """ 149 | element: Optional[ElementHandle] = await self.query_locator(css_or_xpath) 150 | if element is None: 151 | logger.error("Element {!r} does not exist.".format(css_or_xpath)) 152 | else: 153 | value: str = await element.executionContext.evaluate("(node => node.value || node.innerText)", element) 154 | await element.dispose() 155 | return value 156 | -------------------------------------------------------------------------------- /src/pytest_pyppeteer/plugin.py: -------------------------------------------------------------------------------- 1 | # flake8: noqa 2 | from __future__ import annotations 3 | 4 | import sys 5 | from functools import reduce 6 | from inspect import getmembers, getmodule, isfunction 7 | from logging import getLogger 8 | from types import MethodType 9 | from typing import TYPE_CHECKING 10 | 11 | import pytest 12 | from _pytest.mark import Mark 13 | from pyppeteer.launcher import launch 14 | 15 | from pytest_pyppeteer import page 16 | 17 | if TYPE_CHECKING: 18 | from typing import Callable, List, Optional 19 | 20 | from _pytest.config import Config as PytestConfig 21 | from _pytest.config.argparsing import Parser 22 | from _pytest.fixtures import FixtureRequest 23 | from pyppeteer.browser import Browser 24 | from pyppeteer.page import Page 25 | 26 | _page_functions = getmembers( 27 | page, lambda value: isfunction(value) and getmodule(value) is page and not value.__name__.startswith("_") 28 | ) 29 | 30 | _chrome_executable_path = { 31 | "macos": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", 32 | "win64": "C:/Program Files/Google/Chrome/Application/chrome.exe", 33 | "win32": "C:/Program Files (x86)/Google/Chrome/Application/chrome.exe", 34 | } 35 | 36 | logger = getLogger("pytest_pyppeteer") 37 | 38 | 39 | async def new_page(self: Browser) -> Page: 40 | pyppeteer_page = await self.newPage() 41 | dimensions = await pyppeteer_page.evaluate( 42 | """() => { 43 | return { 44 | width: window.outerWidth, 45 | height: window.outerHeight, 46 | deviceScaleFactor: window.devicePixelRatio, 47 | } 48 | }""" 49 | ) 50 | logger.debug("Resize the page viewport to {!r}.".format(dimensions)) 51 | await pyppeteer_page.setViewport(dimensions) 52 | 53 | for name, func in _page_functions: 54 | logger.debug("Bind method {!r} to object {!r}.".format(name, pyppeteer_page)) 55 | setattr(pyppeteer_page, name, MethodType(func, pyppeteer_page)) 56 | return pyppeteer_page 57 | 58 | 59 | def pytest_configure(config: PytestConfig) -> None: 60 | config.addinivalue_line("markers", "options(kwargs): update browser initial options.") 61 | 62 | 63 | def pytest_addoption(parser: Parser) -> None: 64 | group = parser.getgroup("pyppeteer") 65 | group.addoption("--executable-path", default=None, help="Path to a Chromium or Chrome executable.") 66 | group.addoption("--headless", action="store_true", help="Run browser in headless mode.") 67 | group.addoption("--args", action="append", nargs="+", default=list(), help="Additional args passed to the browser.") 68 | group.addoption("--window-size", nargs=2, default=["800", "600"], help="Set the initial browser window size.") 69 | group.addoption("--slow", type=float, default=0.0, help="Slow down the operate in milliseconds.") 70 | 71 | 72 | @pytest.fixture(scope="session") 73 | def platform() -> Optional[str]: 74 | """Current platform. e.g. macos, win32, win64.""" 75 | if sys.platform.startswith("darwin"): 76 | return "macos" 77 | elif sys.platform.startswith("win"): 78 | return "win64" if sys.maxsize > 2 ** 32 else "win32" 79 | else: 80 | logger.error( 81 | "Platform {!r} is unsupported. Currently only support {!r}.".format( 82 | sys.platform, tuple(_chrome_executable_path.keys()) 83 | ) 84 | ) 85 | return None 86 | 87 | 88 | @pytest.fixture(scope="session") 89 | def default_executable_path(platform: Optional[str]) -> Optional[str]: 90 | """The default install path for Chrome browser in the current platform.""" 91 | return _chrome_executable_path.pop(platform, None) 92 | 93 | 94 | @pytest.fixture(scope="session") 95 | def executable_path(pytestconfig: PytestConfig, default_executable_path: Optional[str]) -> Optional[str]: 96 | """The Chrome executable path for this test run.""" 97 | return pytestconfig.getoption("executable_path") or default_executable_path 98 | 99 | 100 | @pytest.fixture(scope="session") 101 | def args(pytestconfig: PytestConfig) -> List[str]: 102 | """The extra args used to initialize Chrome browser.""" 103 | return ["--" + "=".join(arg) for arg in pytestconfig.getoption("args")] 104 | 105 | 106 | @pytest.fixture(scope="session") 107 | def session_options( 108 | pytestconfig: PytestConfig, platform: Optional[str], executable_path: Optional[str], args: List[str] 109 | ) -> dict: 110 | """The default options used to initialize Chrome browser.""" 111 | headless: bool = pytestconfig.getoption("headless") 112 | width, height = pytestconfig.getoption("window_size") 113 | slow: float = pytestconfig.getoption("slow") 114 | 115 | if width == height == "0": 116 | args.append("--start-fullscreen" if platform == "macos" else "--start-maximized") 117 | else: 118 | args.append("--window-size={},{}".format(width, height)) 119 | 120 | return dict(headless=headless, slowMo=slow, args=args, executable_path=executable_path) 121 | 122 | 123 | @pytest.fixture 124 | def options(request: FixtureRequest, session_options: dict) -> dict: 125 | """The final options used to initialize Chrome browser.""" 126 | options_mark: Mark = reduce( 127 | lambda mark1, mark2: mark1.combined_with(mark2), 128 | request.node.iter_markers("options"), 129 | Mark("options", args=tuple(), kwargs=dict()), 130 | ) 131 | return dict(session_options, **options_mark.kwargs) 132 | 133 | 134 | @pytest.fixture 135 | async def browser_factory(options: dict) -> Callable[..., Browser]: 136 | """Provide to create a new browser instance.""" 137 | browsers: List[Browser] = list() 138 | 139 | async def _factory() -> Browser: 140 | logger.debug("The options used to initialize the browser: {!r}".format(options)) 141 | browser = await launch(**options) 142 | # logger.debug("Bind new method {!r} to browser instance {!r}.".format(new_page, browser)) 143 | browser.new_page = MethodType(new_page, browser) 144 | browsers.append(browser) 145 | return browser 146 | 147 | yield _factory 148 | 149 | for browser in browsers: 150 | await browser.close() 151 | 152 | 153 | @pytest.fixture 154 | async def browser(browser_factory: Callable[..., Browser]) -> Browser: 155 | """Provide a new browser instance.""" 156 | yield await browser_factory() 157 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | pytest_plugins = "pytester" 2 | -------------------------------------------------------------------------------- /tests/pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | asyncio_mode=auto 3 | -------------------------------------------------------------------------------- /tests/test_pytest_pyppeteer.py: -------------------------------------------------------------------------------- 1 | import pytest 2 | from _pytest.pytester import Pytester 3 | 4 | from pytest_pyppeteer.page import _parse_locator 5 | 6 | 7 | @pytest.mark.parametrize( 8 | "css_or_xpath, expected_type", 9 | [ 10 | ("foo", "css"), 11 | ("foo#bar", "css"), 12 | ("#bar", "css"), 13 | ("foo.bar", "css"), 14 | (".bar", "css"), 15 | ("//foo", "xpath"), 16 | ("//foo[@id = 'bar']", "xpath"), 17 | ("//*[@id = 'bar']", "xpath"), 18 | ("//foo[@class and contains(concat(' ', normalize-space(@class), ' '), ' bar ')]", "xpath"), 19 | ("//*[@class and contains(concat(' ', normalize-space(@class), ' '), ' bar ')]", "xpath"), 20 | ], 21 | ) 22 | def test_locator_valid_content(css_or_xpath, expected_type): 23 | _type, css_or_xpath = _parse_locator(css_or_xpath) 24 | assert _type == expected_type, "Locator {!r} should be parsed to type {!r}.".format(css_or_xpath, expected_type) 25 | 26 | 27 | @pytest.mark.parametrize("css_or_xpath", ["##foo", "//foo??", "...."]) 28 | def test_locator_invalid_content(css_or_xpath): 29 | with pytest.raises(ValueError, match=css_or_xpath): 30 | _parse_locator(css_or_xpath) 31 | 32 | 33 | @pytest.mark.parametrize("css_or_xpath", [123, 123.0, tuple(), list()]) 34 | def test_locator_unsupported_content(css_or_xpath): 35 | with pytest.raises(TypeError, match="not a string"): 36 | _parse_locator(css_or_xpath) 37 | 38 | 39 | def test_platform(platform): 40 | assert platform is not None 41 | 42 | 43 | def test_platform_unknown(pytester: Pytester): 44 | pytester.makeconftest( 45 | """ 46 | import pytest 47 | 48 | @pytest.fixture(scope="session") 49 | def platform(): 50 | return "unknown" 51 | """ 52 | ) 53 | 54 | testcase_path = pytester.makepyfile( 55 | """ 56 | def test_default_executable_path(default_executable_path): 57 | assert default_executable_path is None 58 | 59 | def test_executable_path(executable_path): 60 | assert executable_path is None 61 | """ 62 | ) 63 | result = pytester.runpytest_inprocess(testcase_path) 64 | 65 | result.assert_outcomes(passed=2) 66 | 67 | 68 | def test_executable_path_default(default_executable_path): 69 | assert default_executable_path is not None 70 | 71 | 72 | def test_executable_path(default_executable_path, executable_path): 73 | assert default_executable_path == executable_path 74 | 75 | 76 | def test_executable_path_commandoption(pytester: Pytester): 77 | testcase_path = pytester.makepyfile( 78 | """ 79 | def test_executable_path(executable_path, default_executable_path): 80 | assert executable_path != default_executable_path 81 | assert executable_path == "path/to/chrome" 82 | """ 83 | ) 84 | result = pytester.runpytest_inprocess(testcase_path, "--executable-path", "path/to/chrome") 85 | 86 | result.assert_outcomes(passed=1) 87 | 88 | 89 | def test_args_default(args): 90 | assert isinstance(args, list) and len(args) == 0 91 | 92 | 93 | def test_args_commandoption(pytester: Pytester): 94 | testcase_path = pytester.makepyfile( 95 | """ 96 | def test_args(args): 97 | assert args == ["--proxy-server=localhost:5555,direct://", "--proxy-bypass-list=192.0.0.1/8;10.0.0.1/8"] 98 | """ 99 | ) 100 | 101 | result = pytester.runpytest_inprocess( 102 | testcase_path, 103 | "--args", 104 | "proxy-server", 105 | "localhost:5555,direct://", 106 | "--args", 107 | "proxy-bypass-list", 108 | "192.0.0.1/8;10.0.0.1/8", 109 | ) 110 | 111 | result.assert_outcomes(passed=1) 112 | 113 | 114 | def test_options_default(options, executable_path): 115 | assert options["headless"] is False 116 | assert options["slowMo"] == 0.0 117 | assert options["executable_path"] == executable_path 118 | assert options["args"] == ["--window-size=800,600"] 119 | 120 | 121 | def test_options_updating(pytester: Pytester): 122 | testcase_path = pytester.makepyfile( 123 | """ 124 | import pytest 125 | 126 | @pytest.mark.options(devtools=True, headless=True) 127 | def test_options(options): 128 | assert options["devtools"] is True 129 | assert options["headless"] is True 130 | """ 131 | ) 132 | 133 | result = pytester.runpytest_inprocess(testcase_path) 134 | 135 | result.assert_outcomes(passed=1) 136 | 137 | 138 | @pytest.mark.asyncio 139 | async def test_scenario(browser): 140 | page = await browser.new_page() 141 | await page.goto("http://www.example.com") 142 | assert (await page.get_value("body > div > h1")) == "Example Domain" 143 | --------------------------------------------------------------------------------