├── .dockerignore
├── .github
├── pull_request_template.md
└── workflows
│ ├── close-inactive-issues.yml
│ ├── docs.yml
│ ├── lint.yml
│ ├── publish.yml
│ └── test.yml
├── .gitignore
├── CHANGELOG.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── docker-compose.yml
├── docs
├── index.md
├── quickstart.md
├── stylesheets
│ └── extra.css
└── tutorials
│ ├── account-creation.md
│ ├── api-responses.md
│ ├── great-power.jpg
│ ├── index.md
│ ├── infinite-scrolling.md
│ └── tutorial-code
│ ├── account-creation-1.py
│ ├── account-creation-2.py
│ ├── api-responses-1.py
│ ├── api-responses-2.py
│ ├── infinite-scrolling-1.py
│ ├── infinite-scrolling-2.py
│ └── infinite-scrolling-3.py
├── examples
├── browserscan.py
├── demo.py
├── expect_download.py
├── fetch_domain.py
├── imgur_upload_image.py
├── mouse_drag_boxes.py
├── network_monitor.py
├── set_user_agent.py
└── wait_for_page.py
├── mkdocs.yml
├── pyproject.toml
├── scripts
├── format.sh
├── generate_cdp.py
├── lint.sh
├── mkdocs_generate_all.sh
├── mkdocs_generate_index.py
├── mkdocs_generate_reference.py
├── mkdocs_generate_release_notes.py
├── release.py
└── test.sh
├── tests
├── Dockerfile
├── __init__.py
├── bot_detection
│ ├── __init__.py
│ └── test_browserscan.py
├── conftest.py
├── core
│ ├── __init__.py
│ ├── test_browser.py
│ ├── test_multiple_browsers.py
│ └── test_tab.py
├── docs
│ ├── __init__.py
│ ├── conftest.py
│ └── tutorials
│ │ ├── __init__.py
│ │ ├── test_account_creation_tutorial.py
│ │ ├── test_api_responses_tutorial.py
│ │ └── test_infinite_scrolling_tutorial.py
├── next_test.sh
└── sample_data
│ ├── __init__.py
│ └── groceries.html
├── uv.lock
└── zendriver
├── __init__.py
├── _version.py
├── cdp
├── README.md
├── __init__.py
├── accessibility.py
├── animation.py
├── audits.py
├── autofill.py
├── background_service.py
├── bluetooth_emulation.py
├── browser.py
├── cache_storage.py
├── cast.py
├── console.py
├── css.py
├── database.py
├── debugger.py
├── device_access.py
├── device_orientation.py
├── dom.py
├── dom_debugger.py
├── dom_snapshot.py
├── dom_storage.py
├── emulation.py
├── event_breakpoints.py
├── extensions.py
├── fed_cm.py
├── fetch.py
├── file_system.py
├── headless_experimental.py
├── heap_profiler.py
├── indexed_db.py
├── input_.py
├── inspector.py
├── io.py
├── layer_tree.py
├── log.py
├── media.py
├── memory.py
├── network.py
├── overlay.py
├── page.py
├── performance.py
├── performance_timeline.py
├── preload.py
├── profiler.py
├── pwa.py
├── py.typed
├── runtime.py
├── schema.py
├── security.py
├── service_worker.py
├── storage.py
├── system_info.py
├── target.py
├── tethering.py
├── tracing.py
├── util.py
├── web_audio.py
└── web_authn.py
├── core
├── _contradict.py
├── browser.py
├── config.py
├── connection.py
├── element.py
├── expect.py
├── tab.py
└── util.py
└── py.typed
/.dockerignore:
--------------------------------------------------------------------------------
1 | # Python
2 | __pycache__
3 | app.egg-info
4 | *.pyc
5 | .mypy_cache
6 | .ruff_cache
7 | .coverage
8 | htmlcov
9 | .venv
10 |
--------------------------------------------------------------------------------
/.github/pull_request_template.md:
--------------------------------------------------------------------------------
1 | ## Description
2 |
3 |
4 |
5 | ## Pre-merge Checklist
6 |
7 |
12 |
13 | - [ ] I have described my change in the section above.
14 | - [ ] I have ran the [`./scripts/format.sh`](https://github.com/stephanlensky/zendriver/blob/main/scripts/format.sh) and [`./scripts/lint.sh`](https://github.com/stephanlensky/zendriver/blob/main/scripts/lint.sh) scripts. My code is properly formatted and has no linting errors.
15 | - [ ] I have added my change to [CHANGELOG.md](https://github.com/stephanlensky/zendriver/blob/main/CHANGELOG.md) under the `[Unreleased]` section.
16 |
--------------------------------------------------------------------------------
/.github/workflows/close-inactive-issues.yml:
--------------------------------------------------------------------------------
1 | name: Close inactive issues
2 | on:
3 | schedule:
4 | - cron: "30 1 * * *"
5 | workflow_dispatch:
6 | jobs:
7 | close-issues:
8 | runs-on: ubuntu-latest
9 | permissions:
10 | issues: write
11 | pull-requests: write
12 | steps:
13 | - uses: actions/stale@v9
14 | with:
15 | days-before-issue-stale: 30
16 | days-before-issue-close: 7
17 | stale-issue-label: "stale"
18 | stale-issue-message: "This issue has been marked stale because it has been open for 30 days with no activity. If there is no activity within 7 days, it will be automatically closed."
19 | close-issue-message: "This issue was automatically closed because it has been inactive for 7 days since being marked as stale."
20 | only-issue-labels: "question"
21 | days-before-pr-stale: -1
22 | days-before-pr-close: -1
23 | repo-token: ${{ secrets.GITHUB_TOKEN }}
24 |
--------------------------------------------------------------------------------
/.github/workflows/docs.yml:
--------------------------------------------------------------------------------
1 | name: docs
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - "docs/**"
8 | # Files which are used during doc generation
9 | - "README.md"
10 | - "CHANGELOG.md"
11 | - "zendriver/cdp/**"
12 | workflow_dispatch:
13 | permissions:
14 | contents: write
15 | jobs:
16 | docs:
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v4
20 | - name: Configure Git Credentials
21 | run: |
22 | git config user.name github-actions[bot]
23 | git config user.email 41898282+github-actions[bot]@users.noreply.github.com
24 | - name: Install uv
25 | uses: astral-sh/setup-uv@v3
26 | - name: Set up Python
27 | run: uv python install
28 | - name: Install dependencies
29 | run: uv sync --all-extras --dev
30 | - name: Auto-generate docs
31 | run: |
32 | uv run python scripts/mkdocs_generate_reference.py
33 | uv run python scripts/mkdocs_generate_release_notes.py
34 | - name: Deploy to GitHub Pages
35 | run: uv run mkdocs gh-deploy --force
36 |
--------------------------------------------------------------------------------
/.github/workflows/lint.yml:
--------------------------------------------------------------------------------
1 | name: lint
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - "**.py"
8 | pull_request:
9 | branches:
10 | - main
11 | paths:
12 | - "**.py"
13 | jobs:
14 | lint:
15 | runs-on: ubuntu-latest
16 | steps:
17 | - uses: actions/checkout@v4
18 | - name: Install uv
19 | uses: astral-sh/setup-uv@v3
20 | - name: Set up Python
21 | run: uv python install
22 | - name: Install dependencies
23 | run: uv sync --all-extras --dev
24 | - name: Lint with ruff/mypy
25 | run: /bin/bash ./scripts/lint.sh
26 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: publish
2 | on:
3 | push:
4 | tags:
5 | - "*"
6 | workflow_dispatch:
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v4
12 | - name: Install uv
13 | uses: astral-sh/setup-uv@v3
14 | - name: Set up Python
15 | run: uv python install
16 | - name: Build wheel
17 | run: uv build
18 | - name: Publish to PyPI
19 | run: uv publish
20 | env:
21 | UV_PUBLISH_TOKEN: ${{ secrets.UV_PUBLISH_TOKEN }}
22 |
--------------------------------------------------------------------------------
/.github/workflows/test.yml:
--------------------------------------------------------------------------------
1 | name: test
2 | on:
3 | push:
4 | branches:
5 | - main
6 | paths:
7 | - "**.py"
8 | pull_request:
9 | branches:
10 | - main
11 | paths:
12 | - "**.py"
13 | workflow_dispatch:
14 | jobs:
15 | test:
16 | runs-on: ${{ matrix.os }}
17 | strategy:
18 | matrix:
19 | os: [ ubuntu-latest, windows-latest ]
20 | steps:
21 | - uses: actions/checkout@v4
22 | - name: Install uv
23 | uses: astral-sh/setup-uv@v3
24 | - name: Set up Python
25 | run: uv python install
26 | - name: Install dependencies
27 | run: uv sync --all-extras --dev
28 | - name: Run tests
29 | env:
30 | ZENDRIVER_TEST_BROWSERS: "headless"
31 | ZENDRIVER_TEST_NO_SANDBOX: "true"
32 | shell: bash
33 | run: ./scripts/test.sh
34 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # mkdocs build
2 | site/
3 |
4 | # auto-generated docs
5 | docs/reference
6 | docs/release-notes.md
7 |
8 | # uv/pyenv
9 | .python-version
10 |
11 | # scratch files
12 | scratch/
13 |
14 | # Byte-compiled / optimized / DLL files
15 | __pycache__/
16 | *.py[cod]
17 | *$py.class
18 |
19 | # C extensions
20 | *.so
21 |
22 | # Distribution / packaging
23 | .Python
24 | build/
25 | develop-eggs/
26 | dist/
27 | eggs/
28 | .eggs/
29 | sdist/
30 | wheels/
31 | share/python-wheels/
32 | *.egg-info/
33 | .installed.cfg
34 | *.egg
35 | MANIFEST
36 |
37 | # PyInstaller
38 | # Usually these files are written by a python script from a template
39 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
40 | *.manifest
41 | *.spec
42 |
43 |
44 | # Unit test / coverage reports
45 | htmlcov/
46 | .tox/
47 | .nox/
48 | .coverage
49 | .coverage.*
50 | .cache
51 | nosetests.xml
52 | coverage.xml
53 | *.cover
54 | *.py,cover
55 | .hypothesis/
56 | .pytest_cache/
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 |
65 | # Sphinx documentation
66 |
67 | !docs/_build/html
68 | !docs/_build/markdown
69 |
70 |
71 |
72 | # IPython
73 | profile_default/
74 | ipython_config.py
75 |
76 | # Environments
77 | .env
78 | .venv
79 | env/
80 | venv/
81 | ENV/
82 | env.bak/
83 | venv.bak/
84 | /docs/_build/doctrees/
85 |
86 | # VisualStudioCode
87 | .vscode
88 | .history
89 |
90 | # Pycharm
91 | /.idea
92 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | All notable changes to this project will be documented in this file.
4 |
5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7 |
8 | ## [Unreleased]
9 |
10 | ### Fixed
11 |
12 | ### Added
13 |
14 | ### Changed
15 |
16 | ### Removed
17 |
18 | ## [0.8.0] - 2025-06-01
19 |
20 | ### Fixed
21 |
22 | - Fixed tests so that they can run on Windows (and still run on Linux like before) @nathanfallet
23 | - Remove usage of asyncio subprocess for better compatibility on Windows @nathanfallet
24 | - Added a missing Chrome Canary path for Windows @nathanfallet
25 | - Added a flag to re-enable `--load-extension` (disabled by default in Chrome 136+) @nathanfallet
26 |
27 | ## [0.7.1] - 2025-05-08
28 |
29 | ### Changed
30 |
31 | - Updated CDP models @jsuarezl
32 |
33 | ## [0.7.0] - 2025-04-28
34 |
35 | ### Added
36 |
37 | - Added `Tab.screenshot_b64` and `Element.screenshot_b64` methods to return screenshot as base64 string @falmar
38 | - Added `Tab.print_to_pdf` to print the current page to a PDF file @stephanlensky
39 |
40 | ## [0.6.1] - 2025-04-25
41 |
42 | ### Fixed
43 |
44 | - Fix race condition in `Browser.get` and `Tab.close` which could cause exceptions, especially when running multiple browsers in parallel @stephanlensky
45 |
46 | ## [0.6.0] - 2025-04-20
47 |
48 | ### Fixed
49 |
50 | - `Browser.get` and `Tab.close` will now wait for their appropiate target events before returning @ccev
51 |
52 | ### Added
53 |
54 | - Added `Tab.save_snapshot` to export the current page to MHTML format.
55 |
56 | ## [0.5.2] - 2025-04-09
57 |
58 | ### Fixed
59 |
60 | - Fixed type annotation of `Element.children` @stephanlensky
61 |
62 | ## [0.5.1] - 2025-02-16
63 |
64 | ### Changed
65 |
66 | - Deprecated `zendriver.loop()` function. You should instead use `asyncio` functions directly, for example:
67 |
68 | ```python
69 | asyncio.run(your_main_method())
70 | ```
71 |
72 | ## [0.5.0] - 2025-02-16
73 |
74 | ### Added
75 |
76 | - Add `tab.expect_download` methods to wait for download file @3mora2
77 |
78 | ## [0.4.3] - 2025-02-11
79 |
80 | ### Added
81 |
82 | - Add logs for Chrome process output on connection failure @stephanlensky
83 |
84 | ### Changed
85 |
86 | - Default and launch changed to use `about:blank` (faster start and less bandwidth) @raycardillo
87 |
88 | ## [0.4.2] - 2025-02-11
89 |
90 | ### Fixed
91 |
92 | - Multiple Browsers can be created without one affecting the other @raycardillo
93 |
94 | ## [0.4.1] - 2025-02-09
95 |
96 | ### Fixed
97 |
98 | - Ignore irrelevant `DOM.disable` errors @raycardillo
99 | - Test scripts improved for running on macOS @raycardillo
100 |
101 | ## [0.4.0] - 2025-02-06
102 |
103 | ### Added
104 |
105 | - Add `tab.expect_request` and `tab.expect_response` methods to wait for a specific request or response @3mora2
106 | - Add `tab.wait_for_ready_state` method for to wait for page to load @3mora2
107 | - Add `tab.remove_handlers` method for removing handlers @khamaileon
108 | - Clean up temporary profiles when `Browser.stop()` is called @barrycarey
109 |
110 | ## [0.3.1] - 2025-01-28
111 |
112 | ### Fixed
113 |
114 | - Fixed bug in `find`/`find_element_by_text` which caused `ProtocolException` when no results were found @stephanlensky
115 |
116 | ## [0.3.0] - 2025-01-25
117 |
118 | ### Fixed
119 |
120 | - Added `Tab.set_user_agent()` function for programmatically configuring the user-agent, language, and platform @stephanlensky
121 | - Improved a few type annotations (`Connection.send()` function now returns correctly typed values based on the provided `cdp_obj`) @stephanlensky
122 |
123 | ## [0.2.3] - 2024-12-14
124 |
125 | ### Fixed
126 |
127 | - Fixed mypy linting errors (attempt 2) @stephanlensky
128 |
129 | ### Added
130 |
131 | - Handle browser process shutdown on 'Failed to connect to browser' @desoul99
132 | - Added configurable browser connection timeout and tries @desoul99
133 |
134 | ## [0.2.2] - 2024-11-23
135 |
136 | ### Fixed
137 |
138 | - Fix `AttributeError: 'tuple' object has no attribute 'value'` error in `connection.py` when using headless browser, @slimshreydy
139 |
140 | ## [0.2.1] - 2024-11-23
141 |
142 | ### Added
143 |
144 | - Add automated testing framework! @stephanlensky
145 | - For now, just a few tests are written, including one to test browserscan.com bot detection
146 | - In the future, we can expand this test suite further (see [Zendriver#18](https://github.com/stephanlensky/zendriver/issues/18))
147 | - Add return type annotation to `Tab.get_content()` @stephanlensky
148 |
149 | ### Changed
150 |
151 | - Upgraded `websockets` to latest version (`>=14.0`) @yoori @stephanlensky
152 |
153 | ## [0.2.0] - 2024-11-17
154 |
155 | ### Changed
156 |
157 | - Updated CDP models @stephanlensky
158 |
159 | ## [0.1.5] - 2024-11-17
160 |
161 | ### Fixed
162 |
163 | - Reverted non-functional fixes for mypy linting errors (oops) @stephanlensky
164 |
165 | ## [0.1.4] - 2024-11-17
166 |
167 | ### Fixed
168 |
169 | - Fixed a large number of mypy linting errors (should not result in any functional change) @stephanlensky
170 |
171 | ### Added
172 |
173 | - Added `zendriver.__version__` attribute to get current package version at runtime @stephanlensky
174 |
175 | ## [0.1.3] - 2024-11-12
176 |
177 | ### Added
178 |
179 | - Added support for `DOM.scrollableFlagUpdated` experimental CDP event. @michaellee94
180 |
181 | ## [0.1.2] - 2024-11-11
182 |
183 | ### Fixed
184 |
185 | - Pinned requirement `websockets<14`, fixing the `AttributeError: 'NoneType' object has no attribute 'closed'` crash which occurs on the latest version of `websockets`. @stephanlensky
186 | - Fixed incorrect `browser.close()` method in examples and documentation -- the correct method is `browser.stop()`. @stephanlensky
187 | - Fixed `atexit` handler to correctly handle async `browser.stop()` method. @stephanlensky
188 |
189 | ## [0.1.1] - 2024-10-29
190 |
191 | ### Added
192 |
193 | - Support for Python 3.10 and Python 3.11. All versions >=3.10 are now supported. @stephanlensky
194 |
195 | ## [0.1.0] - 2024-10-20
196 |
197 | Initial version, forked from [ultrafunkamsterdam/nodriver@`1bb6003`](https://github.com/ultrafunkamsterdam/nodriver/commit/1bb6003c7f0db4d3ec05fdf3fc8c8e0804260103) with a variety of improvements.
198 |
199 | ### Fixed
200 |
201 | - `Browser.set_all` cookies function now correctly uses provided cookies @ilkecan
202 | - "successfully removed temp profile" message printed on exit is now only shown only when a profile was actually removed. Message is now logged at debug level instead of printed. @mreiden @stephanlensky
203 | - Fix crash on starting browser in headless mode @ilkecan
204 | - Fix `Browser.stop()` method to give the browser instance time to shut down before force killing @stephanlensky
205 | - Many `ruff` lint issues @stephanlensky
206 |
207 | ### Added
208 |
209 | - Support for linting with `ruff` and `mypy`. All `ruff` lints are fixed in the initial release, but many `mypy` issues remain to be fixed at a later date. @stephanlensky
210 | - `py.typed` marker so importing as a library in other packages no longer causes `mypy` errors. @stephanlensky
211 |
212 | ### Changed
213 |
214 | - Project is now built with [`uv`](https://github.com/astral-sh/uv). Automatically install dependencies to a venv with `uv sync`, run commands from the venv with `uv run`, and build the project with `uv build`. See the official [`uv` docs](https://docs.astral.sh/uv/) for more information. @stephanlensky
215 | - Docs migrated from sphinx to [mkdocs-material](https://squidfunk.github.io/mkdocs-material/). @stephanlensky
216 | - `Browser.stop()` is now async (so it must be `await`ed) @stephanlensky
217 |
218 | ### Removed
219 |
220 | - Twitter account creation example @stephanlensky
221 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Zendriver ✌️
2 |
3 | > This package is a fork of [`ultrafunkamsterdam/nodriver`](https://github.com/ultrafunkamsterdam/nodriver/), created to add new features, compile unmerged bugfixes, and increase community engagement.
4 |
5 | **Documentation:** [https://slensky.com/zendriver](https://slensky.com/zendriver)
6 |
7 | Zendriver is a blazing fast, async-first, undetectable webscraping/web automation framework implemented using the Chrome Devtools Protocol. Visit websites, scrape content, and run JavaScript using a real browser (no Selenium/Webdriver) all with just a few lines of Python.
8 |
9 | **Docker support is here!** Check out [`stephanlensky/zendriver-docker`](https://github.com/stephanlensky/zendriver-docker) for an example of how to run Zendriver with a real, GPU-accelerated browser (not headless) in a Docker container. (Linux-only)
10 |
11 | ## Features
12 |
13 | - **Undetectable** - Zendriver uses the Chrome Devtools Protocol instead of Selenium/WebDriver, making it (almost) impossible to detect
14 | - **Blazing fast** - Chrome Devtools Protocol is _fast_, much faster than previous Selenium/WebDriver solutions. CDP combined with an async Python API makes Zendriver highly performant.
15 | - **Feature complete and easy to use** - Packed with allowing you to get up and running in just a few lines of code.
16 | - **First-class Docker support** - Traditionally, browser automation has been incredibly difficult to package with Docker, especially if you want to run real, GPU-accelerated Chrome (not headless). Now, deploying with Docker is easier than ever using the officially supported [zendriver-docker project template](https://github.com/stephanlensky/zendriver-docker).
17 | - **Automatic cookie and profile management** - By default, uses fresh profile on each run, cleaning up on exit. Or, save and load cookies to a file to avoid repeating tedious login steps.
18 | - **Smart element lookup** - Find elements selector or text, including iframe content. This could also be used as wait condition for a element to appear, since it will retry for the duration of `timeout` until found. Single element lookup by text using `tab.find()` accepts a `best_match flag`, which will not naively return the first match, but will match candidates by closest matching text length.
19 | - **Easy debugging** - Descriptive `repr` for elements, which represents the element as HTML, makes debugging much easier.
20 |
21 | ## Installation
22 |
23 | To install, simply use `pip` (or your favorite package manager):
24 |
25 | ```sh
26 | pip install zendriver
27 | # or uv add zendriver, poetry add zendriver, etc.
28 | ```
29 |
30 | ## Usage
31 |
32 | Example for visiting [https://www.browserscan.net/bot-detection](https://www.browserscan.net/bot-detection) and saving a screenshot of the results:
33 |
34 | ```python
35 | import asyncio
36 |
37 | import zendriver as zd
38 |
39 |
40 | async def main():
41 | browser = await zd.start()
42 | page = await browser.get("https://www.browserscan.net/bot-detection")
43 | await page.save_screenshot("browserscan.png")
44 | await browser.stop()
45 |
46 |
47 | if __name__ == "__main__":
48 | asyncio.run(main())
49 | ```
50 |
51 | Check out the [Quickstart](https://slensky.com/zendriver/quickstart/) for more information and examples.
52 |
53 | ## Rationale for the fork
54 |
55 | Zendriver remains committed to `nodriver`'s goals of staying undetected for all modern anti-bot solutions and also keeps with the batteries-included approach of its predecessor. Unfortunately, contributions to the original [`nodriver` repo](https://github.com/ultrafunkamsterdam/nodriver/) are heavily restricted, making it difficult to submit issues or pull requests. At the time of writing, there are several pull requests open to fix critical bugs which have beeen left unaddressed for many months.
56 |
57 | Zendriver aims to change this by:
58 |
59 | 1. Including open pull requests in the original `nodriver` repo as part of the initial release
60 | 2. Modernizing the development process to include static analysis tools such as [`ruff`](https://docs.astral.sh/ruff/) and [`mypy`](https://mypy-lang.org/), reducing the number of easy-to-catch bugs which make it through in the future
61 | 3. Opening up the issue tracker and pull requests for community contributions, allowing the project to continue to grow along with its community.
62 |
63 | With these changes in place, we hope to further development of state-of-the-art open-source web automation tools even further, helping to once again make the web truly open for all.
64 |
65 | ## Contributing
66 |
67 | Contributions of all types are always welcome! Please see [CONTRIBUTING.md](https://github.com/stephanlensky/zendriver/blob/main/CONTRIBUTING.md) for details on how to contribute.
68 |
69 | ### Getting additional help
70 |
71 | If you have a question, bug report, or want to make a general inquiry about the project, please create a new GitHub issue. If you are having a problem with Zendriver, please make sure to include your operating system, Chrome version, code example demonstrating the issue, and any other information that may be relevant.
72 |
73 | Questions directed to any personal accounts outside of GitHub will be ignored.
74 |
--------------------------------------------------------------------------------
/docker-compose.yml:
--------------------------------------------------------------------------------
1 | services:
2 | test:
3 | build:
4 | context: .
5 | dockerfile: tests/Dockerfile
6 | ports:
7 | - 5911:5911 # VNC server port
8 | volumes:
9 | - swayvnc-wayvnc-certs:/certs
10 | - ./zendriver:/app/zendriver
11 | - ./tests:/app/tests
12 | - ./scripts:/app/scripts
13 | environment:
14 | - RENDER_GROUP_GID=107 # replace with GID of the group which owns the /dev/dri/renderD128 device
15 | - SWAY_RESOLUTION=1920x1080
16 | - WAYVNC_PORT=5911
17 | - WAYVNC_ENABLE_AUTH=true
18 | - WAYVNC_USERNAME=wayvnc
19 | - WAYVNC_PASSWORD=wayvnc
20 | # When set to true, the test runner will pause after each test is complete to allow debugging with VNC client
21 | # Proceed to the next test by sending Mod+Return over VNC
22 | - ZENDRIVER_PAUSE_AFTER_TEST=false
23 | privileged: true
24 |
25 | volumes:
26 | swayvnc-wayvnc-certs:
27 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | --8<-- "README.md"
2 |
--------------------------------------------------------------------------------
/docs/quickstart.md:
--------------------------------------------------------------------------------
1 | # Quickstart
2 |
3 | ## Installation
4 |
5 | To install, simply use `pip` (or your favorite package manager):
6 |
7 | ```sh
8 | pip install zendriver
9 | # or uv add zendriver, poetry add zendriver, etc.
10 | ```
11 |
12 | ## Basic usage
13 |
14 | Open a browser, navigate to a page, and scrape the content:
15 |
16 | ```python
17 | import asyncio
18 | import zendriver as zd
19 |
20 | async def main():
21 | browser = await zd.start()
22 | page = await browser.get('https://example.com')
23 |
24 | # get HTML content of the page as a string
25 | content = await page.get_content()
26 |
27 | # save a screenshot
28 | await page.save_screenshot()
29 |
30 | # close the browser window
31 | await browser.stop()
32 |
33 |
34 | if __name__ == '__main__':
35 | asyncio.run(main())
36 | ```
37 |
38 | ## More complete example
39 |
40 | ```python
41 | import asyncio
42 | import zendriver as zd
43 |
44 | async def main():
45 | browser = await zd.start()
46 | page = await browser.get('https://slensky.com/zendriver/')
47 |
48 | elems = await page.select_all('*[src]')
49 |
50 | for elem in elems:
51 | await elem.flash()
52 |
53 | page2 = await browser.get('https://twitter.com', new_tab=True)
54 | page3 = await browser.get('https://github.com/ultrafunkamsterdam/nodriver', new_window=True)
55 |
56 | for p in (page, page2, page3):
57 | await p.bring_to_front()
58 | await p.scroll_down(200)
59 | await p # wait for events to be processed
60 | await p.reload()
61 | if p != page3:
62 | await p.close()
63 |
64 | if __name__ == '__main__':
65 | asyncio.run(main())
66 | ```
67 |
68 | I'll leave out the async boilerplate here
69 |
70 | ```python
71 | import zendriver as zd
72 |
73 | browser = await zd.start(
74 | headless=False,
75 | user_data_dir="/path/to/existing/profile", # by specifying it, it won't be automatically cleaned up when finished
76 | browser_executable_path="/path/to/some/other/browser",
77 | browser_args=['--some-browser-arg=true', '--some-other-option'],
78 | lang="en-US" # this could set iso-language-code in navigator, not recommended to change
79 | )
80 | tab = await browser.get('https://somewebsite.com')
81 | ```
82 |
83 | ## Alternative custom options
84 |
85 | I'll leave out the async boilerplate here
86 |
87 | ```python
88 | import zendriver as zd
89 |
90 | config = zd.Config()
91 | config.headless = False
92 | config.user_data_dir="/path/to/existing/profile", # by specifying it, it won't be automatically cleaned up when finished
93 | config.browser_executable_path="/path/to/some/other/browser",
94 | config.browser_args=['--some-browser-arg=true', '--some-other-option'],
95 | config.lang="en-US" # this could set iso-language-code in navigator, not recommended to change
96 | ```
97 |
98 | On Windows, we recommend using `WindowsSelectorEventLoopPolicy` for better compatibility with asyncio:
99 |
100 | ```python
101 | import asyncio
102 | import sys
103 |
104 | if sys.platform == "win32":
105 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
106 | ```
107 |
108 | A more concrete example, which can be found in the ./example/ folder,
109 | shows a script for uploading an image to imgur.
110 |
111 | ```python
112 | import asyncio
113 | from pathlib import Path
114 | import zendriver as zd
115 |
116 | # interesting, this is a typical site which runs completely on javascript, and that causes
117 | # this script to be faster than the js can present the elements. This may be one of the downsides
118 | # of this fast beast. You have to carefully consider timing.
119 | DELAY = 2
120 |
121 | async def main():
122 | browser = await zd.start()
123 | tab = await browser.get("https://imgur.com")
124 |
125 | # now we first need an image to upload, lets make a screenshot of the project page
126 | save_path = Path("screenshot.jpg").resolve()
127 | # create new tab with the project page
128 | temp_tab = await browser.get(
129 | "https://github.com/ultrafunkamsterdam/undetected-chromedriver", new_tab=True
130 | )
131 |
132 | # wait page to load
133 | await temp_tab
134 | # save the screenshot to the previously declared path of screenshot.jpg (which is just current directory)
135 | await temp_tab.save_screenshot(save_path)
136 | # done, discard the temp_tab
137 | await temp_tab.close()
138 |
139 | # accept goddamn cookies
140 | # the best_match flag will filter the best match from
141 | # matching elements containing "consent" and takes the
142 | # one having most similar text length
143 | consent = await tab.find("Consent", best_match=True)
144 | await consent.click()
145 |
146 | # shortcut
147 | await (await tab.find("new post", best_match=True)).click()
148 |
149 | file_input = await tab.select("input[type=file]")
150 | await file_input.send_file(save_path)
151 | # since file upload takes a while , the next buttons are not available yet
152 |
153 | await tab.wait(DELAY)
154 |
155 | # wait until the grab link becomes clickable, by waiting for the toast message
156 | await tab.select(".Toast-message--check")
157 |
158 | # this one is tricky. we are trying to find a element by text content
159 | # usually. the text node itself is not needed, but it's enclosing element.
160 | # in this case however, the text is NOT a text node, but an "placeholder" attribute of a span element.
161 | # so for this one, we use the flag return_enclosing_element and set it to False
162 | title_field = await tab.find("give your post a unique title", best_match=True)
163 | print(title_field)
164 | await title_field.send_keys("undetected zendriver")
165 |
166 | grab_link = await tab.find("grab link", best_match=True)
167 | await grab_link.click()
168 |
169 | # there is a delay for the link sharing popup.
170 | # let's pause for a sec
171 | await tab.wait(DELAY)
172 |
173 | # get inputs of which the value starts with http
174 | input_thing = await tab.select("input[value^=https]")
175 |
176 | my_link = input_thing.attrs.value
177 |
178 | print(my_link)
179 | await browser.stop()
180 |
181 |
182 | if __name__ == "__main__":
183 | asyncio.run(main())
184 | ```
185 |
--------------------------------------------------------------------------------
/docs/stylesheets/extra.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --md-primary-fg-color: #2ea72d;
3 | /* --md-primary-fg-color--light: #ecb7b7;
4 | --md-primary-fg-color--dark: #90030c; */
5 | }
6 |
--------------------------------------------------------------------------------
/docs/tutorials/account-creation.md:
--------------------------------------------------------------------------------
1 | **Target page:** [https://slensky.com/zendriver-examples/login-page.html](https://slensky.com/zendriver-examples/login-page.html)
2 |
3 | In this tutorial, we will demonstrate how to fill out a new account sign-up form and then log in with the newly created account. The example page login/signup is implemented entirely with JavaScript, so created accounts do not persist once the tab has been closed.
4 |
5 | Feel free to open the page now in your current browser to get an idea of what we will be working with!
6 |
7 | ## Initial setup
8 |
9 | Begin by creating a new script for the tutorial:
10 |
11 | ```python
12 | --8<-- "docs/tutorials/tutorial-code/account-creation-1.py"
13 | ```
14 |
15 | ## Creating a new account
16 |
17 | In this example page, you can create a new account by clicking on the "Sign up" link, which makes the sign-up form visible when clicked.
18 |
19 | We can create a new function to click this link, fill out the form, and submit it:
20 |
21 | ```python
22 | --8<-- "docs/tutorials/tutorial-code/account-creation-2.py:7:30"
23 | ```
24 |
25 | ## Logging in
26 |
27 | Next, filling out the login form and logging in:
28 |
29 | ```python
30 | --8<-- "docs/tutorials/tutorial-code/account-creation-2.py:33:52"
31 | ```
32 |
33 | ## Putting it all together
34 |
35 | ```python
36 | --8<-- "docs/tutorials/tutorial-code/account-creation-2.py"
37 | ```
38 |
--------------------------------------------------------------------------------
/docs/tutorials/api-responses.md:
--------------------------------------------------------------------------------
1 | **Target page:** [https://slensky.com/zendriver-examples/api-request.html](https://slensky.com/zendriver-examples/api-request.html)
2 |
3 | In this tutorial, we will demonstrate how to read a dynamically loaded API response using response expectations.
4 |
5 | The example page simulates an API request by waiting for a few seconds and then fetching a static JSON file. While it would be far easier in this case to just fetch the JSON file directly, for demonstration purposes, let's instead pretend that the response comes from a more complex API that cannot easily be called directly.
6 |
7 | ## Initial setup
8 |
9 | Begin by creating a new script for the tutorial:
10 |
11 | ```python
12 | --8<-- "docs/tutorials/tutorial-code/api-responses-1.py"
13 | ```
14 |
15 | ## Reading the API response
16 |
17 | ```python
18 | --8<-- "docs/tutorials/tutorial-code/api-responses-2.py"
19 | ```
20 |
--------------------------------------------------------------------------------
/docs/tutorials/great-power.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/docs/tutorials/great-power.jpg
--------------------------------------------------------------------------------
/docs/tutorials/index.md:
--------------------------------------------------------------------------------
1 | The following tutorials demonstrate how to use Zendriver for a variety of real-world web automation tasks, including
2 |
3 | - Navigating an infinitely scrolling feed
4 | - Creating user accounts
5 | - Retrieving the results of API requests
6 | - (Soon) Many more!
7 |
8 | The tutorials in this documentation use [example pages](https://github.com/stephanlensky/zendriver-examples) created expressly for demonstration purposes. None of the tutorials send real data or create real accounts on real websites.
9 |
10 | Always exercise caution when applying these techniques to automating live sites. Be a good web citizen! Ensure you do not make excessive numbers of requests or become a burden for the website owners, or you will quickly find your requests blocked and your IP banned.
11 |
12 |
13 | 
14 |
15 |
--------------------------------------------------------------------------------
/docs/tutorials/infinite-scrolling.md:
--------------------------------------------------------------------------------
1 | **Target page:** [https://slensky.com/zendriver-examples/scrollable-cards.html](https://slensky.com/zendriver-examples/scrollable-cards.html)
2 |
3 | In this tutorial, we will demonstrate how to scrape a page with an infinitely scrolling feed. Before we get started, check out the live website to get an idea of what we will be working with!
4 |
5 | ## Initial setup
6 |
7 | Begin by creating a new script for the tutorial:
8 |
9 | ```python
10 | --8<-- "docs/tutorials/tutorial-code/infinite-scrolling-1.py"
11 | ```
12 |
13 | In this first version of the code, we do not wait for the cards to load before trying to print them out, so the printed list will always be empty.
14 |
15 | ## Waiting for cards to appear
16 |
17 | To solve this, we need to wait for the cards to load before printing them:
18 |
19 | ```python
20 | --8<-- "docs/tutorials/tutorial-code/infinite-scrolling-2.py"
21 | ```
22 |
23 | The above change was a step in the right direction, but what if we want to keep scrolling down until we find the lucky card?
24 |
25 | ## Finding the lucky card
26 |
27 | In this final version of the script, we continuously scroll down to the bottom of the page, waiting for new sets of cards to appear until we find the lucky card.
28 |
29 | ```python
30 | --8<-- "docs/tutorials/tutorial-code/infinite-scrolling-3.py"
31 | ```
32 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/account-creation-1.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 |
5 |
6 | async def main() -> None:
7 | browser = await zd.start()
8 | page = await browser.get(
9 | "https://slensky.com/zendriver-examples/login-page.html",
10 | )
11 |
12 | # TODO: Sign-up and login
13 |
14 | await browser.stop()
15 |
16 |
17 | if __name__ == "__main__":
18 | asyncio.run(main())
19 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/account-creation-2.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 | from zendriver import Tab
5 |
6 |
7 | async def create_account(page: Tab, name: str, email: str, password: str) -> None:
8 | # Click on the "Sign up" link
9 | sign_up_link = next(a for a in await page.select_all("a") if "Sign up" in a.text)
10 | await sign_up_link.click()
11 | await asyncio.sleep(0.5)
12 |
13 | # Fill in the sign-up form
14 | name_input = await page.select("#signupName")
15 | await name_input.send_keys(name)
16 | email_input = await page.select("#signupEmail")
17 | await email_input.send_keys(email)
18 | password_input = await page.select("#signupPassword")
19 | await password_input.send_keys(password)
20 |
21 | # Click the "Sign Up" button
22 | sign_up_button = next(
23 | button for button in await page.select_all("button") if "Sign Up" in button.text
24 | )
25 | await sign_up_button.click()
26 | await asyncio.sleep(0.5)
27 |
28 | # Click through confirmation dialog
29 | proceed_to_login = await page.find(text="Proceed to Login")
30 | await proceed_to_login.click()
31 |
32 |
33 | async def login(page: Tab, email: str, password: str) -> None:
34 | # Fill in the login form
35 | email_input = await page.select("#loginEmail")
36 | await email_input.send_keys(email)
37 | password_input = await page.select("#loginPassword")
38 | await password_input.send_keys(password)
39 |
40 | # Click the "Login" button
41 | login_button = next(
42 | button for button in await page.select_all("button") if "Login" in button.text
43 | )
44 | await login_button.click()
45 | await asyncio.sleep(0.5)
46 |
47 | # Verify successful login
48 | message = await page.select("#message")
49 | if "Welcome back" in message.text_all:
50 | print("Login successful")
51 | else:
52 | print("Login failed")
53 |
54 |
55 | async def main() -> None:
56 | browser = await zd.start()
57 | page = await browser.get(
58 | "https://slensky.com/zendriver-examples/login-page.html",
59 | )
60 | await asyncio.sleep(0.5) # Wait for the page to load
61 |
62 | name = "John Doe"
63 | email = "john.doe@example.com"
64 | password = "securepassword"
65 |
66 | await create_account(page, name, email, password)
67 | await login(page, email, password)
68 |
69 | await browser.stop()
70 |
71 |
72 | if __name__ == "__main__":
73 | asyncio.run(main())
74 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/api-responses-1.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 |
5 |
6 | async def main() -> None:
7 | browser = await zd.start()
8 |
9 | # TODO: Read the API response
10 | page = await browser.get(
11 | "https://slensky.com/zendriver-examples/api-request.html",
12 | )
13 |
14 | await browser.stop()
15 |
16 |
17 | if __name__ == "__main__":
18 | asyncio.run(main())
19 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/api-responses-2.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import json
3 |
4 | import zendriver as zd
5 | from zendriver.cdp.network import get_response_body
6 |
7 |
8 | async def main() -> None:
9 | browser = await zd.start()
10 |
11 | page = browser.tabs[0]
12 | async with page.expect_response(".*/user-data.json") as response_expectation:
13 | await page.get(
14 | "https://slensky.com/zendriver-examples/api-request.html",
15 | )
16 | response = await response_expectation.value
17 |
18 | request_id = response.request_id
19 | body, _ = await page.send(get_response_body(request_id=request_id))
20 | user_data = json.loads(body)
21 |
22 | print("Successfully read user data response for user:", user_data["name"])
23 | print(json.dumps(user_data, indent=2))
24 |
25 | await browser.stop()
26 |
27 |
28 | if __name__ == "__main__":
29 | asyncio.run(main())
30 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/infinite-scrolling-1.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 |
5 |
6 | async def main() -> None:
7 | browser = await zd.start()
8 | page = await browser.get(
9 | "https://slensky.com/zendriver-examples/scrollable-cards.html",
10 | )
11 |
12 | # Not yet loaded, so empty
13 | card_container = await page.select("#card-container")
14 | cards = card_container.children
15 | print(cards) # []
16 |
17 | await browser.stop()
18 |
19 |
20 | if __name__ == "__main__":
21 | asyncio.run(main())
22 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/infinite-scrolling-2.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 | from zendriver import Element, Tab
5 |
6 |
7 | async def wait_for_cards(page: Tab, initial_card_count: int) -> list[Element]:
8 | while True:
9 | card_container = await page.select("#card-container")
10 | cards = card_container.children
11 | if len(cards) > initial_card_count:
12 | return cards
13 | await asyncio.sleep(0.5)
14 |
15 |
16 | async def main() -> None:
17 | browser = await zd.start()
18 | page = await browser.get(
19 | "https://slensky.com/zendriver-examples/scrollable-cards.html",
20 | )
21 |
22 | # Wait for cards to load
23 | cards = await wait_for_cards(page, initial_card_count=0)
24 |
25 | # Now we can print the cards
26 | # (shows first 10 cards: Card 1, Card 2...Card 9, Card 10)
27 | for card in cards:
28 | print(card.text)
29 |
30 | await browser.stop()
31 |
32 |
33 | if __name__ == "__main__":
34 | asyncio.run(main())
35 |
--------------------------------------------------------------------------------
/docs/tutorials/tutorial-code/infinite-scrolling-3.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 | from zendriver import Element, Tab
5 |
6 |
7 | async def wait_for_cards(page: Tab, initial_card_count: int) -> list[Element]:
8 | while True:
9 | card_container = await page.select("#card-container")
10 | cards = card_container.children
11 | if len(cards) > initial_card_count:
12 | print("Loaded new cards. Current count:", len(cards))
13 | return cards
14 | await asyncio.sleep(0.5)
15 |
16 |
17 | def get_lucky_card(cards: list[Element]) -> Element | None:
18 | for card in cards:
19 | if "Congratulations, you found the lucky card!" in card.text_all:
20 | return card
21 |
22 | return None
23 |
24 |
25 | async def main() -> None:
26 | browser = await zd.start()
27 | page = await browser.get(
28 | "https://slensky.com/zendriver-examples/scrollable-cards.html",
29 | )
30 |
31 | # Wait for the first batch of cards to load
32 | cards = await wait_for_cards(page, initial_card_count=0)
33 |
34 | # Loop until we find the lucky card
35 | while (lucky_card := get_lucky_card(cards)) is None:
36 | # Scroll to the bottom of the page
37 | await page.scroll_down(1000) # 10x page height, likely to be enough
38 |
39 | # Get the new cards
40 | cards = await wait_for_cards(page, initial_card_count=len(cards))
41 |
42 | if lucky_card:
43 | print(f"Lucky card found: Card {cards.index(lucky_card) + 1}")
44 |
45 | await browser.stop()
46 |
47 |
48 | if __name__ == "__main__":
49 | asyncio.run(main())
50 |
--------------------------------------------------------------------------------
/examples/browserscan.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 |
5 |
6 | async def main():
7 | browser = await zd.start()
8 | page = await browser.get("https://www.browserscan.net/bot-detection")
9 | await page.save_screenshot("browserscan.png")
10 | await browser.stop()
11 |
12 |
13 | if __name__ == "__main__":
14 | asyncio.run(main())
15 |
--------------------------------------------------------------------------------
/examples/demo.py:
--------------------------------------------------------------------------------
1 | # coding: utf-8
2 |
3 |
4 | import asyncio
5 | import logging
6 | import logging.handlers
7 | import random
8 | import time
9 |
10 | import mss
11 |
12 | logger = logging.getLogger("demo")
13 | logging.basicConfig(level=10)
14 |
15 |
16 | try:
17 | import zendriver as uc
18 | except (ModuleNotFoundError, ImportError):
19 | import os
20 | import sys
21 |
22 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
23 | import zendriver as uc
24 |
25 | _monitor = mss.mss().monitors[0]
26 | SCREEN_WIDTH = _monitor["width"]
27 | NUM_WINS = SCREEN_WIDTH // 325 # as to not fill up the screen to much
28 |
29 |
30 | class Timing:
31 | def __init__(self):
32 | self.start = None
33 | self.stop = None
34 | self.taken = None
35 |
36 | def __enter__(self):
37 | self.start = time.monotonic()
38 | return self
39 |
40 | def __exit__(self, *args, **kwargs):
41 | self.stop = time.monotonic()
42 | self.taken = self.stop - self.start
43 | print("taken:", self.taken, "seconds")
44 |
45 |
46 | async def main():
47 | driver = await uc.start()
48 |
49 | URL1 = "https://www.bet365.com"
50 | URL2 = "https://www.nowsecure.nl"
51 |
52 | print(
53 | "the startup speed of the windows depend on the line speed and availability/load on the servers"
54 | )
55 | await driver.get(URL2)
56 |
57 | for _ in range(NUM_WINS):
58 | if _ % 2 == 0:
59 | await driver.get(URL1, new_window=True)
60 | else:
61 | await driver.get(URL2, new_window=True)
62 |
63 | await driver
64 |
65 | grid = await driver.tile_windows(max_columns=NUM_WINS)
66 | await driver.sleep(5)
67 | for tab in driver.tabs:
68 | await tab.maximize()
69 |
70 | for _ in range(15):
71 | randbox = lambda: random.choice(grid)
72 | for i, tab in enumerate(driver.tabs):
73 | await tab.activate()
74 | await tab.set_window_size(*randbox())
75 | await driver
76 |
77 | for i, tab in enumerate(driver):
78 | if i >= len(grid):
79 | i = len(grid) - i
80 | await tab.set_window_size(*grid[i])
81 | await tab.sleep()
82 |
83 | await asyncio.gather(
84 | *[move_circle(tab, i % 2) for (i, tab) in enumerate(driver.tabs)]
85 | )
86 |
87 | nowsecure_pages = [tab for tab in driver.tabs if "nowsecure" in tab.url]
88 |
89 | await asyncio.gather(
90 | *[tab.get("https://nowsecure.nl/mouse.html") for tab in nowsecure_pages]
91 | )
92 | await driver.tile_windows(max_columns=NUM_WINS)
93 | await asyncio.gather(*[mouse_move(tab) for tab in nowsecure_pages])
94 |
95 | b365pages = [tab for tab in driver.tabs if "bet365" in tab.url]
96 | # await driver.tile_windows()
97 | await driver.sleep(1)
98 |
99 | positions = await driver.tile_windows(b365pages)
100 | # await asyncio.gather(*[tab.set_window_size(*pos) for (tab, pos) in zip(b365pages, positions)])
101 | await asyncio.gather(*[flash_spans(tab, i) for (i, tab) in enumerate(b365pages)])
102 | await asyncio.gather(*[scroll_task(tab) for tab in b365pages])
103 |
104 | for i, tab in enumerate(driver):
105 | try:
106 | await tab.get("https://www.google.com")
107 | await tab.activate()
108 | # skip first tab
109 | if tab == driver.main_tab:
110 | print("skipping main tab")
111 | continue
112 | except:
113 | pass
114 | await tab.close()
115 |
116 | for i, tab in enumerate(driver):
117 | try:
118 | if i == 0:
119 | continue
120 | # await driver.tile_windows(i)
121 | await tab.activate()
122 | # skip first tab
123 | await tab.close()
124 |
125 | except:
126 | pass
127 | print("TBCI=", driver.connection.listener.time_before_considered_idle)
128 | await driver.stop()
129 |
130 |
131 | async def scroll_task(tab):
132 | await tab.scroll_up(200)
133 | spans = await tab.select_all("span")
134 | [
135 | await s.scroll_into_view()
136 | # since scroll_into_view does not return a value
137 | # we can abuse 'or' to run 2 functions in a list comprehension
138 | or await s.flash()
139 | for s in reversed(spans)
140 | ]
141 | [
142 | (await tab.scroll_up(n // 2) or await tab.scroll_down(n))
143 | # since the above does not return a value
144 | # we can abuse 'or' to run even more while doing a list comprehension
145 | or print("tab %s scrolling down : %d" % (tab, n))
146 | for n in range(0, 75, 15)
147 | ]
148 |
149 |
150 | async def mouse_move(tab):
151 | await tab.activate()
152 | boxes = await tab.select_all(".box")
153 | for box in boxes:
154 | await box.mouse_move()
155 |
156 |
157 | async def move_circle(tab, x=0):
158 | window_id, bounds = await tab.get_window()
159 |
160 | old_left, old_top = bounds.left, bounds.top
161 | old_width, old_height = bounds.width, bounds.height
162 |
163 | center = int(old_left), int(old_top)
164 |
165 | for coord in uc.util.circle(*center, radius=100, num=1050, dir=x):
166 | new_left, new_top = int(coord[0]), int(coord[1])
167 | await tab.set_window_size(new_left, new_top)
168 |
169 | for coord in uc.util.circle(*center, radius=250, num=500, dir=x):
170 | new_left, new_top = int(coord[0] / 2), int(coord[1] / 2)
171 | # await tab.set_window_size(old_left, old_top)
172 | await tab.set_window_size(new_left, new_top)
173 |
174 | await tab.set_window_size(old_left, old_top, old_width, old_height)
175 |
176 |
177 | async def flash_spans(tab, i):
178 | logger.info("flashing spans. i=%d , tab=%s, url=%s" % (i, tab, tab.url))
179 | # await tab.fullscreen()
180 | elems = await tab.select_all("span")
181 | # await tab.medimize()
182 | await tab.activate()
183 | for elem in elems:
184 | await elem.flash(duration=0.25)
185 | await elem.scroll_into_view()
186 |
187 |
188 | if __name__ == "__main__":
189 | with Timing() as t:
190 | uc.loop().run_until_complete(main())
191 |
--------------------------------------------------------------------------------
/examples/expect_download.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import base64
3 | import os
4 |
5 | import zendriver as zd
6 |
7 |
8 | async def main():
9 | path_image = r"Your Image Path"
10 | out_dir = r"."
11 | async with await zd.start() as browser:
12 | page = browser.main_tab
13 | await page.get("https://translate.yandex.com/en/ocr")
14 | await (await page.select('input[type="file"]')).send_file(path_image)
15 | await asyncio.sleep(3)
16 |
17 | async with page.expect_download() as download_ex:
18 | await (await page.select("#downloadButton")).mouse_click()
19 | download = await download_ex.value
20 | print(download.suggested_filename)
21 | with open(os.path.join(out_dir, download.suggested_filename), "wb") as fw:
22 | bytes_file = base64.b64decode(download.url.split(",", 1)[-1])
23 | fw.write(bytes_file)
24 |
25 |
26 | if __name__ == "__main__":
27 | asyncio.run(main())
28 |
--------------------------------------------------------------------------------
/examples/fetch_domain.py:
--------------------------------------------------------------------------------
1 | try:
2 | from zendriver import *
3 | except (ModuleNotFoundError, ImportError):
4 | import sys
5 | import os
6 |
7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8 | from zendriver import *
9 |
10 | import logging
11 |
12 | logging.basicConfig(level=logging.INFO)
13 |
14 |
15 | async def request_handler(ev: cdp.fetch.RequestPaused, tab: Tab):
16 | print("\nRequestPaused handler\n", ev, type(ev))
17 | print("TAB = ", tab)
18 | tab.feed_cdp(cdp.fetch.continue_request(request_id=ev.request_id))
19 |
20 |
21 | async def main():
22 | browser = await start()
23 |
24 | [await browser.get("https://www.google.com", new_window=True) for _ in range(10)]
25 |
26 | for tab in browser:
27 | print(tab)
28 | tab.add_handler(cdp.fetch.RequestPaused, request_handler)
29 | await tab.send(cdp.fetch.enable())
30 |
31 | for tab in browser:
32 | await tab
33 |
34 | for tab in browser:
35 | await tab.activate()
36 |
37 | for tab in reversed(browser):
38 | await tab.activate()
39 | await tab.close()
40 |
41 | await browser.stop()
42 |
43 |
44 | browser = loop().run_until_complete(main())
45 |
--------------------------------------------------------------------------------
/examples/imgur_upload_image.py:
--------------------------------------------------------------------------------
1 | try:
2 | from zendriver import *
3 | except (ModuleNotFoundError, ImportError):
4 | import os
5 | import sys
6 |
7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8 | from zendriver import *
9 |
10 | from pathlib import Path
11 |
12 | # interesting, this is a typical site which runs completely on javascript, and that causes
13 | # this script to be faster than the js can present the elements. This may be one of the downsides
14 | # of this fast beast. You have to carefully consider timing.
15 |
16 | DELAY = 2
17 |
18 |
19 | try:
20 | from zendriver import *
21 | except (ModuleNotFoundError, ImportError):
22 | import os
23 | import sys
24 |
25 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
26 | from zendriver import *
27 |
28 |
29 | async def main():
30 | browser = await start()
31 | tab = await browser.get("https://imgur.com")
32 |
33 | # now we first need an image to upload, lets make a screenshot of the project page
34 | save_path = Path("screenshot.jpg").resolve()
35 | # create new tab with the project page
36 | temp_tab = await browser.get(
37 | "https://github.com/ultrafunkamsterdam/undetected-chromedriver", new_tab=True
38 | )
39 |
40 | # wait page to load
41 | await temp_tab
42 | # save the screenshot to the previously declared path of screenshot.jpg (which is just current directory)
43 | await temp_tab.save_screenshot(save_path)
44 | # done, discard the temp_tab
45 | await temp_tab.close()
46 |
47 | # accept goddamn cookies
48 | # the best_match flag will filter the best match from
49 | # matching elements containing "consent" and takes the
50 | # one having most similar text length
51 | consent = await tab.find("Consent", best_match=True)
52 | await consent.click()
53 |
54 | # shortcut
55 | await (await tab.find("new post", best_match=True)).click()
56 |
57 | file_input = await tab.select("input[type=file]")
58 | await file_input.send_file(save_path)
59 | # since file upload takes a while , the next buttons are not available yet
60 |
61 | await tab.wait(DELAY)
62 |
63 | # wait until the grab link becomes clickable, by waiting for the toast message
64 | await tab.select(".Toast-message--check")
65 |
66 | # this one is tricky. we are trying to find a element by text content
67 | # usually. the text node itself is not needed, but it's enclosing element.
68 | # in this case however, the text is NOT a text node, but an "placeholder" attribute of a span element.
69 | # so for this one, we use the flag return_enclosing_element and set it to False
70 | title_field = await tab.find("give your post a unique title", best_match=True)
71 | print(title_field)
72 | await title_field.send_keys("undetected zendriver")
73 |
74 | grab_link = await tab.find("grab link", best_match=True)
75 | await grab_link.click()
76 |
77 | # there is a delay for the link sharing popup.
78 | # let's pause for a sec
79 | await tab.wait(DELAY)
80 |
81 | # get inputs of which the value starts with http
82 | input_thing = await tab.select("input[value^=https]")
83 |
84 | my_link = input_thing.attrs.value
85 |
86 | print(my_link)
87 | await tab
88 |
89 |
90 | if __name__ == "__main__":
91 | loop = loop()
92 | loop.run_until_complete(main())
93 |
--------------------------------------------------------------------------------
/examples/mouse_drag_boxes.py:
--------------------------------------------------------------------------------
1 | try:
2 | from zendriver import *
3 | except (ModuleNotFoundError, ImportError):
4 | import sys
5 | import os
6 |
7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8 | from zendriver import *
9 |
10 |
11 | async def main():
12 | browser = await start()
13 | await demo_drag_to_target(browser)
14 | await demo_drag_to_target_in_steps(browser)
15 | await demo_drag_to_absolute_position(browser)
16 | await demo_drag_to_absolute_position_in_steps(browser)
17 | await demo_drag_to_relative_position(browser)
18 | await demo_drag_to_relative_position_in_steps(browser)
19 |
20 |
21 | async def demo_drag_to_target(browser):
22 | tab = await browser.get("https://nowsecure.nl/mouse.html?boxes=50")
23 | boxes = await tab.select_all(".box")
24 | area = await tab.select(".area-a")
25 | for box in boxes:
26 | await box.mouse_drag(area)
27 |
28 |
29 | async def demo_drag_to_target_in_steps(browser):
30 | tab = await browser.get("https://nowsecure.nl/mouse.html")
31 | boxes = await tab.select_all(".box")
32 | area = await tab.select(".area-a")
33 |
34 | for box in boxes:
35 | await box.mouse_drag(area, steps=100)
36 |
37 |
38 | async def demo_drag_to_absolute_position(browser):
39 | tab = await browser.get("https://nowsecure.nl/mouse.html?boxes=50")
40 | boxes = await tab.select_all(".box")
41 | area = await tab.select(".area-a")
42 |
43 | for box in boxes:
44 | await box.mouse_drag((500, 500))
45 |
46 |
47 | async def demo_drag_to_absolute_position_in_steps(browser):
48 | tab = await browser.get("https://nowsecure.nl/mouse.html")
49 | boxes = await tab.select_all(".box")
50 | area = await tab.select(".area-a")
51 |
52 | for box in boxes:
53 | await box.mouse_drag((500, 500), steps=50)
54 |
55 |
56 | async def demo_drag_to_relative_position(browser):
57 | tab = await browser.get("https://nowsecure.nl/mouse.html?boxes=50")
58 | boxes = await tab.select_all(".box")
59 |
60 | for box in boxes:
61 | await box.mouse_drag((500, 500), relative=True)
62 |
63 |
64 | async def demo_drag_to_relative_position_in_steps(browser):
65 | tab = await browser.get("https://nowsecure.nl/mouse.html")
66 | boxes = await tab.select_all(".box")
67 |
68 | for box in boxes:
69 | await box.mouse_drag((500, 500), relative=True, steps=50)
70 |
71 |
72 | if __name__ == "__main__":
73 | loop().run_until_complete(main())
74 |
--------------------------------------------------------------------------------
/examples/network_monitor.py:
--------------------------------------------------------------------------------
1 | try:
2 | from zendriver import cdp, loop, start
3 | except (ModuleNotFoundError, ImportError):
4 | import os
5 | import sys
6 |
7 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8 | from zendriver import cdp, loop, start
9 |
10 |
11 | async def main():
12 | browser = await start()
13 |
14 | tab = browser.main_tab
15 | tab.add_handler(cdp.network.RequestWillBeSent, send_handler)
16 | tab.add_handler(cdp.network.ResponseReceived, receive_handler)
17 |
18 | tab = await browser.get("https://www.google.com/?hl=en")
19 |
20 | reject_btn = await tab.find("reject all", best_match=True)
21 | await reject_btn.click()
22 |
23 | search_inp = await tab.select("textarea")
24 | await search_inp.send_keys("undetected zendriver")
25 |
26 | search_btn = await tab.find("google search", True)
27 | await search_btn.click()
28 |
29 | for _ in range(10):
30 | await tab.scroll_down(50)
31 |
32 | await tab
33 | await tab.back()
34 |
35 | search_inp = await tab.select("textarea")
36 |
37 | for letter in "undetected zendriver":
38 | await search_inp.clear_input()
39 | await search_inp.send_keys(
40 | "undetected zendriver".replace(letter, letter.upper())
41 | )
42 | await tab.wait(0.1)
43 |
44 | all_urls = await tab.get_all_urls()
45 | for u in all_urls:
46 | print("downloading %s" % u)
47 | await tab.download_file(u)
48 |
49 | await tab.sleep(10)
50 |
51 |
52 | async def receive_handler(event: cdp.network.ResponseReceived):
53 | print(event.response)
54 |
55 |
56 | async def send_handler(event: cdp.network.RequestWillBeSent):
57 | r = event.request
58 | s = f"{r.method} {r.url}"
59 | for k, v in r.headers.items():
60 | s += f"\n\t{k} : {v}"
61 | print(s)
62 |
63 |
64 | if __name__ == "__main__":
65 | loop().run_until_complete(main())
66 |
--------------------------------------------------------------------------------
/examples/set_user_agent.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import zendriver as zd
4 |
5 |
6 | async def main():
7 | browser = await zd.start()
8 | tab = browser.main_tab
9 | await tab.set_user_agent("My user agent", accept_language="de", platform="Win32")
10 |
11 | print(await tab.evaluate("navigator.userAgent")) # My user agent
12 | print(await tab.evaluate("navigator.language")) # de
13 | print(await tab.evaluate("navigator.platform")) # Win32
14 |
15 | await browser.stop()
16 |
17 |
18 | if __name__ == "__main__":
19 | asyncio.run(main())
20 |
--------------------------------------------------------------------------------
/examples/wait_for_page.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import zendriver as zd
3 |
4 |
5 | async def main():
6 | async with await zd.start() as browser:
7 | tab = browser.main_tab
8 | async with tab.expect_request("https://github.com/") as request_info:
9 | async with tab.expect_response(
10 | "https://github.githubassets.com/assets/.*"
11 | ) as response_info:
12 | await tab.get("https://github.com/")
13 | await tab.wait_for_ready_state(until="complete")
14 |
15 | req = await request_info.value
16 | print(req.request_id)
17 |
18 | res = await response_info.value
19 | print(res.request_id)
20 |
21 |
22 | if __name__ == "__main__":
23 | asyncio.run(main())
24 |
--------------------------------------------------------------------------------
/mkdocs.yml:
--------------------------------------------------------------------------------
1 | extra_css:
2 | - stylesheets/extra.css
3 | markdown_extensions:
4 | - pymdownx.highlight:
5 | anchor_linenums: true
6 | - pymdownx.inlinehilite
7 | - pymdownx.snippets
8 | - pymdownx.superfences
9 | - pymdownx.details
10 | - def_list
11 | - admonition
12 | - md_in_html
13 | nav:
14 | - Documentation:
15 | - Introduction: index.md
16 | - Quickstart: quickstart.md
17 | - Tutorials:
18 | - About these Tutorials: tutorials/index.md
19 | - Tutorials:
20 | - Infinitely scrolling feed: tutorials/infinite-scrolling.md
21 | - Creating accounts: tutorials/account-creation.md
22 | - Reading API responses: tutorials/api-responses.md
23 | - Reference:
24 | - cdp:
25 | - accessibility: reference/cdp/accessibility.md
26 | - animation: reference/cdp/animation.md
27 | - audits: reference/cdp/audits.md
28 | - autofill: reference/cdp/autofill.md
29 | - background_service: reference/cdp/background_service.md
30 | - bluetooth_emulation: reference/cdp/bluetooth_emulation.md
31 | - browser: reference/cdp/browser.md
32 | - cache_storage: reference/cdp/cache_storage.md
33 | - cast: reference/cdp/cast.md
34 | - console: reference/cdp/console.md
35 | - css: reference/cdp/css.md
36 | - database: reference/cdp/database.md
37 | - debugger: reference/cdp/debugger.md
38 | - device_access: reference/cdp/device_access.md
39 | - device_orientation: reference/cdp/device_orientation.md
40 | - dom: reference/cdp/dom.md
41 | - dom_debugger: reference/cdp/dom_debugger.md
42 | - dom_snapshot: reference/cdp/dom_snapshot.md
43 | - dom_storage: reference/cdp/dom_storage.md
44 | - emulation: reference/cdp/emulation.md
45 | - event_breakpoints: reference/cdp/event_breakpoints.md
46 | - extensions: reference/cdp/extensions.md
47 | - fed_cm: reference/cdp/fed_cm.md
48 | - fetch: reference/cdp/fetch.md
49 | - file_system: reference/cdp/file_system.md
50 | - headless_experimental: reference/cdp/headless_experimental.md
51 | - heap_profiler: reference/cdp/heap_profiler.md
52 | - indexed_db: reference/cdp/indexed_db.md
53 | - input_: reference/cdp/input_.md
54 | - inspector: reference/cdp/inspector.md
55 | - io: reference/cdp/io.md
56 | - layer_tree: reference/cdp/layer_tree.md
57 | - log: reference/cdp/log.md
58 | - media: reference/cdp/media.md
59 | - memory: reference/cdp/memory.md
60 | - network: reference/cdp/network.md
61 | - overlay: reference/cdp/overlay.md
62 | - page: reference/cdp/page.md
63 | - performance: reference/cdp/performance.md
64 | - performance_timeline: reference/cdp/performance_timeline.md
65 | - preload: reference/cdp/preload.md
66 | - profiler: reference/cdp/profiler.md
67 | - pwa: reference/cdp/pwa.md
68 | - runtime: reference/cdp/runtime.md
69 | - schema: reference/cdp/schema.md
70 | - security: reference/cdp/security.md
71 | - service_worker: reference/cdp/service_worker.md
72 | - storage: reference/cdp/storage.md
73 | - system_info: reference/cdp/system_info.md
74 | - target: reference/cdp/target.md
75 | - tethering: reference/cdp/tethering.md
76 | - tracing: reference/cdp/tracing.md
77 | - util: reference/cdp/util.md
78 | - web_audio: reference/cdp/web_audio.md
79 | - web_authn: reference/cdp/web_authn.md
80 | - Release Notes:
81 | - Release Notes: release-notes.md
82 | plugins:
83 | - search
84 | - mkdocstrings:
85 | handlers:
86 | python:
87 | options:
88 | docstring_style: sphinx
89 | show_if_no_docstring: true
90 | show_root_heading: false
91 | show_root_toc_entry: false
92 | site_name: Zendriver
93 | site_url: https://slensky.com/zendriver
94 | theme:
95 | features:
96 | - navigation.footer
97 | - navigation.tabs
98 | - navigation.expand
99 | name: material
100 | palette:
101 | - accent: green
102 | primary: custom
103 | scheme: slate
104 | toggle:
105 | icon: material/toggle-switch-off-outline
106 | name: Switch to light mode
107 | - accent: green
108 | primary: custom
109 | scheme: default
110 | toggle:
111 | icon: material/toggle-switch
112 | name: Switch to dark mode
113 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "zendriver"
3 | version = "0.8.0"
4 | description = "A blazing fast, async-first, undetectable webscraping/web automation framework"
5 | readme = "README.md"
6 | authors = [{ name = "Stephan Lensky", email = "oss@slensky.com" }]
7 | license = { file = "LICENSE" }
8 | classifiers = [
9 | "Development Status :: 3 - Alpha",
10 | "Intended Audience :: Developers",
11 | "Topic :: Internet :: WWW/HTTP :: Browsers",
12 | "License :: OSI Approved :: GNU Affero General Public License v3",
13 | "Programming Language :: Python :: 3",
14 | "Programming Language :: Python :: 3.10",
15 | "Programming Language :: Python :: 3.11",
16 | "Programming Language :: Python :: 3.12",
17 | "Programming Language :: Python :: 3.13",
18 | ]
19 | requires-python = ">=3.10"
20 | dependencies = [
21 | "asyncio-atexit>=1.0.1",
22 | "deprecated>=1.2.14",
23 | "mss>=9.0.2",
24 | "websockets>=14.0",
25 | ]
26 |
27 | [build-system]
28 | requires = ["hatchling"]
29 | build-backend = "hatchling.build"
30 |
31 | [tool.uv]
32 | dev-dependencies = [
33 | "mkdocs-material>=9.5.42",
34 | "mkdocstrings[python]>=0.26.2",
35 | "mypy>=1.12.0",
36 | "pytest-asyncio>=0.24.0",
37 | "pytest>=8.3.3",
38 | "pyyaml>=6.0.2",
39 | "ruff>=0.7.4",
40 | "types-pyyaml>=6.0.12.20240917",
41 | "types-requests>=2.32.0.20241016",
42 | "pytest-mock>=3.14.0",
43 | "types-deprecated>=1.2.15.20241117",
44 | "inflection>=0.5.1",
45 | ]
46 |
47 | [tool.pytest.ini_options]
48 | asyncio_mode = "auto"
49 | asyncio_default_fixture_loop_scope = "function"
50 | log_level = "INFO"
51 |
52 | [tool.ruff]
53 | exclude = ["zendriver/cdp"]
54 |
55 | [tool.ruff.lint]
56 | exclude = [
57 | "examples/demo.py",
58 | "examples/fetch_domain.py",
59 | "examples/imgur_upload_image.py",
60 | "examples/mouse_drag_boxes.py",
61 | ]
62 |
63 | [tool.mypy]
64 | exclude = [
65 | "zendriver/cdp",
66 | "examples/demo.py",
67 | "examples/imgur_upload_image.py",
68 | ]
69 | check_untyped_defs = true
70 |
71 | [[tool.mypy.overrides]]
72 | module = [
73 | "asyncio_atexit",
74 | ]
75 | ignore_missing_imports = true
76 |
--------------------------------------------------------------------------------
/scripts/format.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | python_files=(zendriver scripts examples tests)
7 |
8 | uv run ruff check "${python_files[@]}" --fix
9 | uv run ruff format "${python_files[@]}"
10 |
--------------------------------------------------------------------------------
/scripts/lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | python_files=(zendriver scripts examples tests)
7 |
8 | echo "Running ruff check..."
9 | uv run ruff check "${python_files[@]}"
10 | echo "Running ruff format check..."
11 | uv run ruff format "${python_files[@]}" --check
12 | echo "Running mypy..."
13 | uv run mypy "${python_files[@]}"
14 |
--------------------------------------------------------------------------------
/scripts/mkdocs_generate_all.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 | set -x
5 |
6 | uv run python scripts/mkdocs_generate_reference.py
7 | uv run python scripts/mkdocs_generate_release_notes.py
8 |
--------------------------------------------------------------------------------
/scripts/mkdocs_generate_index.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S uv run
2 | """
3 | Copy README.md to docs homepage at docs/index.md.
4 | """
5 |
6 | from pathlib import Path
7 |
8 | REPO_ROOT = Path(__file__).parent.parent
9 | README_MD = REPO_ROOT / "README.md"
10 | INDEX_MD = REPO_ROOT / "docs" / "index.md"
11 |
12 |
13 | def main() -> None:
14 | readme_content = README_MD.read_text()
15 | INDEX_MD.write_text(readme_content)
16 | print("Successfully generated index.md!")
17 |
18 |
19 | if __name__ == "__main__":
20 | main()
21 |
--------------------------------------------------------------------------------
/scripts/mkdocs_generate_reference.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S uv run
2 | """
3 | Automatically generate code reference pages under docs/reference.
4 | """
5 |
6 | import shutil
7 | from pathlib import Path
8 |
9 | import yaml
10 |
11 | PACKAGE_NAME = "zendriver"
12 | REPO_ROOT = Path(__file__).parent.parent
13 | PACKAGE_ROOT = REPO_ROOT / PACKAGE_NAME
14 | REFERENCE_DOCS_ROOT = REPO_ROOT / "docs" / "reference"
15 | MKDOCS_YML = REPO_ROOT / "mkdocs.yml"
16 |
17 |
18 | def clean_reference_docs_dir() -> None:
19 | if not REFERENCE_DOCS_ROOT.exists():
20 | return
21 |
22 | for path in REFERENCE_DOCS_ROOT.glob("*"):
23 | if path.is_dir():
24 | shutil.rmtree(path)
25 | else:
26 | path.unlink()
27 |
28 |
29 | def get_documented_modules() -> list[Path]:
30 | return [
31 | path.relative_to(PACKAGE_ROOT)
32 | for path in sorted(PACKAGE_ROOT.rglob("cdp/*.py"))
33 | if not path.stem.startswith("_")
34 | ]
35 |
36 |
37 | def load_mkdocs_yml() -> dict:
38 | mkdocs_yml_path = MKDOCS_YML
39 | with mkdocs_yml_path.open() as f:
40 | return yaml.safe_load(f)
41 |
42 |
43 | def write_mkdocs_yml(mkdocs_yml: dict) -> None:
44 | mkdocs_yml_path = MKDOCS_YML
45 | with mkdocs_yml_path.open("w") as f:
46 | yaml.safe_dump(mkdocs_yml, f)
47 |
48 |
49 | def get_nav_item_by_title(nav_section: list[dict], title: str) -> list[dict]:
50 | for item in nav_section:
51 | if title in item:
52 | return item[title]
53 |
54 | raise ValueError(f"Title '{title}' not found in nav section")
55 |
56 |
57 | def write_doc_md(doc_path: Path, module: str) -> None:
58 | doc_path.parent.mkdir(parents=True, exist_ok=True)
59 |
60 | with doc_path.open("w") as f:
61 | f.write(f"::: {PACKAGE_NAME}.{module}\n")
62 |
63 |
64 | def add_nav_items(reference_docs: list[Path]) -> None:
65 | mkdocs_yml = load_mkdocs_yml()
66 | current_section = mkdocs_yml["nav"]
67 | reference_section = get_nav_item_by_title(current_section, "Reference")
68 | reference_section.clear()
69 |
70 | for doc_path in reference_docs:
71 | path_parts = list(doc_path.relative_to(REFERENCE_DOCS_ROOT).parts)
72 | current_section = reference_section
73 | while len(path_parts) > 1:
74 | current_part = path_parts[0]
75 | try:
76 | current_section = get_nav_item_by_title(current_section, current_part)
77 | except ValueError:
78 | current_section.append({current_part: []})
79 | current_section = current_section[-1][current_part]
80 |
81 | path_parts = path_parts[1:]
82 |
83 | current_section.append(
84 | {
85 | path_parts[0].removesuffix(".md"): doc_path.relative_to(
86 | REPO_ROOT / "docs"
87 | ).as_posix()
88 | }
89 | )
90 |
91 | write_mkdocs_yml(mkdocs_yml)
92 |
93 |
94 | def main() -> None:
95 | clean_reference_docs_dir()
96 |
97 | reference_docs: list[Path] = []
98 | for module_path in get_documented_modules():
99 | doc_path = REFERENCE_DOCS_ROOT / module_path.with_suffix(".md")
100 | module = ".".join(module_path.with_suffix("").parts)
101 |
102 | print(f"Generating {doc_path}...")
103 | write_doc_md(doc_path, module)
104 | reference_docs.append(doc_path)
105 |
106 | add_nav_items(reference_docs)
107 |
108 |
109 | if __name__ == "__main__":
110 | main()
111 |
--------------------------------------------------------------------------------
/scripts/mkdocs_generate_release_notes.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S uv run
2 | """
3 | Generate release notes from CHANGELOG.md and write them to docs/release-notes.md.
4 | """
5 |
6 | from pathlib import Path
7 |
8 | REPO_ROOT = Path(__file__).parent.parent
9 | CHANGELOG_MD = REPO_ROOT / "CHANGELOG.md"
10 | RELEASE_NOTES_MD = REPO_ROOT / "docs" / "release-notes.md"
11 |
12 |
13 | def get_releases() -> str:
14 | with CHANGELOG_MD.open() as f:
15 | changelog_md = f.read()
16 |
17 | sections = changelog_md.split("\n## ")[1:]
18 | unreleased = sections.pop(0)
19 | # small sanity check
20 | if not unreleased.startswith("[Unreleased]"):
21 | raise ValueError("Unexpected CHANGELOG.md format, aborting!")
22 |
23 | return "\n\n".join(f"## {section.strip()}" for section in sections)
24 |
25 |
26 | def main() -> None:
27 | releases = get_releases()
28 |
29 | release_notes_md = f"# Release Notes\n\n{releases}\n"
30 |
31 | with RELEASE_NOTES_MD.open("w") as f:
32 | f.write(release_notes_md)
33 |
34 | print("Successfully generated release notes!")
35 |
36 |
37 | if __name__ == "__main__":
38 | main()
39 |
--------------------------------------------------------------------------------
/scripts/test.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 |
3 | set -e
4 |
5 | command=( "$@" )
6 | if [ "${#command[@]}" -eq 0 ]; then
7 | command=( "pytest" )
8 | fi
9 |
10 | chrome_executable=$(uv run python -c "from zendriver.core.config import find_chrome_executable;print(find_chrome_executable())")
11 | echo "Chrome executable: $chrome_executable"
12 |
13 | chrome_version=$(uv run python -c "import os, subprocess, sys; path = r'$chrome_executable'; print(subprocess.run([path, '--version'], capture_output=True, text=True).stdout.strip()) if os.name != 'nt' else print('SKIP: Windows chrome.exe may not return version')")
14 | echo "Chrome version: $chrome_version"
15 |
16 | set -x
17 | uv run "${command[@]}"
18 |
--------------------------------------------------------------------------------
/tests/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM ghcr.io/stephanlensky/swayvnc-chrome:latest
2 |
3 | ENV PYTHONUNBUFFERED=1
4 |
5 | COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/
6 |
7 | ENV UV_COMPILE_BYTECODE=1
8 | ENV UV_LINK_MODE=copy
9 |
10 | # Make directory for the app
11 | RUN mkdir /app
12 | RUN chown $DOCKER_USER:$DOCKER_USER /app
13 |
14 | # Switch to the non-root user
15 | USER $DOCKER_USER
16 |
17 | # Add a hotkey to swaywm config for starting next test (for ZENDRIVER_PAUSE_AFTER_TEST=true)
18 | RUN echo 'bindsym Mod4+Return exec /app/tests/next_test.sh' >> ~/.config/sway/config
19 |
20 | # Set the working directory
21 | WORKDIR /app
22 |
23 | # Install python
24 | RUN uv python install 3.13
25 |
26 | # Install the Python project's dependencies using the lockfile and settings
27 | COPY --chown=$DOCKER_USER:$DOCKER_USER pyproject.toml uv.lock /app/
28 | RUN --mount=type=cache,target=/home/$DOCKER_USER/.cache/uv,uid=$PUID,gid=$PGID \
29 | uv sync --frozen --no-install-project
30 |
31 | # Then, add the rest of the project source code and install it
32 | # Installing separately from its dependencies allows optimal layer caching
33 | COPY --chown=$DOCKER_USER:$DOCKER_USER . /app
34 |
35 | # Add binaries from the project's virtual environment to the PATH
36 | ENV PATH="/app/.venv/bin:$PATH"
37 |
38 | # Sync the project's dependencies and install the project
39 | RUN --mount=type=cache,target=/home/$DOCKER_USER/.cache/uv,uid=$PUID,gid=$PGID \
40 | uv sync --frozen
41 |
42 | USER root
43 | # Pass custom command to entrypoint script provided by the base image
44 | ENTRYPOINT ["/entrypoint.sh", "./scripts/test.sh"]
45 |
--------------------------------------------------------------------------------
/tests/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/tests/__init__.py
--------------------------------------------------------------------------------
/tests/bot_detection/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/tests/bot_detection/__init__.py
--------------------------------------------------------------------------------
/tests/bot_detection/test_browserscan.py:
--------------------------------------------------------------------------------
1 | import zendriver as zd
2 |
3 |
4 | async def test_browserscan(browser: zd.Browser):
5 | page = await browser.get("https://www.browserscan.net/bot-detection")
6 |
7 | # wait for the page to fully load
8 | await page.wait_for_ready_state("complete")
9 |
10 | # give the javascript some time to finish executing
11 | await page.wait(2)
12 |
13 | element = await page.find_element_by_text("Test Results:")
14 | assert (
15 | element is not None
16 | and element.parent is not None
17 | and isinstance(element.parent.children[-1], zd.Element)
18 | )
19 | assert element.parent.children[-1].text == "Normal"
20 |
--------------------------------------------------------------------------------
/tests/conftest.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import logging
3 | import os
4 | import signal
5 | import sys
6 | from contextlib import AbstractAsyncContextManager
7 | from enum import Enum
8 | from threading import Event
9 | from typing import AsyncGenerator
10 |
11 | import pytest
12 |
13 | import zendriver as zd
14 |
15 | logger = logging.getLogger(__name__)
16 |
17 |
18 | class BrowserMode(Enum):
19 | HEADLESS = "headless"
20 | HEADFUL = "headful"
21 | ALL = "all"
22 |
23 | @property
24 | def fixture_params(self):
25 | if self == BrowserMode.HEADLESS:
26 | return [{"headless": True}]
27 | elif self == BrowserMode.HEADFUL:
28 | return [{"headless": False}]
29 | elif self == BrowserMode.ALL:
30 | return [{"headless": True}, {"headless": False}]
31 |
32 |
33 | NEXT_TEST_EVENT = Event()
34 |
35 |
36 | class TestConfig:
37 | BROWSER_MODE = BrowserMode(os.getenv("ZENDRIVER_TEST_BROWSERS", "all"))
38 | PAUSE_AFTER_TEST = os.getenv("ZENDRIVER_PAUSE_AFTER_TEST", "false") == "true"
39 | SANDBOX = os.getenv("ZENDRIVER_TEST_SANDBOX", "false") == "true"
40 | USE_WAYLAND = os.getenv("WAYLAND_DISPLAY") is not None
41 |
42 |
43 | class CreateBrowser(AbstractAsyncContextManager):
44 | def __init__(
45 | self,
46 | *,
47 | headless: bool = True,
48 | sandbox: bool = TestConfig.SANDBOX,
49 | browser_args: list[str] | None = None,
50 | browser_connection_max_tries: int = 15,
51 | browser_connection_timeout: float = 3.0,
52 | ):
53 | args = []
54 | if not headless and TestConfig.USE_WAYLAND:
55 | # use wayland backend instead of x11
56 | args.extend(
57 | ["--disable-features=UseOzonePlatform", "--ozone-platform=wayland"]
58 | )
59 | if browser_args is not None:
60 | args.extend(browser_args)
61 |
62 | self.config = zd.Config(
63 | headless=headless,
64 | sandbox=sandbox,
65 | browser_args=args,
66 | browser_connection_max_tries=browser_connection_max_tries,
67 | browser_connection_timeout=browser_connection_timeout,
68 | )
69 |
70 | self.browser: zd.Browser | None = None
71 | self.browser_pid: int | None = None
72 |
73 | async def __aenter__(self) -> zd.Browser:
74 | self.browser = await zd.start(self.config)
75 | browser_pid = self.browser._process_pid
76 | assert browser_pid is not None and browser_pid > 0
77 | await self.browser.wait(0)
78 | return self.browser
79 |
80 | async def __aexit__(self, exc_type, exc_val, exc_tb):
81 | if self.browser is not None:
82 | await self.browser.stop()
83 | assert self.browser_pid is None
84 |
85 |
86 | @pytest.fixture
87 | def create_browser() -> type[CreateBrowser]:
88 | if sys.platform == "win32":
89 | asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy()) # type: ignore
90 |
91 | return CreateBrowser
92 |
93 |
94 | @pytest.fixture(params=TestConfig.BROWSER_MODE.fixture_params)
95 | def headless(request: pytest.FixtureRequest) -> bool:
96 | return request.param["headless"]
97 |
98 |
99 | @pytest.fixture
100 | async def browser(
101 | headless: bool, create_browser: type[CreateBrowser]
102 | ) -> AsyncGenerator[zd.Browser, None]:
103 | NEXT_TEST_EVENT.clear()
104 |
105 | async with create_browser(headless=headless) as browser:
106 | yield browser
107 |
108 | if TestConfig.PAUSE_AFTER_TEST:
109 | logger.info(
110 | "Pausing after test. Send next test hotkey (default Mod+Return) to continue to next test"
111 | )
112 | NEXT_TEST_EVENT.wait()
113 | await browser.stop()
114 | assert browser._process_pid is None
115 |
116 |
117 | # signal handler for starting next test
118 | def handle_next_test(signum, frame):
119 | if not TestConfig.PAUSE_AFTER_TEST:
120 | logger.warning(
121 | "Next test signal received, but ZENDRIVER_PAUSE_AFTER_TEST is not set."
122 | )
123 | logger.warning(
124 | "To enable pausing after each test, set ZENDRIVER_PAUSE_AFTER_TEST=true"
125 | )
126 | return
127 |
128 | NEXT_TEST_EVENT.set()
129 |
130 |
131 | if hasattr(signal, "SIGUSR1"):
132 | signal.signal(signal.SIGUSR1, handle_next_test)
133 | else:
134 | logger.warning(
135 | "SIGUSR1 not available on this platform, handle_next_test will not be called."
136 | )
137 |
--------------------------------------------------------------------------------
/tests/core/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/tests/core/__init__.py
--------------------------------------------------------------------------------
/tests/core/test_browser.py:
--------------------------------------------------------------------------------
1 | import pytest
2 | from pytest_mock import MockerFixture
3 |
4 | import zendriver as zd
5 | from tests.conftest import CreateBrowser
6 |
7 |
8 | async def test_connection_error_raises_exception_and_logs_stderr(
9 | create_browser: type[CreateBrowser],
10 | mocker: MockerFixture,
11 | caplog: pytest.LogCaptureFixture,
12 | ):
13 | mocker.patch(
14 | "zendriver.core.browser.Browser.test_connection",
15 | return_value=False,
16 | )
17 | with caplog.at_level("INFO"):
18 | with pytest.raises(Exception):
19 | async with create_browser(
20 | browser_connection_max_tries=1, browser_connection_timeout=0.1
21 | ) as _:
22 | pass
23 | assert "Browser stderr" in caplog.text
24 |
25 |
26 | async def test_get_content_gets_html_content(browser: zd.Browser):
27 | page = await browser.get("https://example.com")
28 | content = await page.get_content()
29 | assert content.lower().startswith("")
30 |
31 |
32 | async def test_update_target_sets_target_title(browser: zd.Browser):
33 | page = await browser.get("https://example.com")
34 | await page.update_target()
35 | assert page.target
36 | assert page.target.title == "Example Domain"
37 |
--------------------------------------------------------------------------------
/tests/core/test_multiple_browsers.py:
--------------------------------------------------------------------------------
1 | import zendriver as zd
2 | from tests.conftest import CreateBrowser
3 |
4 |
5 | async def test_multiple_browsers_diff_userdata(create_browser: type[CreateBrowser]):
6 | config = create_browser().config
7 |
8 | browser1 = await zd.start(config)
9 | browser2 = await zd.start(config)
10 | browser3 = await zd.start(config)
11 |
12 | assert not browser1.config.uses_custom_data_dir
13 | assert not browser2.config.uses_custom_data_dir
14 | assert not browser3.config.uses_custom_data_dir
15 |
16 | # make sure ports are unique
17 | ports = {browser1.config.port, browser2.config.port, browser3.config.port}
18 | assert len(ports) == 3
19 |
20 | # make sure user data dirs are unique
21 | udds = {
22 | browser1.config.user_data_dir,
23 | browser2.config.user_data_dir,
24 | browser3.config.user_data_dir,
25 | }
26 | assert len(udds) == 3
27 |
28 | page1 = await browser1.get("https://example.com/one")
29 | await page1
30 | assert page1.target
31 | assert page1.target.title == "Example Domain"
32 |
33 | page2 = await browser2.get("https://example.com/two")
34 | await page2
35 | assert page2.target
36 | assert page2.target.title == "Example Domain"
37 |
38 | page3 = await browser3.get("https://example.com/three")
39 | await page3
40 | assert page3.target
41 | assert page3.target.title == "Example Domain"
42 |
43 | await browser1.stop()
44 | await browser2.stop()
45 | await browser3.stop()
46 |
--------------------------------------------------------------------------------
/tests/core/test_tab.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 |
3 | import pytest
4 |
5 | import zendriver as zd
6 | from tests.sample_data import sample_file
7 |
8 |
9 | async def test_set_user_agent_sets_navigator_values(browser: zd.Browser):
10 | tab = browser.main_tab
11 |
12 | await tab.set_user_agent(
13 | "Test user agent", accept_language="testLang", platform="TestPlatform"
14 | )
15 |
16 | navigator_user_agent = await tab.evaluate("navigator.userAgent")
17 | navigator_language = await tab.evaluate("navigator.language")
18 | navigator_platform = await tab.evaluate("navigator.platform")
19 | assert navigator_user_agent == "Test user agent"
20 | assert navigator_language == "testLang"
21 | assert navigator_platform == "TestPlatform"
22 |
23 |
24 | async def test_set_user_agent_defaults_existing_user_agent(browser: zd.Browser):
25 | tab = browser.main_tab
26 | existing_user_agent = await tab.evaluate("navigator.userAgent")
27 |
28 | await tab.set_user_agent(accept_language="testLang")
29 |
30 | navigator_user_agent = await tab.evaluate("navigator.userAgent")
31 | navigator_language = await tab.evaluate("navigator.language")
32 | assert navigator_user_agent == existing_user_agent
33 | assert navigator_language == "testLang"
34 |
35 |
36 | async def test_find_finds_element_by_text(browser: zd.Browser):
37 | tab = await browser.get(sample_file("groceries.html"))
38 |
39 | result = await tab.find("Apples")
40 |
41 | assert result is not None
42 | assert result.tag == "li"
43 | assert result.text == "Apples"
44 |
45 |
46 | async def test_find_times_out_if_element_not_found(browser: zd.Browser):
47 | tab = await browser.get(sample_file("groceries.html"))
48 |
49 | with pytest.raises(asyncio.TimeoutError):
50 | await tab.find("Clothes", timeout=1)
51 |
52 |
53 | async def test_select(browser: zd.Browser):
54 | tab = await browser.get(sample_file("groceries.html"))
55 |
56 | result = await tab.select("li[aria-label^='Apples']")
57 |
58 | assert result is not None
59 | assert result.tag == "li"
60 | assert result.text == "Apples"
61 |
62 |
63 | async def test_add_handler_type_event(browser: zd.Browser):
64 | tab = await browser.get(sample_file("groceries.html"))
65 |
66 | async def request_handler_1(event):
67 | pass
68 |
69 | async def request_handler_2(event):
70 | pass
71 |
72 | assert len(tab.handlers) == 0
73 |
74 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler_1)
75 |
76 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler_2)
77 |
78 | assert len(tab.handlers) == 1
79 | assert len(tab.handlers[zd.cdp.network.RequestWillBeSent]) == 2
80 | assert tab.handlers[zd.cdp.network.RequestWillBeSent] == [
81 | request_handler_1,
82 | request_handler_2,
83 | ]
84 |
85 |
86 | async def test_add_handler_module_event(browser: zd.Browser):
87 | tab = await browser.get(sample_file("groceries.html"))
88 |
89 | async def request_handler(event):
90 | pass
91 |
92 | assert len(tab.handlers) == 0
93 |
94 | tab.add_handler(zd.cdp.network, request_handler)
95 |
96 | assert len(tab.handlers) == 27
97 |
98 |
99 | async def test_remove_handlers(browser: zd.Browser):
100 | tab = await browser.get(sample_file("groceries.html"))
101 |
102 | async def request_handler(event):
103 | pass
104 |
105 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler)
106 | assert len(tab.handlers) == 1
107 |
108 | tab.remove_handlers()
109 | assert len(tab.handlers) == 0
110 |
111 |
112 | async def test_remove_handlers_specific_event(browser: zd.Browser):
113 | tab = await browser.get(sample_file("groceries.html"))
114 |
115 | async def request_handler(event):
116 | pass
117 |
118 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler)
119 | assert len(tab.handlers) == 1
120 |
121 | tab.remove_handlers(
122 | zd.cdp.network.RequestWillBeSent,
123 | )
124 | assert len(tab.handlers) == 0
125 |
126 |
127 | async def test_remove_specific_handler(browser: zd.Browser):
128 | tab = await browser.get(sample_file("groceries.html"))
129 |
130 | async def request_handler_1(event):
131 | pass
132 |
133 | async def request_handler_2(event):
134 | pass
135 |
136 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler_1)
137 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler_2)
138 | assert len(tab.handlers) == 1
139 | assert len(tab.handlers[zd.cdp.network.RequestWillBeSent]) == 2
140 |
141 | tab.remove_handlers(zd.cdp.network.RequestWillBeSent, request_handler_1)
142 | assert len(tab.handlers) == 1
143 | assert len(tab.handlers[zd.cdp.network.RequestWillBeSent]) == 1
144 |
145 |
146 | async def test_remove_handlers_without_event(browser: zd.Browser):
147 | tab = await browser.get(sample_file("groceries.html"))
148 |
149 | async def request_handler(event):
150 | pass
151 |
152 | tab.add_handler(zd.cdp.network.RequestWillBeSent, request_handler)
153 | assert len(tab.handlers) == 1
154 |
155 | with pytest.raises(ValueError) as e:
156 | tab.remove_handlers(handler=request_handler)
157 | assert str(e) == "if handler is provided, event_type should be provided as well"
158 |
159 |
160 | async def test_wait_for_ready_state(browser: zd.Browser):
161 | tab = await browser.get(sample_file("groceries.html"))
162 |
163 | await tab.wait_for_ready_state("complete")
164 |
165 | ready_state = await tab.evaluate("document.readyState")
166 | assert ready_state == "complete"
167 |
168 |
169 | async def test_expect_request(browser: zd.Browser):
170 | tab = browser.main_tab
171 |
172 | async with tab.expect_request(sample_file("groceries.html")) as request_info:
173 | await tab.get(sample_file("groceries.html"))
174 | req = await asyncio.wait_for(request_info.value, timeout=3)
175 | assert type(req) is zd.cdp.network.RequestWillBeSent
176 | assert type(req.request) is zd.cdp.network.Request
177 | assert req.request.url == sample_file("groceries.html")
178 | assert req.request_id is not None
179 |
180 | response_body = await request_info.response_body
181 | assert response_body is not None
182 | assert type(response_body) is tuple
183 |
184 |
185 | async def test_expect_response(browser: zd.Browser):
186 | tab = browser.main_tab
187 |
188 | async with tab.expect_response(sample_file("groceries.html")) as response_info:
189 | await tab.get(sample_file("groceries.html"))
190 | resp = await asyncio.wait_for(response_info.value, timeout=3)
191 | assert type(resp) is zd.cdp.network.ResponseReceived
192 | assert type(resp.response) is zd.cdp.network.Response
193 | assert resp.request_id is not None
194 |
195 | response_body = await response_info.response_body
196 | assert response_body is not None
197 | assert type(response_body) is tuple
198 |
199 |
200 | async def test_expect_download(browser: zd.Browser):
201 | tab = browser.main_tab
202 |
203 | async with tab.expect_download() as download_ex:
204 | await tab.get(sample_file("groceries.html"))
205 | await (await tab.select("#download_file")).click()
206 | download = await asyncio.wait_for(download_ex.value, timeout=3)
207 | assert type(download) is zd.cdp.browser.DownloadWillBegin
208 | assert download.url is not None
209 |
--------------------------------------------------------------------------------
/tests/docs/__init__.py:
--------------------------------------------------------------------------------
1 | import importlib.util
2 | import sys
3 | from pathlib import Path
4 | from types import ModuleType
5 |
6 |
7 | def import_from_path(file_path: Path | str) -> ModuleType:
8 | if isinstance(file_path, str):
9 | file_path = Path(file_path)
10 |
11 | module_name = file_path.stem
12 | spec = importlib.util.spec_from_file_location(module_name, file_path)
13 | if spec is None or spec.loader is None:
14 | raise ImportError(f"Cannot find module {module_name}")
15 |
16 | module = importlib.util.module_from_spec(spec)
17 | sys.modules[module_name] = module
18 | spec.loader.exec_module(module)
19 | return module
20 |
--------------------------------------------------------------------------------
/tests/docs/conftest.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import Mock
2 |
3 | import pytest
4 | from pytest_mock import MockerFixture
5 |
6 | from zendriver.core.browser import Browser
7 |
8 |
9 | @pytest.fixture
10 | def mock_print(mocker: MockerFixture) -> Mock:
11 | return mocker.patch("builtins.print")
12 |
13 |
14 | @pytest.fixture
15 | def mock_start(mocker: MockerFixture, browser: Browser) -> Mock:
16 | return mocker.patch("zendriver.start", return_value=browser)
17 |
--------------------------------------------------------------------------------
/tests/docs/tutorials/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/tests/docs/tutorials/__init__.py
--------------------------------------------------------------------------------
/tests/docs/tutorials/test_account_creation_tutorial.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import Mock
2 |
3 | from pytest_mock import MockerFixture
4 |
5 | from tests.docs import import_from_path
6 |
7 |
8 | async def test_account_creation_tutorial_1(
9 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
10 | ) -> None:
11 | module = import_from_path("docs/tutorials/tutorial-code/account-creation-1.py")
12 |
13 | await module.main() # just loading the page
14 |
15 |
16 | async def test_account_creation_tutorial_2(
17 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
18 | ) -> None:
19 | module = import_from_path("docs/tutorials/tutorial-code/account-creation-2.py")
20 |
21 | await module.main()
22 |
23 | mock_print.assert_has_calls(
24 | [
25 | mocker.call("Login successful"),
26 | ]
27 | )
28 |
--------------------------------------------------------------------------------
/tests/docs/tutorials/test_api_responses_tutorial.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import Mock
2 |
3 | from pytest_mock import MockerFixture
4 |
5 | from tests.docs import import_from_path
6 |
7 |
8 | async def test_api_responses_tutorial_1(
9 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
10 | ) -> None:
11 | module = import_from_path("docs/tutorials/tutorial-code/api-responses-1.py")
12 |
13 | await module.main()
14 |
15 |
16 | async def test_api_responses_tutorial_2(
17 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
18 | ) -> None:
19 | module = import_from_path("docs/tutorials/tutorial-code/api-responses-2.py")
20 |
21 | await module.main()
22 |
23 | mock_print.assert_any_call(
24 | "Successfully read user data response for user:", "Zendriver"
25 | )
26 |
--------------------------------------------------------------------------------
/tests/docs/tutorials/test_infinite_scrolling_tutorial.py:
--------------------------------------------------------------------------------
1 | from unittest.mock import Mock
2 |
3 | from pytest_mock import MockerFixture
4 |
5 | from tests.docs import import_from_path
6 |
7 |
8 | async def test_infinite_scrolling_tutorial_1(
9 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
10 | ) -> None:
11 | module = import_from_path("docs/tutorials/tutorial-code/infinite-scrolling-1.py")
12 |
13 | await module.main()
14 |
15 | mock_print.assert_called_once_with([])
16 |
17 |
18 | async def test_infinite_scrolling_tutorial_2(
19 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
20 | ) -> None:
21 | module = import_from_path("docs/tutorials/tutorial-code/infinite-scrolling-2.py")
22 |
23 | await module.main()
24 |
25 | mock_print.assert_has_calls(
26 | [mocker.call(f"Card {i}") for i in range(1, 11)],
27 | )
28 |
29 |
30 | async def test_infinite_scrolling_tutorial_3(
31 | mocker: MockerFixture, mock_print: Mock, mock_start: Mock
32 | ) -> None:
33 | module = import_from_path("docs/tutorials/tutorial-code/infinite-scrolling-3.py")
34 |
35 | await module.main()
36 |
37 | mock_print.assert_has_calls(
38 | [
39 | mocker.call("Loaded new cards. Current count:", 10),
40 | mocker.call("Loaded new cards. Current count:", 20),
41 | mocker.call("Loaded new cards. Current count:", 30),
42 | mocker.call("Lucky card found: Card 27"),
43 | ]
44 | )
45 |
--------------------------------------------------------------------------------
/tests/next_test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | # Script to be run by "Next test" hotkey (default Mod+Return) configured in Dockerfile.
3 | #
4 | # Sends a SIGUSR1 signal to the pytest process to trigger the next test to run.
5 | # (Applies only when ZENDRIVER_PAUSE_AFTER_TEST env variable is set to true)
6 |
7 | set -e
8 |
9 | pkill -USR1 -f "/app/.venv/bin/python /app/.venv/bin/pytest tests"
10 |
--------------------------------------------------------------------------------
/tests/sample_data/__init__.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 |
4 | def sample_file(name: str) -> str:
5 | path = (Path(__file__).parent / name).resolve()
6 | return path.as_uri()
7 |
--------------------------------------------------------------------------------
/tests/sample_data/groceries.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Grocery List
5 |
26 |
27 |
28 |
29 | Grocery List
30 |
31 | - Apples
32 | - Bananas
33 | - Carrots
34 | - Donuts
35 | - Eggs
36 | - French Fries
37 | - Grapes
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/zendriver/__init__.py:
--------------------------------------------------------------------------------
1 | from zendriver import cdp
2 | from zendriver._version import __version__
3 | from zendriver.core import util
4 | from zendriver.core._contradict import (
5 | ContraDict, # noqa
6 | cdict,
7 | )
8 | from zendriver.core.browser import Browser
9 | from zendriver.core.config import Config
10 | from zendriver.core.connection import Connection
11 | from zendriver.core.element import Element
12 | from zendriver.core.tab import Tab
13 | from zendriver.core.util import loop, start
14 |
15 | __all__ = [
16 | "__version__",
17 | "loop",
18 | "Browser",
19 | "Tab",
20 | "cdp",
21 | "Config",
22 | "start",
23 | "util",
24 | "Element",
25 | "ContraDict",
26 | "cdict",
27 | "Connection",
28 | ]
29 |
--------------------------------------------------------------------------------
/zendriver/_version.py:
--------------------------------------------------------------------------------
1 | __version__ = "0.8.0"
2 |
--------------------------------------------------------------------------------
/zendriver/cdp/README.md:
--------------------------------------------------------------------------------
1 | ## Generated by PyCDP
2 |
3 | The modules of this package were generated by [pycdp], do not modify their contents because the
4 | changes will be overwritten in next generations.
5 |
--------------------------------------------------------------------------------
/zendriver/cdp/__init__.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 |
6 | from . import (
7 | accessibility,
8 | animation,
9 | audits,
10 | autofill,
11 | background_service,
12 | bluetooth_emulation,
13 | browser,
14 | css,
15 | cache_storage,
16 | cast,
17 | console,
18 | dom,
19 | dom_debugger,
20 | dom_snapshot,
21 | dom_storage,
22 | debugger,
23 | device_access,
24 | device_orientation,
25 | emulation,
26 | event_breakpoints,
27 | extensions,
28 | fed_cm,
29 | fetch,
30 | file_system,
31 | headless_experimental,
32 | heap_profiler,
33 | io,
34 | indexed_db,
35 | input_,
36 | inspector,
37 | layer_tree,
38 | log,
39 | media,
40 | memory,
41 | network,
42 | overlay,
43 | pwa,
44 | page,
45 | performance,
46 | performance_timeline,
47 | preload,
48 | profiler,
49 | runtime,
50 | schema,
51 | security,
52 | service_worker,
53 | storage,
54 | system_info,
55 | target,
56 | tethering,
57 | tracing,
58 | web_audio,
59 | web_authn,
60 | )
61 |
--------------------------------------------------------------------------------
/zendriver/cdp/autofill.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Autofill (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 | from . import dom
15 | from . import page
16 |
17 |
18 | @dataclass
19 | class CreditCard:
20 | #: 16-digit credit card number.
21 | number: str
22 |
23 | #: Name of the credit card owner.
24 | name: str
25 |
26 | #: 2-digit expiry month.
27 | expiry_month: str
28 |
29 | #: 4-digit expiry year.
30 | expiry_year: str
31 |
32 | #: 3-digit card verification code.
33 | cvc: str
34 |
35 | def to_json(self) -> T_JSON_DICT:
36 | json: T_JSON_DICT = dict()
37 | json["number"] = self.number
38 | json["name"] = self.name
39 | json["expiryMonth"] = self.expiry_month
40 | json["expiryYear"] = self.expiry_year
41 | json["cvc"] = self.cvc
42 | return json
43 |
44 | @classmethod
45 | def from_json(cls, json: T_JSON_DICT) -> CreditCard:
46 | return cls(
47 | number=str(json["number"]),
48 | name=str(json["name"]),
49 | expiry_month=str(json["expiryMonth"]),
50 | expiry_year=str(json["expiryYear"]),
51 | cvc=str(json["cvc"]),
52 | )
53 |
54 |
55 | @dataclass
56 | class AddressField:
57 | #: address field name, for example GIVEN_NAME.
58 | name: str
59 |
60 | #: address field value, for example Jon Doe.
61 | value: str
62 |
63 | def to_json(self) -> T_JSON_DICT:
64 | json: T_JSON_DICT = dict()
65 | json["name"] = self.name
66 | json["value"] = self.value
67 | return json
68 |
69 | @classmethod
70 | def from_json(cls, json: T_JSON_DICT) -> AddressField:
71 | return cls(
72 | name=str(json["name"]),
73 | value=str(json["value"]),
74 | )
75 |
76 |
77 | @dataclass
78 | class AddressFields:
79 | """
80 | A list of address fields.
81 | """
82 |
83 | fields: typing.List[AddressField]
84 |
85 | def to_json(self) -> T_JSON_DICT:
86 | json: T_JSON_DICT = dict()
87 | json["fields"] = [i.to_json() for i in self.fields]
88 | return json
89 |
90 | @classmethod
91 | def from_json(cls, json: T_JSON_DICT) -> AddressFields:
92 | return cls(
93 | fields=[AddressField.from_json(i) for i in json["fields"]],
94 | )
95 |
96 |
97 | @dataclass
98 | class Address:
99 | #: fields and values defining an address.
100 | fields: typing.List[AddressField]
101 |
102 | def to_json(self) -> T_JSON_DICT:
103 | json: T_JSON_DICT = dict()
104 | json["fields"] = [i.to_json() for i in self.fields]
105 | return json
106 |
107 | @classmethod
108 | def from_json(cls, json: T_JSON_DICT) -> Address:
109 | return cls(
110 | fields=[AddressField.from_json(i) for i in json["fields"]],
111 | )
112 |
113 |
114 | @dataclass
115 | class AddressUI:
116 | """
117 | Defines how an address can be displayed like in chrome://settings/addresses.
118 | Address UI is a two dimensional array, each inner array is an "address information line", and when rendered in a UI surface should be displayed as such.
119 | The following address UI for instance:
120 | [[{name: "GIVE_NAME", value: "Jon"}, {name: "FAMILY_NAME", value: "Doe"}], [{name: "CITY", value: "Munich"}, {name: "ZIP", value: "81456"}]]
121 | should allow the receiver to render:
122 | Jon Doe
123 | Munich 81456
124 | """
125 |
126 | #: A two dimension array containing the representation of values from an address profile.
127 | address_fields: typing.List[AddressFields]
128 |
129 | def to_json(self) -> T_JSON_DICT:
130 | json: T_JSON_DICT = dict()
131 | json["addressFields"] = [i.to_json() for i in self.address_fields]
132 | return json
133 |
134 | @classmethod
135 | def from_json(cls, json: T_JSON_DICT) -> AddressUI:
136 | return cls(
137 | address_fields=[AddressFields.from_json(i) for i in json["addressFields"]],
138 | )
139 |
140 |
141 | class FillingStrategy(enum.Enum):
142 | """
143 | Specified whether a filled field was done so by using the html autocomplete attribute or autofill heuristics.
144 | """
145 |
146 | AUTOCOMPLETE_ATTRIBUTE = "autocompleteAttribute"
147 | AUTOFILL_INFERRED = "autofillInferred"
148 |
149 | def to_json(self) -> str:
150 | return self.value
151 |
152 | @classmethod
153 | def from_json(cls, json: str) -> FillingStrategy:
154 | return cls(json)
155 |
156 |
157 | @dataclass
158 | class FilledField:
159 | #: The type of the field, e.g text, password etc.
160 | html_type: str
161 |
162 | #: the html id
163 | id_: str
164 |
165 | #: the html name
166 | name: str
167 |
168 | #: the field value
169 | value: str
170 |
171 | #: The actual field type, e.g FAMILY_NAME
172 | autofill_type: str
173 |
174 | #: The filling strategy
175 | filling_strategy: FillingStrategy
176 |
177 | #: The frame the field belongs to
178 | frame_id: page.FrameId
179 |
180 | #: The form field's DOM node
181 | field_id: dom.BackendNodeId
182 |
183 | def to_json(self) -> T_JSON_DICT:
184 | json: T_JSON_DICT = dict()
185 | json["htmlType"] = self.html_type
186 | json["id"] = self.id_
187 | json["name"] = self.name
188 | json["value"] = self.value
189 | json["autofillType"] = self.autofill_type
190 | json["fillingStrategy"] = self.filling_strategy.to_json()
191 | json["frameId"] = self.frame_id.to_json()
192 | json["fieldId"] = self.field_id.to_json()
193 | return json
194 |
195 | @classmethod
196 | def from_json(cls, json: T_JSON_DICT) -> FilledField:
197 | return cls(
198 | html_type=str(json["htmlType"]),
199 | id_=str(json["id"]),
200 | name=str(json["name"]),
201 | value=str(json["value"]),
202 | autofill_type=str(json["autofillType"]),
203 | filling_strategy=FillingStrategy.from_json(json["fillingStrategy"]),
204 | frame_id=page.FrameId.from_json(json["frameId"]),
205 | field_id=dom.BackendNodeId.from_json(json["fieldId"]),
206 | )
207 |
208 |
209 | def trigger(
210 | field_id: dom.BackendNodeId,
211 | card: CreditCard,
212 | frame_id: typing.Optional[page.FrameId] = None,
213 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
214 | """
215 | Trigger autofill on a form identified by the fieldId.
216 | If the field and related form cannot be autofilled, returns an error.
217 |
218 | :param field_id: Identifies a field that serves as an anchor for autofill.
219 | :param frame_id: *(Optional)* Identifies the frame that field belongs to.
220 | :param card: Credit card information to fill out the form. Credit card data is not saved.
221 | """
222 | params: T_JSON_DICT = dict()
223 | params["fieldId"] = field_id.to_json()
224 | if frame_id is not None:
225 | params["frameId"] = frame_id.to_json()
226 | params["card"] = card.to_json()
227 | cmd_dict: T_JSON_DICT = {
228 | "method": "Autofill.trigger",
229 | "params": params,
230 | }
231 | json = yield cmd_dict
232 |
233 |
234 | def set_addresses(
235 | addresses: typing.List[Address],
236 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
237 | """
238 | Set addresses so that developers can verify their forms implementation.
239 |
240 | :param addresses:
241 | """
242 | params: T_JSON_DICT = dict()
243 | params["addresses"] = [i.to_json() for i in addresses]
244 | cmd_dict: T_JSON_DICT = {
245 | "method": "Autofill.setAddresses",
246 | "params": params,
247 | }
248 | json = yield cmd_dict
249 |
250 |
251 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
252 | """
253 | Disables autofill domain notifications.
254 | """
255 | cmd_dict: T_JSON_DICT = {
256 | "method": "Autofill.disable",
257 | }
258 | json = yield cmd_dict
259 |
260 |
261 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
262 | """
263 | Enables autofill domain notifications.
264 | """
265 | cmd_dict: T_JSON_DICT = {
266 | "method": "Autofill.enable",
267 | }
268 | json = yield cmd_dict
269 |
270 |
271 | @event_class("Autofill.addressFormFilled")
272 | @dataclass
273 | class AddressFormFilled:
274 | """
275 | Emitted when an address form is filled.
276 | """
277 |
278 | #: Information about the fields that were filled
279 | filled_fields: typing.List[FilledField]
280 | #: An UI representation of the address used to fill the form.
281 | #: Consists of a 2D array where each child represents an address/profile line.
282 | address_ui: AddressUI
283 |
284 | @classmethod
285 | def from_json(cls, json: T_JSON_DICT) -> AddressFormFilled:
286 | return cls(
287 | filled_fields=[FilledField.from_json(i) for i in json["filledFields"]],
288 | address_ui=AddressUI.from_json(json["addressUi"]),
289 | )
290 |
--------------------------------------------------------------------------------
/zendriver/cdp/background_service.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: BackgroundService (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 | from . import network
15 | from . import service_worker
16 |
17 |
18 | class ServiceName(enum.Enum):
19 | """
20 | The Background Service that will be associated with the commands/events.
21 | Every Background Service operates independently, but they share the same
22 | API.
23 | """
24 |
25 | BACKGROUND_FETCH = "backgroundFetch"
26 | BACKGROUND_SYNC = "backgroundSync"
27 | PUSH_MESSAGING = "pushMessaging"
28 | NOTIFICATIONS = "notifications"
29 | PAYMENT_HANDLER = "paymentHandler"
30 | PERIODIC_BACKGROUND_SYNC = "periodicBackgroundSync"
31 |
32 | def to_json(self) -> str:
33 | return self.value
34 |
35 | @classmethod
36 | def from_json(cls, json: str) -> ServiceName:
37 | return cls(json)
38 |
39 |
40 | @dataclass
41 | class EventMetadata:
42 | """
43 | A key-value pair for additional event information to pass along.
44 | """
45 |
46 | key: str
47 |
48 | value: str
49 |
50 | def to_json(self) -> T_JSON_DICT:
51 | json: T_JSON_DICT = dict()
52 | json["key"] = self.key
53 | json["value"] = self.value
54 | return json
55 |
56 | @classmethod
57 | def from_json(cls, json: T_JSON_DICT) -> EventMetadata:
58 | return cls(
59 | key=str(json["key"]),
60 | value=str(json["value"]),
61 | )
62 |
63 |
64 | @dataclass
65 | class BackgroundServiceEvent:
66 | #: Timestamp of the event (in seconds).
67 | timestamp: network.TimeSinceEpoch
68 |
69 | #: The origin this event belongs to.
70 | origin: str
71 |
72 | #: The Service Worker ID that initiated the event.
73 | service_worker_registration_id: service_worker.RegistrationID
74 |
75 | #: The Background Service this event belongs to.
76 | service: ServiceName
77 |
78 | #: A description of the event.
79 | event_name: str
80 |
81 | #: An identifier that groups related events together.
82 | instance_id: str
83 |
84 | #: A list of event-specific information.
85 | event_metadata: typing.List[EventMetadata]
86 |
87 | #: Storage key this event belongs to.
88 | storage_key: str
89 |
90 | def to_json(self) -> T_JSON_DICT:
91 | json: T_JSON_DICT = dict()
92 | json["timestamp"] = self.timestamp.to_json()
93 | json["origin"] = self.origin
94 | json["serviceWorkerRegistrationId"] = (
95 | self.service_worker_registration_id.to_json()
96 | )
97 | json["service"] = self.service.to_json()
98 | json["eventName"] = self.event_name
99 | json["instanceId"] = self.instance_id
100 | json["eventMetadata"] = [i.to_json() for i in self.event_metadata]
101 | json["storageKey"] = self.storage_key
102 | return json
103 |
104 | @classmethod
105 | def from_json(cls, json: T_JSON_DICT) -> BackgroundServiceEvent:
106 | return cls(
107 | timestamp=network.TimeSinceEpoch.from_json(json["timestamp"]),
108 | origin=str(json["origin"]),
109 | service_worker_registration_id=service_worker.RegistrationID.from_json(
110 | json["serviceWorkerRegistrationId"]
111 | ),
112 | service=ServiceName.from_json(json["service"]),
113 | event_name=str(json["eventName"]),
114 | instance_id=str(json["instanceId"]),
115 | event_metadata=[EventMetadata.from_json(i) for i in json["eventMetadata"]],
116 | storage_key=str(json["storageKey"]),
117 | )
118 |
119 |
120 | def start_observing(
121 | service: ServiceName,
122 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
123 | """
124 | Enables event updates for the service.
125 |
126 | :param service:
127 | """
128 | params: T_JSON_DICT = dict()
129 | params["service"] = service.to_json()
130 | cmd_dict: T_JSON_DICT = {
131 | "method": "BackgroundService.startObserving",
132 | "params": params,
133 | }
134 | json = yield cmd_dict
135 |
136 |
137 | def stop_observing(
138 | service: ServiceName,
139 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
140 | """
141 | Disables event updates for the service.
142 |
143 | :param service:
144 | """
145 | params: T_JSON_DICT = dict()
146 | params["service"] = service.to_json()
147 | cmd_dict: T_JSON_DICT = {
148 | "method": "BackgroundService.stopObserving",
149 | "params": params,
150 | }
151 | json = yield cmd_dict
152 |
153 |
154 | def set_recording(
155 | should_record: bool, service: ServiceName
156 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
157 | """
158 | Set the recording state for the service.
159 |
160 | :param should_record:
161 | :param service:
162 | """
163 | params: T_JSON_DICT = dict()
164 | params["shouldRecord"] = should_record
165 | params["service"] = service.to_json()
166 | cmd_dict: T_JSON_DICT = {
167 | "method": "BackgroundService.setRecording",
168 | "params": params,
169 | }
170 | json = yield cmd_dict
171 |
172 |
173 | def clear_events(
174 | service: ServiceName,
175 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
176 | """
177 | Clears all stored data for the service.
178 |
179 | :param service:
180 | """
181 | params: T_JSON_DICT = dict()
182 | params["service"] = service.to_json()
183 | cmd_dict: T_JSON_DICT = {
184 | "method": "BackgroundService.clearEvents",
185 | "params": params,
186 | }
187 | json = yield cmd_dict
188 |
189 |
190 | @event_class("BackgroundService.recordingStateChanged")
191 | @dataclass
192 | class RecordingStateChanged:
193 | """
194 | Called when the recording state for the service has been updated.
195 | """
196 |
197 | is_recording: bool
198 | service: ServiceName
199 |
200 | @classmethod
201 | def from_json(cls, json: T_JSON_DICT) -> RecordingStateChanged:
202 | return cls(
203 | is_recording=bool(json["isRecording"]),
204 | service=ServiceName.from_json(json["service"]),
205 | )
206 |
207 |
208 | @event_class("BackgroundService.backgroundServiceEventReceived")
209 | @dataclass
210 | class BackgroundServiceEventReceived:
211 | """
212 | Called with all existing backgroundServiceEvents when enabled, and all new
213 | events afterwards if enabled and recording.
214 | """
215 |
216 | background_service_event: BackgroundServiceEvent
217 |
218 | @classmethod
219 | def from_json(cls, json: T_JSON_DICT) -> BackgroundServiceEventReceived:
220 | return cls(
221 | background_service_event=BackgroundServiceEvent.from_json(
222 | json["backgroundServiceEvent"]
223 | )
224 | )
225 |
--------------------------------------------------------------------------------
/zendriver/cdp/cast.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Cast (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | @dataclass
16 | class Sink:
17 | name: str
18 |
19 | id_: str
20 |
21 | #: Text describing the current session. Present only if there is an active
22 | #: session on the sink.
23 | session: typing.Optional[str] = None
24 |
25 | def to_json(self) -> T_JSON_DICT:
26 | json: T_JSON_DICT = dict()
27 | json["name"] = self.name
28 | json["id"] = self.id_
29 | if self.session is not None:
30 | json["session"] = self.session
31 | return json
32 |
33 | @classmethod
34 | def from_json(cls, json: T_JSON_DICT) -> Sink:
35 | return cls(
36 | name=str(json["name"]),
37 | id_=str(json["id"]),
38 | session=str(json["session"])
39 | if json.get("session", None) is not None
40 | else None,
41 | )
42 |
43 |
44 | def enable(
45 | presentation_url: typing.Optional[str] = None,
46 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
47 | """
48 | Starts observing for sinks that can be used for tab mirroring, and if set,
49 | sinks compatible with ``presentationUrl`` as well. When sinks are found, a
50 | ``sinksUpdated`` event is fired.
51 | Also starts observing for issue messages. When an issue is added or removed,
52 | an ``issueUpdated`` event is fired.
53 |
54 | :param presentation_url: *(Optional)*
55 | """
56 | params: T_JSON_DICT = dict()
57 | if presentation_url is not None:
58 | params["presentationUrl"] = presentation_url
59 | cmd_dict: T_JSON_DICT = {
60 | "method": "Cast.enable",
61 | "params": params,
62 | }
63 | json = yield cmd_dict
64 |
65 |
66 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
67 | """
68 | Stops observing for sinks and issues.
69 | """
70 | cmd_dict: T_JSON_DICT = {
71 | "method": "Cast.disable",
72 | }
73 | json = yield cmd_dict
74 |
75 |
76 | def set_sink_to_use(sink_name: str) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
77 | """
78 | Sets a sink to be used when the web page requests the browser to choose a
79 | sink via Presentation API, Remote Playback API, or Cast SDK.
80 |
81 | :param sink_name:
82 | """
83 | params: T_JSON_DICT = dict()
84 | params["sinkName"] = sink_name
85 | cmd_dict: T_JSON_DICT = {
86 | "method": "Cast.setSinkToUse",
87 | "params": params,
88 | }
89 | json = yield cmd_dict
90 |
91 |
92 | def start_desktop_mirroring(
93 | sink_name: str,
94 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
95 | """
96 | Starts mirroring the desktop to the sink.
97 |
98 | :param sink_name:
99 | """
100 | params: T_JSON_DICT = dict()
101 | params["sinkName"] = sink_name
102 | cmd_dict: T_JSON_DICT = {
103 | "method": "Cast.startDesktopMirroring",
104 | "params": params,
105 | }
106 | json = yield cmd_dict
107 |
108 |
109 | def start_tab_mirroring(
110 | sink_name: str,
111 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
112 | """
113 | Starts mirroring the tab to the sink.
114 |
115 | :param sink_name:
116 | """
117 | params: T_JSON_DICT = dict()
118 | params["sinkName"] = sink_name
119 | cmd_dict: T_JSON_DICT = {
120 | "method": "Cast.startTabMirroring",
121 | "params": params,
122 | }
123 | json = yield cmd_dict
124 |
125 |
126 | def stop_casting(sink_name: str) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
127 | """
128 | Stops the active Cast session on the sink.
129 |
130 | :param sink_name:
131 | """
132 | params: T_JSON_DICT = dict()
133 | params["sinkName"] = sink_name
134 | cmd_dict: T_JSON_DICT = {
135 | "method": "Cast.stopCasting",
136 | "params": params,
137 | }
138 | json = yield cmd_dict
139 |
140 |
141 | @event_class("Cast.sinksUpdated")
142 | @dataclass
143 | class SinksUpdated:
144 | """
145 | This is fired whenever the list of available sinks changes. A sink is a
146 | device or a software surface that you can cast to.
147 | """
148 |
149 | sinks: typing.List[Sink]
150 |
151 | @classmethod
152 | def from_json(cls, json: T_JSON_DICT) -> SinksUpdated:
153 | return cls(sinks=[Sink.from_json(i) for i in json["sinks"]])
154 |
155 |
156 | @event_class("Cast.issueUpdated")
157 | @dataclass
158 | class IssueUpdated:
159 | """
160 | This is fired whenever the outstanding issue/error message changes.
161 | ``issueMessage`` is empty if there is no issue.
162 | """
163 |
164 | issue_message: str
165 |
166 | @classmethod
167 | def from_json(cls, json: T_JSON_DICT) -> IssueUpdated:
168 | return cls(issue_message=str(json["issueMessage"]))
169 |
--------------------------------------------------------------------------------
/zendriver/cdp/console.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Console
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | @dataclass
16 | class ConsoleMessage:
17 | """
18 | Console message.
19 | """
20 |
21 | #: Message source.
22 | source: str
23 |
24 | #: Message severity.
25 | level: str
26 |
27 | #: Message text.
28 | text: str
29 |
30 | #: URL of the message origin.
31 | url: typing.Optional[str] = None
32 |
33 | #: Line number in the resource that generated this message (1-based).
34 | line: typing.Optional[int] = None
35 |
36 | #: Column number in the resource that generated this message (1-based).
37 | column: typing.Optional[int] = None
38 |
39 | def to_json(self) -> T_JSON_DICT:
40 | json: T_JSON_DICT = dict()
41 | json["source"] = self.source
42 | json["level"] = self.level
43 | json["text"] = self.text
44 | if self.url is not None:
45 | json["url"] = self.url
46 | if self.line is not None:
47 | json["line"] = self.line
48 | if self.column is not None:
49 | json["column"] = self.column
50 | return json
51 |
52 | @classmethod
53 | def from_json(cls, json: T_JSON_DICT) -> ConsoleMessage:
54 | return cls(
55 | source=str(json["source"]),
56 | level=str(json["level"]),
57 | text=str(json["text"]),
58 | url=str(json["url"]) if json.get("url", None) is not None else None,
59 | line=int(json["line"]) if json.get("line", None) is not None else None,
60 | column=int(json["column"])
61 | if json.get("column", None) is not None
62 | else None,
63 | )
64 |
65 |
66 | def clear_messages() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
67 | """
68 | Does nothing.
69 | """
70 | cmd_dict: T_JSON_DICT = {
71 | "method": "Console.clearMessages",
72 | }
73 | json = yield cmd_dict
74 |
75 |
76 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
77 | """
78 | Disables console domain, prevents further console messages from being reported to the client.
79 | """
80 | cmd_dict: T_JSON_DICT = {
81 | "method": "Console.disable",
82 | }
83 | json = yield cmd_dict
84 |
85 |
86 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
87 | """
88 | Enables console domain, sends the messages collected so far to the client by means of the
89 | ``messageAdded`` notification.
90 | """
91 | cmd_dict: T_JSON_DICT = {
92 | "method": "Console.enable",
93 | }
94 | json = yield cmd_dict
95 |
96 |
97 | @event_class("Console.messageAdded")
98 | @dataclass
99 | class MessageAdded:
100 | """
101 | Issued when new console message is added.
102 | """
103 |
104 | #: Console message that has been added.
105 | message: ConsoleMessage
106 |
107 | @classmethod
108 | def from_json(cls, json: T_JSON_DICT) -> MessageAdded:
109 | return cls(message=ConsoleMessage.from_json(json["message"]))
110 |
--------------------------------------------------------------------------------
/zendriver/cdp/database.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Database (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class DatabaseId(str):
16 | """
17 | Unique identifier of Database object.
18 | """
19 |
20 | def to_json(self) -> str:
21 | return self
22 |
23 | @classmethod
24 | def from_json(cls, json: str) -> DatabaseId:
25 | return cls(json)
26 |
27 | def __repr__(self):
28 | return "DatabaseId({})".format(super().__repr__())
29 |
30 |
31 | @dataclass
32 | class Database:
33 | """
34 | Database object.
35 | """
36 |
37 | #: Database ID.
38 | id_: DatabaseId
39 |
40 | #: Database domain.
41 | domain: str
42 |
43 | #: Database name.
44 | name: str
45 |
46 | #: Database version.
47 | version: str
48 |
49 | def to_json(self) -> T_JSON_DICT:
50 | json: T_JSON_DICT = dict()
51 | json["id"] = self.id_.to_json()
52 | json["domain"] = self.domain
53 | json["name"] = self.name
54 | json["version"] = self.version
55 | return json
56 |
57 | @classmethod
58 | def from_json(cls, json: T_JSON_DICT) -> Database:
59 | return cls(
60 | id_=DatabaseId.from_json(json["id"]),
61 | domain=str(json["domain"]),
62 | name=str(json["name"]),
63 | version=str(json["version"]),
64 | )
65 |
66 |
67 | @dataclass
68 | class Error:
69 | """
70 | Database error.
71 | """
72 |
73 | #: Error message.
74 | message: str
75 |
76 | #: Error code.
77 | code: int
78 |
79 | def to_json(self) -> T_JSON_DICT:
80 | json: T_JSON_DICT = dict()
81 | json["message"] = self.message
82 | json["code"] = self.code
83 | return json
84 |
85 | @classmethod
86 | def from_json(cls, json: T_JSON_DICT) -> Error:
87 | return cls(
88 | message=str(json["message"]),
89 | code=int(json["code"]),
90 | )
91 |
92 |
93 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
94 | """
95 | Disables database tracking, prevents database events from being sent to the client.
96 | """
97 | cmd_dict: T_JSON_DICT = {
98 | "method": "Database.disable",
99 | }
100 | json = yield cmd_dict
101 |
102 |
103 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
104 | """
105 | Enables database tracking, database events will now be delivered to the client.
106 | """
107 | cmd_dict: T_JSON_DICT = {
108 | "method": "Database.enable",
109 | }
110 | json = yield cmd_dict
111 |
112 |
113 | def execute_sql(
114 | database_id: DatabaseId, query: str
115 | ) -> typing.Generator[
116 | T_JSON_DICT,
117 | T_JSON_DICT,
118 | typing.Tuple[
119 | typing.Optional[typing.List[str]],
120 | typing.Optional[typing.List[typing.Any]],
121 | typing.Optional[Error],
122 | ],
123 | ]:
124 | """
125 | :param database_id:
126 | :param query:
127 | :returns: A tuple with the following items:
128 |
129 | 0. **columnNames** -
130 | 1. **values** -
131 | 2. **sqlError** -
132 | """
133 | params: T_JSON_DICT = dict()
134 | params["databaseId"] = database_id.to_json()
135 | params["query"] = query
136 | cmd_dict: T_JSON_DICT = {
137 | "method": "Database.executeSQL",
138 | "params": params,
139 | }
140 | json = yield cmd_dict
141 | return (
142 | [str(i) for i in json["columnNames"]]
143 | if json.get("columnNames", None) is not None
144 | else None,
145 | [i for i in json["values"]] if json.get("values", None) is not None else None,
146 | Error.from_json(json["sqlError"])
147 | if json.get("sqlError", None) is not None
148 | else None,
149 | )
150 |
151 |
152 | def get_database_table_names(
153 | database_id: DatabaseId,
154 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, typing.List[str]]:
155 | """
156 | :param database_id:
157 | :returns:
158 | """
159 | params: T_JSON_DICT = dict()
160 | params["databaseId"] = database_id.to_json()
161 | cmd_dict: T_JSON_DICT = {
162 | "method": "Database.getDatabaseTableNames",
163 | "params": params,
164 | }
165 | json = yield cmd_dict
166 | return [str(i) for i in json["tableNames"]]
167 |
168 |
169 | @event_class("Database.addDatabase")
170 | @dataclass
171 | class AddDatabase:
172 | database: Database
173 |
174 | @classmethod
175 | def from_json(cls, json: T_JSON_DICT) -> AddDatabase:
176 | return cls(database=Database.from_json(json["database"]))
177 |
--------------------------------------------------------------------------------
/zendriver/cdp/device_access.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: DeviceAccess (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class RequestId(str):
16 | """
17 | Device request id.
18 | """
19 |
20 | def to_json(self) -> str:
21 | return self
22 |
23 | @classmethod
24 | def from_json(cls, json: str) -> RequestId:
25 | return cls(json)
26 |
27 | def __repr__(self):
28 | return "RequestId({})".format(super().__repr__())
29 |
30 |
31 | class DeviceId(str):
32 | """
33 | A device id.
34 | """
35 |
36 | def to_json(self) -> str:
37 | return self
38 |
39 | @classmethod
40 | def from_json(cls, json: str) -> DeviceId:
41 | return cls(json)
42 |
43 | def __repr__(self):
44 | return "DeviceId({})".format(super().__repr__())
45 |
46 |
47 | @dataclass
48 | class PromptDevice:
49 | """
50 | Device information displayed in a user prompt to select a device.
51 | """
52 |
53 | id_: DeviceId
54 |
55 | #: Display name as it appears in a device request user prompt.
56 | name: str
57 |
58 | def to_json(self) -> T_JSON_DICT:
59 | json: T_JSON_DICT = dict()
60 | json["id"] = self.id_.to_json()
61 | json["name"] = self.name
62 | return json
63 |
64 | @classmethod
65 | def from_json(cls, json: T_JSON_DICT) -> PromptDevice:
66 | return cls(
67 | id_=DeviceId.from_json(json["id"]),
68 | name=str(json["name"]),
69 | )
70 |
71 |
72 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
73 | """
74 | Enable events in this domain.
75 | """
76 | cmd_dict: T_JSON_DICT = {
77 | "method": "DeviceAccess.enable",
78 | }
79 | json = yield cmd_dict
80 |
81 |
82 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
83 | """
84 | Disable events in this domain.
85 | """
86 | cmd_dict: T_JSON_DICT = {
87 | "method": "DeviceAccess.disable",
88 | }
89 | json = yield cmd_dict
90 |
91 |
92 | def select_prompt(
93 | id_: RequestId, device_id: DeviceId
94 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
95 | """
96 | Select a device in response to a DeviceAccess.deviceRequestPrompted event.
97 |
98 | :param id_:
99 | :param device_id:
100 | """
101 | params: T_JSON_DICT = dict()
102 | params["id"] = id_.to_json()
103 | params["deviceId"] = device_id.to_json()
104 | cmd_dict: T_JSON_DICT = {
105 | "method": "DeviceAccess.selectPrompt",
106 | "params": params,
107 | }
108 | json = yield cmd_dict
109 |
110 |
111 | def cancel_prompt(id_: RequestId) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
112 | """
113 | Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event.
114 |
115 | :param id_:
116 | """
117 | params: T_JSON_DICT = dict()
118 | params["id"] = id_.to_json()
119 | cmd_dict: T_JSON_DICT = {
120 | "method": "DeviceAccess.cancelPrompt",
121 | "params": params,
122 | }
123 | json = yield cmd_dict
124 |
125 |
126 | @event_class("DeviceAccess.deviceRequestPrompted")
127 | @dataclass
128 | class DeviceRequestPrompted:
129 | """
130 | A device request opened a user prompt to select a device. Respond with the
131 | selectPrompt or cancelPrompt command.
132 | """
133 |
134 | id_: RequestId
135 | devices: typing.List[PromptDevice]
136 |
137 | @classmethod
138 | def from_json(cls, json: T_JSON_DICT) -> DeviceRequestPrompted:
139 | return cls(
140 | id_=RequestId.from_json(json["id"]),
141 | devices=[PromptDevice.from_json(i) for i in json["devices"]],
142 | )
143 |
--------------------------------------------------------------------------------
/zendriver/cdp/device_orientation.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: DeviceOrientation (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | def clear_device_orientation_override() -> typing.Generator[
16 | T_JSON_DICT, T_JSON_DICT, None
17 | ]:
18 | """
19 | Clears the overridden Device Orientation.
20 | """
21 | cmd_dict: T_JSON_DICT = {
22 | "method": "DeviceOrientation.clearDeviceOrientationOverride",
23 | }
24 | json = yield cmd_dict
25 |
26 |
27 | def set_device_orientation_override(
28 | alpha: float, beta: float, gamma: float
29 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
30 | """
31 | Overrides the Device Orientation.
32 |
33 | :param alpha: Mock alpha
34 | :param beta: Mock beta
35 | :param gamma: Mock gamma
36 | """
37 | params: T_JSON_DICT = dict()
38 | params["alpha"] = alpha
39 | params["beta"] = beta
40 | params["gamma"] = gamma
41 | cmd_dict: T_JSON_DICT = {
42 | "method": "DeviceOrientation.setDeviceOrientationOverride",
43 | "params": params,
44 | }
45 | json = yield cmd_dict
46 |
--------------------------------------------------------------------------------
/zendriver/cdp/dom_storage.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: DOMStorage (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class SerializedStorageKey(str):
16 | def to_json(self) -> str:
17 | return self
18 |
19 | @classmethod
20 | def from_json(cls, json: str) -> SerializedStorageKey:
21 | return cls(json)
22 |
23 | def __repr__(self):
24 | return "SerializedStorageKey({})".format(super().__repr__())
25 |
26 |
27 | @dataclass
28 | class StorageId:
29 | """
30 | DOM Storage identifier.
31 | """
32 |
33 | #: Whether the storage is local storage (not session storage).
34 | is_local_storage: bool
35 |
36 | #: Security origin for the storage.
37 | security_origin: typing.Optional[str] = None
38 |
39 | #: Represents a key by which DOM Storage keys its CachedStorageAreas
40 | storage_key: typing.Optional[SerializedStorageKey] = None
41 |
42 | def to_json(self) -> T_JSON_DICT:
43 | json: T_JSON_DICT = dict()
44 | json["isLocalStorage"] = self.is_local_storage
45 | if self.security_origin is not None:
46 | json["securityOrigin"] = self.security_origin
47 | if self.storage_key is not None:
48 | json["storageKey"] = self.storage_key.to_json()
49 | return json
50 |
51 | @classmethod
52 | def from_json(cls, json: T_JSON_DICT) -> StorageId:
53 | return cls(
54 | is_local_storage=bool(json["isLocalStorage"]),
55 | security_origin=str(json["securityOrigin"])
56 | if json.get("securityOrigin", None) is not None
57 | else None,
58 | storage_key=SerializedStorageKey.from_json(json["storageKey"])
59 | if json.get("storageKey", None) is not None
60 | else None,
61 | )
62 |
63 |
64 | class Item(list):
65 | """
66 | DOM Storage item.
67 | """
68 |
69 | def to_json(self) -> typing.List[str]:
70 | return self
71 |
72 | @classmethod
73 | def from_json(cls, json: typing.List[str]) -> Item:
74 | return cls(json)
75 |
76 | def __repr__(self):
77 | return "Item({})".format(super().__repr__())
78 |
79 |
80 | def clear(storage_id: StorageId) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
81 | """
82 | :param storage_id:
83 | """
84 | params: T_JSON_DICT = dict()
85 | params["storageId"] = storage_id.to_json()
86 | cmd_dict: T_JSON_DICT = {
87 | "method": "DOMStorage.clear",
88 | "params": params,
89 | }
90 | json = yield cmd_dict
91 |
92 |
93 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
94 | """
95 | Disables storage tracking, prevents storage events from being sent to the client.
96 | """
97 | cmd_dict: T_JSON_DICT = {
98 | "method": "DOMStorage.disable",
99 | }
100 | json = yield cmd_dict
101 |
102 |
103 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
104 | """
105 | Enables storage tracking, storage events will now be delivered to the client.
106 | """
107 | cmd_dict: T_JSON_DICT = {
108 | "method": "DOMStorage.enable",
109 | }
110 | json = yield cmd_dict
111 |
112 |
113 | def get_dom_storage_items(
114 | storage_id: StorageId,
115 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, typing.List[Item]]:
116 | """
117 | :param storage_id:
118 | :returns:
119 | """
120 | params: T_JSON_DICT = dict()
121 | params["storageId"] = storage_id.to_json()
122 | cmd_dict: T_JSON_DICT = {
123 | "method": "DOMStorage.getDOMStorageItems",
124 | "params": params,
125 | }
126 | json = yield cmd_dict
127 | return [Item.from_json(i) for i in json["entries"]]
128 |
129 |
130 | def remove_dom_storage_item(
131 | storage_id: StorageId, key: str
132 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
133 | """
134 | :param storage_id:
135 | :param key:
136 | """
137 | params: T_JSON_DICT = dict()
138 | params["storageId"] = storage_id.to_json()
139 | params["key"] = key
140 | cmd_dict: T_JSON_DICT = {
141 | "method": "DOMStorage.removeDOMStorageItem",
142 | "params": params,
143 | }
144 | json = yield cmd_dict
145 |
146 |
147 | def set_dom_storage_item(
148 | storage_id: StorageId, key: str, value: str
149 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
150 | """
151 | :param storage_id:
152 | :param key:
153 | :param value:
154 | """
155 | params: T_JSON_DICT = dict()
156 | params["storageId"] = storage_id.to_json()
157 | params["key"] = key
158 | params["value"] = value
159 | cmd_dict: T_JSON_DICT = {
160 | "method": "DOMStorage.setDOMStorageItem",
161 | "params": params,
162 | }
163 | json = yield cmd_dict
164 |
165 |
166 | @event_class("DOMStorage.domStorageItemAdded")
167 | @dataclass
168 | class DomStorageItemAdded:
169 | storage_id: StorageId
170 | key: str
171 | new_value: str
172 |
173 | @classmethod
174 | def from_json(cls, json: T_JSON_DICT) -> DomStorageItemAdded:
175 | return cls(
176 | storage_id=StorageId.from_json(json["storageId"]),
177 | key=str(json["key"]),
178 | new_value=str(json["newValue"]),
179 | )
180 |
181 |
182 | @event_class("DOMStorage.domStorageItemRemoved")
183 | @dataclass
184 | class DomStorageItemRemoved:
185 | storage_id: StorageId
186 | key: str
187 |
188 | @classmethod
189 | def from_json(cls, json: T_JSON_DICT) -> DomStorageItemRemoved:
190 | return cls(
191 | storage_id=StorageId.from_json(json["storageId"]), key=str(json["key"])
192 | )
193 |
194 |
195 | @event_class("DOMStorage.domStorageItemUpdated")
196 | @dataclass
197 | class DomStorageItemUpdated:
198 | storage_id: StorageId
199 | key: str
200 | old_value: str
201 | new_value: str
202 |
203 | @classmethod
204 | def from_json(cls, json: T_JSON_DICT) -> DomStorageItemUpdated:
205 | return cls(
206 | storage_id=StorageId.from_json(json["storageId"]),
207 | key=str(json["key"]),
208 | old_value=str(json["oldValue"]),
209 | new_value=str(json["newValue"]),
210 | )
211 |
212 |
213 | @event_class("DOMStorage.domStorageItemsCleared")
214 | @dataclass
215 | class DomStorageItemsCleared:
216 | storage_id: StorageId
217 |
218 | @classmethod
219 | def from_json(cls, json: T_JSON_DICT) -> DomStorageItemsCleared:
220 | return cls(storage_id=StorageId.from_json(json["storageId"]))
221 |
--------------------------------------------------------------------------------
/zendriver/cdp/event_breakpoints.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: EventBreakpoints (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | def set_instrumentation_breakpoint(
16 | event_name: str,
17 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
18 | """
19 | Sets breakpoint on particular native event.
20 |
21 | :param event_name: Instrumentation name to stop on.
22 | """
23 | params: T_JSON_DICT = dict()
24 | params["eventName"] = event_name
25 | cmd_dict: T_JSON_DICT = {
26 | "method": "EventBreakpoints.setInstrumentationBreakpoint",
27 | "params": params,
28 | }
29 | json = yield cmd_dict
30 |
31 |
32 | def remove_instrumentation_breakpoint(
33 | event_name: str,
34 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
35 | """
36 | Removes breakpoint on particular native event.
37 |
38 | :param event_name: Instrumentation name to stop on.
39 | """
40 | params: T_JSON_DICT = dict()
41 | params["eventName"] = event_name
42 | cmd_dict: T_JSON_DICT = {
43 | "method": "EventBreakpoints.removeInstrumentationBreakpoint",
44 | "params": params,
45 | }
46 | json = yield cmd_dict
47 |
48 |
49 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
50 | """
51 | Removes all breakpoints
52 | """
53 | cmd_dict: T_JSON_DICT = {
54 | "method": "EventBreakpoints.disable",
55 | }
56 | json = yield cmd_dict
57 |
--------------------------------------------------------------------------------
/zendriver/cdp/extensions.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Extensions (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class StorageArea(enum.Enum):
16 | """
17 | Storage areas.
18 | """
19 |
20 | SESSION = "session"
21 | LOCAL = "local"
22 | SYNC = "sync"
23 | MANAGED = "managed"
24 |
25 | def to_json(self) -> str:
26 | return self.value
27 |
28 | @classmethod
29 | def from_json(cls, json: str) -> StorageArea:
30 | return cls(json)
31 |
32 |
33 | def load_unpacked(path: str) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, str]:
34 | """
35 | Installs an unpacked extension from the filesystem similar to
36 | --load-extension CLI flags. Returns extension ID once the extension
37 | has been installed. Available if the client is connected using the
38 | --remote-debugging-pipe flag and the --enable-unsafe-extension-debugging
39 | flag is set.
40 |
41 | :param path: Absolute file path.
42 | :returns: Extension id.
43 | """
44 | params: T_JSON_DICT = dict()
45 | params["path"] = path
46 | cmd_dict: T_JSON_DICT = {
47 | "method": "Extensions.loadUnpacked",
48 | "params": params,
49 | }
50 | json = yield cmd_dict
51 | return str(json["id"])
52 |
53 |
54 | def uninstall(id_: str) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
55 | """
56 | Uninstalls an unpacked extension (others not supported) from the profile.
57 | Available if the client is connected using the --remote-debugging-pipe flag
58 | and the --enable-unsafe-extension-debugging.
59 |
60 | :param id_: Extension id.
61 | """
62 | params: T_JSON_DICT = dict()
63 | params["id"] = id_
64 | cmd_dict: T_JSON_DICT = {
65 | "method": "Extensions.uninstall",
66 | "params": params,
67 | }
68 | json = yield cmd_dict
69 |
70 |
71 | def get_storage_items(
72 | id_: str, storage_area: StorageArea, keys: typing.Optional[typing.List[str]] = None
73 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, dict]:
74 | """
75 | Gets data from extension storage in the given ``storageArea``. If ``keys`` is
76 | specified, these are used to filter the result.
77 |
78 | :param id_: ID of extension.
79 | :param storage_area: StorageArea to retrieve data from.
80 | :param keys: *(Optional)* Keys to retrieve.
81 | :returns:
82 | """
83 | params: T_JSON_DICT = dict()
84 | params["id"] = id_
85 | params["storageArea"] = storage_area.to_json()
86 | if keys is not None:
87 | params["keys"] = [i for i in keys]
88 | cmd_dict: T_JSON_DICT = {
89 | "method": "Extensions.getStorageItems",
90 | "params": params,
91 | }
92 | json = yield cmd_dict
93 | return dict(json["data"])
94 |
95 |
96 | def remove_storage_items(
97 | id_: str, storage_area: StorageArea, keys: typing.List[str]
98 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
99 | """
100 | Removes ``keys`` from extension storage in the given ``storageArea``.
101 |
102 | :param id_: ID of extension.
103 | :param storage_area: StorageArea to remove data from.
104 | :param keys: Keys to remove.
105 | """
106 | params: T_JSON_DICT = dict()
107 | params["id"] = id_
108 | params["storageArea"] = storage_area.to_json()
109 | params["keys"] = [i for i in keys]
110 | cmd_dict: T_JSON_DICT = {
111 | "method": "Extensions.removeStorageItems",
112 | "params": params,
113 | }
114 | json = yield cmd_dict
115 |
116 |
117 | def clear_storage_items(
118 | id_: str, storage_area: StorageArea
119 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
120 | """
121 | Clears extension storage in the given ``storageArea``.
122 |
123 | :param id_: ID of extension.
124 | :param storage_area: StorageArea to remove data from.
125 | """
126 | params: T_JSON_DICT = dict()
127 | params["id"] = id_
128 | params["storageArea"] = storage_area.to_json()
129 | cmd_dict: T_JSON_DICT = {
130 | "method": "Extensions.clearStorageItems",
131 | "params": params,
132 | }
133 | json = yield cmd_dict
134 |
135 |
136 | def set_storage_items(
137 | id_: str, storage_area: StorageArea, values: dict
138 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
139 | """
140 | Sets ``values`` in extension storage in the given ``storageArea``. The provided ``values``
141 | will be merged with existing values in the storage area.
142 |
143 | :param id_: ID of extension.
144 | :param storage_area: StorageArea to set data in.
145 | :param values: Values to set.
146 | """
147 | params: T_JSON_DICT = dict()
148 | params["id"] = id_
149 | params["storageArea"] = storage_area.to_json()
150 | params["values"] = values
151 | cmd_dict: T_JSON_DICT = {
152 | "method": "Extensions.setStorageItems",
153 | "params": params,
154 | }
155 | json = yield cmd_dict
156 |
--------------------------------------------------------------------------------
/zendriver/cdp/fed_cm.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: FedCm (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class LoginState(enum.Enum):
16 | """
17 | Whether this is a sign-up or sign-in action for this account, i.e.
18 | whether this account has ever been used to sign in to this RP before.
19 | """
20 |
21 | SIGN_IN = "SignIn"
22 | SIGN_UP = "SignUp"
23 |
24 | def to_json(self) -> str:
25 | return self.value
26 |
27 | @classmethod
28 | def from_json(cls, json: str) -> LoginState:
29 | return cls(json)
30 |
31 |
32 | class DialogType(enum.Enum):
33 | """
34 | The types of FedCM dialogs.
35 | """
36 |
37 | ACCOUNT_CHOOSER = "AccountChooser"
38 | AUTO_REAUTHN = "AutoReauthn"
39 | CONFIRM_IDP_LOGIN = "ConfirmIdpLogin"
40 | ERROR = "Error"
41 |
42 | def to_json(self) -> str:
43 | return self.value
44 |
45 | @classmethod
46 | def from_json(cls, json: str) -> DialogType:
47 | return cls(json)
48 |
49 |
50 | class DialogButton(enum.Enum):
51 | """
52 | The buttons on the FedCM dialog.
53 | """
54 |
55 | CONFIRM_IDP_LOGIN_CONTINUE = "ConfirmIdpLoginContinue"
56 | ERROR_GOT_IT = "ErrorGotIt"
57 | ERROR_MORE_DETAILS = "ErrorMoreDetails"
58 |
59 | def to_json(self) -> str:
60 | return self.value
61 |
62 | @classmethod
63 | def from_json(cls, json: str) -> DialogButton:
64 | return cls(json)
65 |
66 |
67 | class AccountUrlType(enum.Enum):
68 | """
69 | The URLs that each account has
70 | """
71 |
72 | TERMS_OF_SERVICE = "TermsOfService"
73 | PRIVACY_POLICY = "PrivacyPolicy"
74 |
75 | def to_json(self) -> str:
76 | return self.value
77 |
78 | @classmethod
79 | def from_json(cls, json: str) -> AccountUrlType:
80 | return cls(json)
81 |
82 |
83 | @dataclass
84 | class Account:
85 | """
86 | Corresponds to IdentityRequestAccount
87 | """
88 |
89 | account_id: str
90 |
91 | email: str
92 |
93 | name: str
94 |
95 | given_name: str
96 |
97 | picture_url: str
98 |
99 | idp_config_url: str
100 |
101 | idp_login_url: str
102 |
103 | login_state: LoginState
104 |
105 | #: These two are only set if the loginState is signUp
106 | terms_of_service_url: typing.Optional[str] = None
107 |
108 | privacy_policy_url: typing.Optional[str] = None
109 |
110 | def to_json(self) -> T_JSON_DICT:
111 | json: T_JSON_DICT = dict()
112 | json["accountId"] = self.account_id
113 | json["email"] = self.email
114 | json["name"] = self.name
115 | json["givenName"] = self.given_name
116 | json["pictureUrl"] = self.picture_url
117 | json["idpConfigUrl"] = self.idp_config_url
118 | json["idpLoginUrl"] = self.idp_login_url
119 | json["loginState"] = self.login_state.to_json()
120 | if self.terms_of_service_url is not None:
121 | json["termsOfServiceUrl"] = self.terms_of_service_url
122 | if self.privacy_policy_url is not None:
123 | json["privacyPolicyUrl"] = self.privacy_policy_url
124 | return json
125 |
126 | @classmethod
127 | def from_json(cls, json: T_JSON_DICT) -> Account:
128 | return cls(
129 | account_id=str(json["accountId"]),
130 | email=str(json["email"]),
131 | name=str(json["name"]),
132 | given_name=str(json["givenName"]),
133 | picture_url=str(json["pictureUrl"]),
134 | idp_config_url=str(json["idpConfigUrl"]),
135 | idp_login_url=str(json["idpLoginUrl"]),
136 | login_state=LoginState.from_json(json["loginState"]),
137 | terms_of_service_url=str(json["termsOfServiceUrl"])
138 | if json.get("termsOfServiceUrl", None) is not None
139 | else None,
140 | privacy_policy_url=str(json["privacyPolicyUrl"])
141 | if json.get("privacyPolicyUrl", None) is not None
142 | else None,
143 | )
144 |
145 |
146 | def enable(
147 | disable_rejection_delay: typing.Optional[bool] = None,
148 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
149 | """
150 | :param disable_rejection_delay: *(Optional)* Allows callers to disable the promise rejection delay that would normally happen, if this is unimportant to what's being tested. (step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in)
151 | """
152 | params: T_JSON_DICT = dict()
153 | if disable_rejection_delay is not None:
154 | params["disableRejectionDelay"] = disable_rejection_delay
155 | cmd_dict: T_JSON_DICT = {
156 | "method": "FedCm.enable",
157 | "params": params,
158 | }
159 | json = yield cmd_dict
160 |
161 |
162 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
163 | cmd_dict: T_JSON_DICT = {
164 | "method": "FedCm.disable",
165 | }
166 | json = yield cmd_dict
167 |
168 |
169 | def select_account(
170 | dialog_id: str, account_index: int
171 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
172 | """
173 | :param dialog_id:
174 | :param account_index:
175 | """
176 | params: T_JSON_DICT = dict()
177 | params["dialogId"] = dialog_id
178 | params["accountIndex"] = account_index
179 | cmd_dict: T_JSON_DICT = {
180 | "method": "FedCm.selectAccount",
181 | "params": params,
182 | }
183 | json = yield cmd_dict
184 |
185 |
186 | def click_dialog_button(
187 | dialog_id: str, dialog_button: DialogButton
188 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
189 | """
190 | :param dialog_id:
191 | :param dialog_button:
192 | """
193 | params: T_JSON_DICT = dict()
194 | params["dialogId"] = dialog_id
195 | params["dialogButton"] = dialog_button.to_json()
196 | cmd_dict: T_JSON_DICT = {
197 | "method": "FedCm.clickDialogButton",
198 | "params": params,
199 | }
200 | json = yield cmd_dict
201 |
202 |
203 | def open_url(
204 | dialog_id: str, account_index: int, account_url_type: AccountUrlType
205 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
206 | """
207 | :param dialog_id:
208 | :param account_index:
209 | :param account_url_type:
210 | """
211 | params: T_JSON_DICT = dict()
212 | params["dialogId"] = dialog_id
213 | params["accountIndex"] = account_index
214 | params["accountUrlType"] = account_url_type.to_json()
215 | cmd_dict: T_JSON_DICT = {
216 | "method": "FedCm.openUrl",
217 | "params": params,
218 | }
219 | json = yield cmd_dict
220 |
221 |
222 | def dismiss_dialog(
223 | dialog_id: str, trigger_cooldown: typing.Optional[bool] = None
224 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
225 | """
226 | :param dialog_id:
227 | :param trigger_cooldown: *(Optional)*
228 | """
229 | params: T_JSON_DICT = dict()
230 | params["dialogId"] = dialog_id
231 | if trigger_cooldown is not None:
232 | params["triggerCooldown"] = trigger_cooldown
233 | cmd_dict: T_JSON_DICT = {
234 | "method": "FedCm.dismissDialog",
235 | "params": params,
236 | }
237 | json = yield cmd_dict
238 |
239 |
240 | def reset_cooldown() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
241 | """
242 | Resets the cooldown time, if any, to allow the next FedCM call to show
243 | a dialog even if one was recently dismissed by the user.
244 | """
245 | cmd_dict: T_JSON_DICT = {
246 | "method": "FedCm.resetCooldown",
247 | }
248 | json = yield cmd_dict
249 |
250 |
251 | @event_class("FedCm.dialogShown")
252 | @dataclass
253 | class DialogShown:
254 | dialog_id: str
255 | dialog_type: DialogType
256 | accounts: typing.List[Account]
257 | #: These exist primarily so that the caller can verify the
258 | #: RP context was used appropriately.
259 | title: str
260 | subtitle: typing.Optional[str]
261 |
262 | @classmethod
263 | def from_json(cls, json: T_JSON_DICT) -> DialogShown:
264 | return cls(
265 | dialog_id=str(json["dialogId"]),
266 | dialog_type=DialogType.from_json(json["dialogType"]),
267 | accounts=[Account.from_json(i) for i in json["accounts"]],
268 | title=str(json["title"]),
269 | subtitle=str(json["subtitle"])
270 | if json.get("subtitle", None) is not None
271 | else None,
272 | )
273 |
274 |
275 | @event_class("FedCm.dialogClosed")
276 | @dataclass
277 | class DialogClosed:
278 | """
279 | Triggered when a dialog is closed, either by user action, JS abort,
280 | or a command below.
281 | """
282 |
283 | dialog_id: str
284 |
285 | @classmethod
286 | def from_json(cls, json: T_JSON_DICT) -> DialogClosed:
287 | return cls(dialog_id=str(json["dialogId"]))
288 |
--------------------------------------------------------------------------------
/zendriver/cdp/file_system.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: FileSystem (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 | from . import network
15 | from . import storage
16 |
17 |
18 | @dataclass
19 | class File:
20 | name: str
21 |
22 | #: Timestamp
23 | last_modified: network.TimeSinceEpoch
24 |
25 | #: Size in bytes
26 | size: float
27 |
28 | type_: str
29 |
30 | def to_json(self) -> T_JSON_DICT:
31 | json: T_JSON_DICT = dict()
32 | json["name"] = self.name
33 | json["lastModified"] = self.last_modified.to_json()
34 | json["size"] = self.size
35 | json["type"] = self.type_
36 | return json
37 |
38 | @classmethod
39 | def from_json(cls, json: T_JSON_DICT) -> File:
40 | return cls(
41 | name=str(json["name"]),
42 | last_modified=network.TimeSinceEpoch.from_json(json["lastModified"]),
43 | size=float(json["size"]),
44 | type_=str(json["type"]),
45 | )
46 |
47 |
48 | @dataclass
49 | class Directory:
50 | name: str
51 |
52 | nested_directories: typing.List[str]
53 |
54 | #: Files that are directly nested under this directory.
55 | nested_files: typing.List[File]
56 |
57 | def to_json(self) -> T_JSON_DICT:
58 | json: T_JSON_DICT = dict()
59 | json["name"] = self.name
60 | json["nestedDirectories"] = [i for i in self.nested_directories]
61 | json["nestedFiles"] = [i.to_json() for i in self.nested_files]
62 | return json
63 |
64 | @classmethod
65 | def from_json(cls, json: T_JSON_DICT) -> Directory:
66 | return cls(
67 | name=str(json["name"]),
68 | nested_directories=[str(i) for i in json["nestedDirectories"]],
69 | nested_files=[File.from_json(i) for i in json["nestedFiles"]],
70 | )
71 |
72 |
73 | @dataclass
74 | class BucketFileSystemLocator:
75 | #: Storage key
76 | storage_key: storage.SerializedStorageKey
77 |
78 | #: Path to the directory using each path component as an array item.
79 | path_components: typing.List[str]
80 |
81 | #: Bucket name. Not passing a ``bucketName`` will retrieve the default Bucket. (https://developer.mozilla.org/en-US/docs/Web/API/Storage_API#storage_buckets)
82 | bucket_name: typing.Optional[str] = None
83 |
84 | def to_json(self) -> T_JSON_DICT:
85 | json: T_JSON_DICT = dict()
86 | json["storageKey"] = self.storage_key.to_json()
87 | json["pathComponents"] = [i for i in self.path_components]
88 | if self.bucket_name is not None:
89 | json["bucketName"] = self.bucket_name
90 | return json
91 |
92 | @classmethod
93 | def from_json(cls, json: T_JSON_DICT) -> BucketFileSystemLocator:
94 | return cls(
95 | storage_key=storage.SerializedStorageKey.from_json(json["storageKey"]),
96 | path_components=[str(i) for i in json["pathComponents"]],
97 | bucket_name=str(json["bucketName"])
98 | if json.get("bucketName", None) is not None
99 | else None,
100 | )
101 |
102 |
103 | def get_directory(
104 | bucket_file_system_locator: BucketFileSystemLocator,
105 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, Directory]:
106 | """
107 | :param bucket_file_system_locator:
108 | :returns: Returns the directory object at the path.
109 | """
110 | params: T_JSON_DICT = dict()
111 | params["bucketFileSystemLocator"] = bucket_file_system_locator.to_json()
112 | cmd_dict: T_JSON_DICT = {
113 | "method": "FileSystem.getDirectory",
114 | "params": params,
115 | }
116 | json = yield cmd_dict
117 | return Directory.from_json(json["directory"])
118 |
--------------------------------------------------------------------------------
/zendriver/cdp/headless_experimental.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: HeadlessExperimental (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | from deprecated.sphinx import deprecated # type: ignore
16 |
17 |
18 | @dataclass
19 | class ScreenshotParams:
20 | """
21 | Encoding options for a screenshot.
22 | """
23 |
24 | #: Image compression format (defaults to png).
25 | format_: typing.Optional[str] = None
26 |
27 | #: Compression quality from range [0..100] (jpeg and webp only).
28 | quality: typing.Optional[int] = None
29 |
30 | #: Optimize image encoding for speed, not for resulting size (defaults to false)
31 | optimize_for_speed: typing.Optional[bool] = None
32 |
33 | def to_json(self) -> T_JSON_DICT:
34 | json: T_JSON_DICT = dict()
35 | if self.format_ is not None:
36 | json["format"] = self.format_
37 | if self.quality is not None:
38 | json["quality"] = self.quality
39 | if self.optimize_for_speed is not None:
40 | json["optimizeForSpeed"] = self.optimize_for_speed
41 | return json
42 |
43 | @classmethod
44 | def from_json(cls, json: T_JSON_DICT) -> ScreenshotParams:
45 | return cls(
46 | format_=str(json["format"])
47 | if json.get("format", None) is not None
48 | else None,
49 | quality=int(json["quality"])
50 | if json.get("quality", None) is not None
51 | else None,
52 | optimize_for_speed=bool(json["optimizeForSpeed"])
53 | if json.get("optimizeForSpeed", None) is not None
54 | else None,
55 | )
56 |
57 |
58 | def begin_frame(
59 | frame_time_ticks: typing.Optional[float] = None,
60 | interval: typing.Optional[float] = None,
61 | no_display_updates: typing.Optional[bool] = None,
62 | screenshot: typing.Optional[ScreenshotParams] = None,
63 | ) -> typing.Generator[
64 | T_JSON_DICT, T_JSON_DICT, typing.Tuple[bool, typing.Optional[str]]
65 | ]:
66 | """
67 | Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a
68 | screenshot from the resulting frame. Requires that the target was created with enabled
69 | BeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also
70 | https://goo.gle/chrome-headless-rendering for more background.
71 |
72 | :param frame_time_ticks: *(Optional)* Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set, the current time will be used.
73 | :param interval: *(Optional)* The interval between BeginFrames that is reported to the compositor, in milliseconds. Defaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.
74 | :param no_display_updates: *(Optional)* Whether updates should not be committed and drawn onto the display. False by default. If true, only side effects of the BeginFrame will be run, such as layout and animations, but any visual updates may not be visible on the display or in screenshots.
75 | :param screenshot: *(Optional)* If set, a screenshot of the frame will be captured and returned in the response. Otherwise, no screenshot will be captured. Note that capturing a screenshot can fail, for example, during renderer initialization. In such a case, no screenshot data will be returned.
76 | :returns: A tuple with the following items:
77 |
78 | 0. **hasDamage** - Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the display. Reported for diagnostic uses, may be removed in the future.
79 | 1. **screenshotData** - *(Optional)* Base64-encoded image data of the screenshot, if one was requested and successfully taken. (Encoded as a base64 string when passed over JSON)
80 | """
81 | params: T_JSON_DICT = dict()
82 | if frame_time_ticks is not None:
83 | params["frameTimeTicks"] = frame_time_ticks
84 | if interval is not None:
85 | params["interval"] = interval
86 | if no_display_updates is not None:
87 | params["noDisplayUpdates"] = no_display_updates
88 | if screenshot is not None:
89 | params["screenshot"] = screenshot.to_json()
90 | cmd_dict: T_JSON_DICT = {
91 | "method": "HeadlessExperimental.beginFrame",
92 | "params": params,
93 | }
94 | json = yield cmd_dict
95 | return (
96 | bool(json["hasDamage"]),
97 | str(json["screenshotData"])
98 | if json.get("screenshotData", None) is not None
99 | else None,
100 | )
101 |
102 |
103 | @deprecated(version="1.3")
104 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
105 | """
106 | Disables headless events for the target.
107 |
108 | .. deprecated:: 1.3
109 | """
110 | cmd_dict: T_JSON_DICT = {
111 | "method": "HeadlessExperimental.disable",
112 | }
113 | json = yield cmd_dict
114 |
115 |
116 | @deprecated(version="1.3")
117 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
118 | """
119 | Enables headless events for the target.
120 |
121 | .. deprecated:: 1.3
122 | """
123 | cmd_dict: T_JSON_DICT = {
124 | "method": "HeadlessExperimental.enable",
125 | }
126 | json = yield cmd_dict
127 |
--------------------------------------------------------------------------------
/zendriver/cdp/inspector.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Inspector (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
16 | """
17 | Disables inspector domain notifications.
18 | """
19 | cmd_dict: T_JSON_DICT = {
20 | "method": "Inspector.disable",
21 | }
22 | json = yield cmd_dict
23 |
24 |
25 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
26 | """
27 | Enables inspector domain notifications.
28 | """
29 | cmd_dict: T_JSON_DICT = {
30 | "method": "Inspector.enable",
31 | }
32 | json = yield cmd_dict
33 |
34 |
35 | @event_class("Inspector.detached")
36 | @dataclass
37 | class Detached:
38 | """
39 | Fired when remote debugging connection is about to be terminated. Contains detach reason.
40 | """
41 |
42 | #: The reason why connection has been terminated.
43 | reason: str
44 |
45 | @classmethod
46 | def from_json(cls, json: T_JSON_DICT) -> Detached:
47 | return cls(reason=str(json["reason"]))
48 |
49 |
50 | @event_class("Inspector.targetCrashed")
51 | @dataclass
52 | class TargetCrashed:
53 | """
54 | Fired when debugging target has crashed
55 | """
56 |
57 | @classmethod
58 | def from_json(cls, json: T_JSON_DICT) -> TargetCrashed:
59 | return cls()
60 |
61 |
62 | @event_class("Inspector.targetReloadedAfterCrash")
63 | @dataclass
64 | class TargetReloadedAfterCrash:
65 | """
66 | Fired when debugging target has reloaded after crash
67 | """
68 |
69 | @classmethod
70 | def from_json(cls, json: T_JSON_DICT) -> TargetReloadedAfterCrash:
71 | return cls()
72 |
--------------------------------------------------------------------------------
/zendriver/cdp/io.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: IO
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 | from . import runtime
15 |
16 |
17 | class StreamHandle(str):
18 | """
19 | This is either obtained from another method or specified as ``blob:`` where
20 | ```` is an UUID of a Blob.
21 | """
22 |
23 | def to_json(self) -> str:
24 | return self
25 |
26 | @classmethod
27 | def from_json(cls, json: str) -> StreamHandle:
28 | return cls(json)
29 |
30 | def __repr__(self):
31 | return "StreamHandle({})".format(super().__repr__())
32 |
33 |
34 | def close(handle: StreamHandle) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
35 | """
36 | Close the stream, discard any temporary backing storage.
37 |
38 | :param handle: Handle of the stream to close.
39 | """
40 | params: T_JSON_DICT = dict()
41 | params["handle"] = handle.to_json()
42 | cmd_dict: T_JSON_DICT = {
43 | "method": "IO.close",
44 | "params": params,
45 | }
46 | json = yield cmd_dict
47 |
48 |
49 | def read(
50 | handle: StreamHandle,
51 | offset: typing.Optional[int] = None,
52 | size: typing.Optional[int] = None,
53 | ) -> typing.Generator[
54 | T_JSON_DICT, T_JSON_DICT, typing.Tuple[typing.Optional[bool], str, bool]
55 | ]:
56 | """
57 | Read a chunk of the stream
58 |
59 | :param handle: Handle of the stream to read.
60 | :param offset: *(Optional)* Seek to the specified offset before reading (if not specified, proceed with offset following the last read). Some types of streams may only support sequential reads.
61 | :param size: *(Optional)* Maximum number of bytes to read (left upon the agent discretion if not specified).
62 | :returns: A tuple with the following items:
63 |
64 | 0. **base64Encoded** - *(Optional)* Set if the data is base64-encoded
65 | 1. **data** - Data that were read.
66 | 2. **eof** - Set if the end-of-file condition occurred while reading.
67 | """
68 | params: T_JSON_DICT = dict()
69 | params["handle"] = handle.to_json()
70 | if offset is not None:
71 | params["offset"] = offset
72 | if size is not None:
73 | params["size"] = size
74 | cmd_dict: T_JSON_DICT = {
75 | "method": "IO.read",
76 | "params": params,
77 | }
78 | json = yield cmd_dict
79 | return (
80 | bool(json["base64Encoded"])
81 | if json.get("base64Encoded", None) is not None
82 | else None,
83 | str(json["data"]),
84 | bool(json["eof"]),
85 | )
86 |
87 |
88 | def resolve_blob(
89 | object_id: runtime.RemoteObjectId,
90 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, str]:
91 | """
92 | Return UUID of Blob object specified by a remote object id.
93 |
94 | :param object_id: Object id of a Blob object wrapper.
95 | :returns: UUID of the specified Blob.
96 | """
97 | params: T_JSON_DICT = dict()
98 | params["objectId"] = object_id.to_json()
99 | cmd_dict: T_JSON_DICT = {
100 | "method": "IO.resolveBlob",
101 | "params": params,
102 | }
103 | json = yield cmd_dict
104 | return str(json["uuid"])
105 |
--------------------------------------------------------------------------------
/zendriver/cdp/log.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Log
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 | from . import network
15 | from . import runtime
16 |
17 |
18 | @dataclass
19 | class LogEntry:
20 | """
21 | Log entry.
22 | """
23 |
24 | #: Log entry source.
25 | source: str
26 |
27 | #: Log entry severity.
28 | level: str
29 |
30 | #: Logged text.
31 | text: str
32 |
33 | #: Timestamp when this entry was added.
34 | timestamp: runtime.Timestamp
35 |
36 | category: typing.Optional[str] = None
37 |
38 | #: URL of the resource if known.
39 | url: typing.Optional[str] = None
40 |
41 | #: Line number in the resource.
42 | line_number: typing.Optional[int] = None
43 |
44 | #: JavaScript stack trace.
45 | stack_trace: typing.Optional[runtime.StackTrace] = None
46 |
47 | #: Identifier of the network request associated with this entry.
48 | network_request_id: typing.Optional[network.RequestId] = None
49 |
50 | #: Identifier of the worker associated with this entry.
51 | worker_id: typing.Optional[str] = None
52 |
53 | #: Call arguments.
54 | args: typing.Optional[typing.List[runtime.RemoteObject]] = None
55 |
56 | def to_json(self) -> T_JSON_DICT:
57 | json: T_JSON_DICT = dict()
58 | json["source"] = self.source
59 | json["level"] = self.level
60 | json["text"] = self.text
61 | json["timestamp"] = self.timestamp.to_json()
62 | if self.category is not None:
63 | json["category"] = self.category
64 | if self.url is not None:
65 | json["url"] = self.url
66 | if self.line_number is not None:
67 | json["lineNumber"] = self.line_number
68 | if self.stack_trace is not None:
69 | json["stackTrace"] = self.stack_trace.to_json()
70 | if self.network_request_id is not None:
71 | json["networkRequestId"] = self.network_request_id.to_json()
72 | if self.worker_id is not None:
73 | json["workerId"] = self.worker_id
74 | if self.args is not None:
75 | json["args"] = [i.to_json() for i in self.args]
76 | return json
77 |
78 | @classmethod
79 | def from_json(cls, json: T_JSON_DICT) -> LogEntry:
80 | return cls(
81 | source=str(json["source"]),
82 | level=str(json["level"]),
83 | text=str(json["text"]),
84 | timestamp=runtime.Timestamp.from_json(json["timestamp"]),
85 | category=str(json["category"])
86 | if json.get("category", None) is not None
87 | else None,
88 | url=str(json["url"]) if json.get("url", None) is not None else None,
89 | line_number=int(json["lineNumber"])
90 | if json.get("lineNumber", None) is not None
91 | else None,
92 | stack_trace=runtime.StackTrace.from_json(json["stackTrace"])
93 | if json.get("stackTrace", None) is not None
94 | else None,
95 | network_request_id=network.RequestId.from_json(json["networkRequestId"])
96 | if json.get("networkRequestId", None) is not None
97 | else None,
98 | worker_id=str(json["workerId"])
99 | if json.get("workerId", None) is not None
100 | else None,
101 | args=[runtime.RemoteObject.from_json(i) for i in json["args"]]
102 | if json.get("args", None) is not None
103 | else None,
104 | )
105 |
106 |
107 | @dataclass
108 | class ViolationSetting:
109 | """
110 | Violation configuration setting.
111 | """
112 |
113 | #: Violation type.
114 | name: str
115 |
116 | #: Time threshold to trigger upon.
117 | threshold: float
118 |
119 | def to_json(self) -> T_JSON_DICT:
120 | json: T_JSON_DICT = dict()
121 | json["name"] = self.name
122 | json["threshold"] = self.threshold
123 | return json
124 |
125 | @classmethod
126 | def from_json(cls, json: T_JSON_DICT) -> ViolationSetting:
127 | return cls(
128 | name=str(json["name"]),
129 | threshold=float(json["threshold"]),
130 | )
131 |
132 |
133 | def clear() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
134 | """
135 | Clears the log.
136 | """
137 | cmd_dict: T_JSON_DICT = {
138 | "method": "Log.clear",
139 | }
140 | json = yield cmd_dict
141 |
142 |
143 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
144 | """
145 | Disables log domain, prevents further log entries from being reported to the client.
146 | """
147 | cmd_dict: T_JSON_DICT = {
148 | "method": "Log.disable",
149 | }
150 | json = yield cmd_dict
151 |
152 |
153 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
154 | """
155 | Enables log domain, sends the entries collected so far to the client by means of the
156 | ``entryAdded`` notification.
157 | """
158 | cmd_dict: T_JSON_DICT = {
159 | "method": "Log.enable",
160 | }
161 | json = yield cmd_dict
162 |
163 |
164 | def start_violations_report(
165 | config: typing.List[ViolationSetting],
166 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
167 | """
168 | start violation reporting.
169 |
170 | :param config: Configuration for violations.
171 | """
172 | params: T_JSON_DICT = dict()
173 | params["config"] = [i.to_json() for i in config]
174 | cmd_dict: T_JSON_DICT = {
175 | "method": "Log.startViolationsReport",
176 | "params": params,
177 | }
178 | json = yield cmd_dict
179 |
180 |
181 | def stop_violations_report() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
182 | """
183 | Stop violation reporting.
184 | """
185 | cmd_dict: T_JSON_DICT = {
186 | "method": "Log.stopViolationsReport",
187 | }
188 | json = yield cmd_dict
189 |
190 |
191 | @event_class("Log.entryAdded")
192 | @dataclass
193 | class EntryAdded:
194 | """
195 | Issued when new message was logged.
196 | """
197 |
198 | #: The entry.
199 | entry: LogEntry
200 |
201 | @classmethod
202 | def from_json(cls, json: T_JSON_DICT) -> EntryAdded:
203 | return cls(entry=LogEntry.from_json(json["entry"]))
204 |
--------------------------------------------------------------------------------
/zendriver/cdp/media.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Media (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class PlayerId(str):
16 | """
17 | Players will get an ID that is unique within the agent context.
18 | """
19 |
20 | def to_json(self) -> str:
21 | return self
22 |
23 | @classmethod
24 | def from_json(cls, json: str) -> PlayerId:
25 | return cls(json)
26 |
27 | def __repr__(self):
28 | return "PlayerId({})".format(super().__repr__())
29 |
30 |
31 | class Timestamp(float):
32 | def to_json(self) -> float:
33 | return self
34 |
35 | @classmethod
36 | def from_json(cls, json: float) -> Timestamp:
37 | return cls(json)
38 |
39 | def __repr__(self):
40 | return "Timestamp({})".format(super().__repr__())
41 |
42 |
43 | @dataclass
44 | class PlayerMessage:
45 | """
46 | Have one type per entry in MediaLogRecord::Type
47 | Corresponds to kMessage
48 | """
49 |
50 | #: Keep in sync with MediaLogMessageLevel
51 | #: We are currently keeping the message level 'error' separate from the
52 | #: PlayerError type because right now they represent different things,
53 | #: this one being a DVLOG(ERROR) style log message that gets printed
54 | #: based on what log level is selected in the UI, and the other is a
55 | #: representation of a media::PipelineStatus object. Soon however we're
56 | #: going to be moving away from using PipelineStatus for errors and
57 | #: introducing a new error type which should hopefully let us integrate
58 | #: the error log level into the PlayerError type.
59 | level: str
60 |
61 | message: str
62 |
63 | def to_json(self) -> T_JSON_DICT:
64 | json: T_JSON_DICT = dict()
65 | json["level"] = self.level
66 | json["message"] = self.message
67 | return json
68 |
69 | @classmethod
70 | def from_json(cls, json: T_JSON_DICT) -> PlayerMessage:
71 | return cls(
72 | level=str(json["level"]),
73 | message=str(json["message"]),
74 | )
75 |
76 |
77 | @dataclass
78 | class PlayerProperty:
79 | """
80 | Corresponds to kMediaPropertyChange
81 | """
82 |
83 | name: str
84 |
85 | value: str
86 |
87 | def to_json(self) -> T_JSON_DICT:
88 | json: T_JSON_DICT = dict()
89 | json["name"] = self.name
90 | json["value"] = self.value
91 | return json
92 |
93 | @classmethod
94 | def from_json(cls, json: T_JSON_DICT) -> PlayerProperty:
95 | return cls(
96 | name=str(json["name"]),
97 | value=str(json["value"]),
98 | )
99 |
100 |
101 | @dataclass
102 | class PlayerEvent:
103 | """
104 | Corresponds to kMediaEventTriggered
105 | """
106 |
107 | timestamp: Timestamp
108 |
109 | value: str
110 |
111 | def to_json(self) -> T_JSON_DICT:
112 | json: T_JSON_DICT = dict()
113 | json["timestamp"] = self.timestamp.to_json()
114 | json["value"] = self.value
115 | return json
116 |
117 | @classmethod
118 | def from_json(cls, json: T_JSON_DICT) -> PlayerEvent:
119 | return cls(
120 | timestamp=Timestamp.from_json(json["timestamp"]),
121 | value=str(json["value"]),
122 | )
123 |
124 |
125 | @dataclass
126 | class PlayerErrorSourceLocation:
127 | """
128 | Represents logged source line numbers reported in an error.
129 | NOTE: file and line are from chromium c++ implementation code, not js.
130 | """
131 |
132 | file: str
133 |
134 | line: int
135 |
136 | def to_json(self) -> T_JSON_DICT:
137 | json: T_JSON_DICT = dict()
138 | json["file"] = self.file
139 | json["line"] = self.line
140 | return json
141 |
142 | @classmethod
143 | def from_json(cls, json: T_JSON_DICT) -> PlayerErrorSourceLocation:
144 | return cls(
145 | file=str(json["file"]),
146 | line=int(json["line"]),
147 | )
148 |
149 |
150 | @dataclass
151 | class PlayerError:
152 | """
153 | Corresponds to kMediaError
154 | """
155 |
156 | error_type: str
157 |
158 | #: Code is the numeric enum entry for a specific set of error codes, such
159 | #: as PipelineStatusCodes in media/base/pipeline_status.h
160 | code: int
161 |
162 | #: A trace of where this error was caused / where it passed through.
163 | stack: typing.List[PlayerErrorSourceLocation]
164 |
165 | #: Errors potentially have a root cause error, ie, a DecoderError might be
166 | #: caused by an WindowsError
167 | cause: typing.List[PlayerError]
168 |
169 | #: Extra data attached to an error, such as an HRESULT, Video Codec, etc.
170 | data: dict
171 |
172 | def to_json(self) -> T_JSON_DICT:
173 | json: T_JSON_DICT = dict()
174 | json["errorType"] = self.error_type
175 | json["code"] = self.code
176 | json["stack"] = [i.to_json() for i in self.stack]
177 | json["cause"] = [i.to_json() for i in self.cause]
178 | json["data"] = self.data
179 | return json
180 |
181 | @classmethod
182 | def from_json(cls, json: T_JSON_DICT) -> PlayerError:
183 | return cls(
184 | error_type=str(json["errorType"]),
185 | code=int(json["code"]),
186 | stack=[PlayerErrorSourceLocation.from_json(i) for i in json["stack"]],
187 | cause=[PlayerError.from_json(i) for i in json["cause"]],
188 | data=dict(json["data"]),
189 | )
190 |
191 |
192 | def enable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
193 | """
194 | Enables the Media domain
195 | """
196 | cmd_dict: T_JSON_DICT = {
197 | "method": "Media.enable",
198 | }
199 | json = yield cmd_dict
200 |
201 |
202 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
203 | """
204 | Disables the Media domain.
205 | """
206 | cmd_dict: T_JSON_DICT = {
207 | "method": "Media.disable",
208 | }
209 | json = yield cmd_dict
210 |
211 |
212 | @event_class("Media.playerPropertiesChanged")
213 | @dataclass
214 | class PlayerPropertiesChanged:
215 | """
216 | This can be called multiple times, and can be used to set / override /
217 | remove player properties. A null propValue indicates removal.
218 | """
219 |
220 | player_id: PlayerId
221 | properties: typing.List[PlayerProperty]
222 |
223 | @classmethod
224 | def from_json(cls, json: T_JSON_DICT) -> PlayerPropertiesChanged:
225 | return cls(
226 | player_id=PlayerId.from_json(json["playerId"]),
227 | properties=[PlayerProperty.from_json(i) for i in json["properties"]],
228 | )
229 |
230 |
231 | @event_class("Media.playerEventsAdded")
232 | @dataclass
233 | class PlayerEventsAdded:
234 | """
235 | Send events as a list, allowing them to be batched on the browser for less
236 | congestion. If batched, events must ALWAYS be in chronological order.
237 | """
238 |
239 | player_id: PlayerId
240 | events: typing.List[PlayerEvent]
241 |
242 | @classmethod
243 | def from_json(cls, json: T_JSON_DICT) -> PlayerEventsAdded:
244 | return cls(
245 | player_id=PlayerId.from_json(json["playerId"]),
246 | events=[PlayerEvent.from_json(i) for i in json["events"]],
247 | )
248 |
249 |
250 | @event_class("Media.playerMessagesLogged")
251 | @dataclass
252 | class PlayerMessagesLogged:
253 | """
254 | Send a list of any messages that need to be delivered.
255 | """
256 |
257 | player_id: PlayerId
258 | messages: typing.List[PlayerMessage]
259 |
260 | @classmethod
261 | def from_json(cls, json: T_JSON_DICT) -> PlayerMessagesLogged:
262 | return cls(
263 | player_id=PlayerId.from_json(json["playerId"]),
264 | messages=[PlayerMessage.from_json(i) for i in json["messages"]],
265 | )
266 |
267 |
268 | @event_class("Media.playerErrorsRaised")
269 | @dataclass
270 | class PlayerErrorsRaised:
271 | """
272 | Send a list of any errors that need to be delivered.
273 | """
274 |
275 | player_id: PlayerId
276 | errors: typing.List[PlayerError]
277 |
278 | @classmethod
279 | def from_json(cls, json: T_JSON_DICT) -> PlayerErrorsRaised:
280 | return cls(
281 | player_id=PlayerId.from_json(json["playerId"]),
282 | errors=[PlayerError.from_json(i) for i in json["errors"]],
283 | )
284 |
285 |
286 | @event_class("Media.playersCreated")
287 | @dataclass
288 | class PlayersCreated:
289 | """
290 | Called whenever a player is created, or when a new agent joins and receives
291 | a list of active players. If an agent is restored, it will receive the full
292 | list of player ids and all events again.
293 | """
294 |
295 | players: typing.List[PlayerId]
296 |
297 | @classmethod
298 | def from_json(cls, json: T_JSON_DICT) -> PlayersCreated:
299 | return cls(players=[PlayerId.from_json(i) for i in json["players"]])
300 |
--------------------------------------------------------------------------------
/zendriver/cdp/memory.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Memory (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | class PressureLevel(enum.Enum):
16 | """
17 | Memory pressure level.
18 | """
19 |
20 | MODERATE = "moderate"
21 | CRITICAL = "critical"
22 |
23 | def to_json(self) -> str:
24 | return self.value
25 |
26 | @classmethod
27 | def from_json(cls, json: str) -> PressureLevel:
28 | return cls(json)
29 |
30 |
31 | @dataclass
32 | class SamplingProfileNode:
33 | """
34 | Heap profile sample.
35 | """
36 |
37 | #: Size of the sampled allocation.
38 | size: float
39 |
40 | #: Total bytes attributed to this sample.
41 | total: float
42 |
43 | #: Execution stack at the point of allocation.
44 | stack: typing.List[str]
45 |
46 | def to_json(self) -> T_JSON_DICT:
47 | json: T_JSON_DICT = dict()
48 | json["size"] = self.size
49 | json["total"] = self.total
50 | json["stack"] = [i for i in self.stack]
51 | return json
52 |
53 | @classmethod
54 | def from_json(cls, json: T_JSON_DICT) -> SamplingProfileNode:
55 | return cls(
56 | size=float(json["size"]),
57 | total=float(json["total"]),
58 | stack=[str(i) for i in json["stack"]],
59 | )
60 |
61 |
62 | @dataclass
63 | class SamplingProfile:
64 | """
65 | Array of heap profile samples.
66 | """
67 |
68 | samples: typing.List[SamplingProfileNode]
69 |
70 | modules: typing.List[Module]
71 |
72 | def to_json(self) -> T_JSON_DICT:
73 | json: T_JSON_DICT = dict()
74 | json["samples"] = [i.to_json() for i in self.samples]
75 | json["modules"] = [i.to_json() for i in self.modules]
76 | return json
77 |
78 | @classmethod
79 | def from_json(cls, json: T_JSON_DICT) -> SamplingProfile:
80 | return cls(
81 | samples=[SamplingProfileNode.from_json(i) for i in json["samples"]],
82 | modules=[Module.from_json(i) for i in json["modules"]],
83 | )
84 |
85 |
86 | @dataclass
87 | class Module:
88 | """
89 | Executable module information
90 | """
91 |
92 | #: Name of the module.
93 | name: str
94 |
95 | #: UUID of the module.
96 | uuid: str
97 |
98 | #: Base address where the module is loaded into memory. Encoded as a decimal
99 | #: or hexadecimal (0x prefixed) string.
100 | base_address: str
101 |
102 | #: Size of the module in bytes.
103 | size: float
104 |
105 | def to_json(self) -> T_JSON_DICT:
106 | json: T_JSON_DICT = dict()
107 | json["name"] = self.name
108 | json["uuid"] = self.uuid
109 | json["baseAddress"] = self.base_address
110 | json["size"] = self.size
111 | return json
112 |
113 | @classmethod
114 | def from_json(cls, json: T_JSON_DICT) -> Module:
115 | return cls(
116 | name=str(json["name"]),
117 | uuid=str(json["uuid"]),
118 | base_address=str(json["baseAddress"]),
119 | size=float(json["size"]),
120 | )
121 |
122 |
123 | @dataclass
124 | class DOMCounter:
125 | """
126 | DOM object counter data.
127 | """
128 |
129 | #: Object name. Note: object names should be presumed volatile and clients should not expect
130 | #: the returned names to be consistent across runs.
131 | name: str
132 |
133 | #: Object count.
134 | count: int
135 |
136 | def to_json(self) -> T_JSON_DICT:
137 | json: T_JSON_DICT = dict()
138 | json["name"] = self.name
139 | json["count"] = self.count
140 | return json
141 |
142 | @classmethod
143 | def from_json(cls, json: T_JSON_DICT) -> DOMCounter:
144 | return cls(
145 | name=str(json["name"]),
146 | count=int(json["count"]),
147 | )
148 |
149 |
150 | def get_dom_counters() -> typing.Generator[
151 | T_JSON_DICT, T_JSON_DICT, typing.Tuple[int, int, int]
152 | ]:
153 | """
154 | Retruns current DOM object counters.
155 |
156 | :returns: A tuple with the following items:
157 |
158 | 0. **documents** -
159 | 1. **nodes** -
160 | 2. **jsEventListeners** -
161 | """
162 | cmd_dict: T_JSON_DICT = {
163 | "method": "Memory.getDOMCounters",
164 | }
165 | json = yield cmd_dict
166 | return (int(json["documents"]), int(json["nodes"]), int(json["jsEventListeners"]))
167 |
168 |
169 | def get_dom_counters_for_leak_detection() -> typing.Generator[
170 | T_JSON_DICT, T_JSON_DICT, typing.List[DOMCounter]
171 | ]:
172 | """
173 | Retruns DOM object counters after preparing renderer for leak detection.
174 |
175 | :returns: DOM object counters.
176 | """
177 | cmd_dict: T_JSON_DICT = {
178 | "method": "Memory.getDOMCountersForLeakDetection",
179 | }
180 | json = yield cmd_dict
181 | return [DOMCounter.from_json(i) for i in json["counters"]]
182 |
183 |
184 | def prepare_for_leak_detection() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
185 | """
186 | Prepares for leak detection by terminating workers, stopping spellcheckers,
187 | dropping non-essential internal caches, running garbage collections, etc.
188 | """
189 | cmd_dict: T_JSON_DICT = {
190 | "method": "Memory.prepareForLeakDetection",
191 | }
192 | json = yield cmd_dict
193 |
194 |
195 | def forcibly_purge_java_script_memory() -> typing.Generator[
196 | T_JSON_DICT, T_JSON_DICT, None
197 | ]:
198 | """
199 | Simulate OomIntervention by purging V8 memory.
200 | """
201 | cmd_dict: T_JSON_DICT = {
202 | "method": "Memory.forciblyPurgeJavaScriptMemory",
203 | }
204 | json = yield cmd_dict
205 |
206 |
207 | def set_pressure_notifications_suppressed(
208 | suppressed: bool,
209 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
210 | """
211 | Enable/disable suppressing memory pressure notifications in all processes.
212 |
213 | :param suppressed: If true, memory pressure notifications will be suppressed.
214 | """
215 | params: T_JSON_DICT = dict()
216 | params["suppressed"] = suppressed
217 | cmd_dict: T_JSON_DICT = {
218 | "method": "Memory.setPressureNotificationsSuppressed",
219 | "params": params,
220 | }
221 | json = yield cmd_dict
222 |
223 |
224 | def simulate_pressure_notification(
225 | level: PressureLevel,
226 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
227 | """
228 | Simulate a memory pressure notification in all processes.
229 |
230 | :param level: Memory pressure level of the notification.
231 | """
232 | params: T_JSON_DICT = dict()
233 | params["level"] = level.to_json()
234 | cmd_dict: T_JSON_DICT = {
235 | "method": "Memory.simulatePressureNotification",
236 | "params": params,
237 | }
238 | json = yield cmd_dict
239 |
240 |
241 | def start_sampling(
242 | sampling_interval: typing.Optional[int] = None,
243 | suppress_randomness: typing.Optional[bool] = None,
244 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
245 | """
246 | Start collecting native memory profile.
247 |
248 | :param sampling_interval: *(Optional)* Average number of bytes between samples.
249 | :param suppress_randomness: *(Optional)* Do not randomize intervals between samples.
250 | """
251 | params: T_JSON_DICT = dict()
252 | if sampling_interval is not None:
253 | params["samplingInterval"] = sampling_interval
254 | if suppress_randomness is not None:
255 | params["suppressRandomness"] = suppress_randomness
256 | cmd_dict: T_JSON_DICT = {
257 | "method": "Memory.startSampling",
258 | "params": params,
259 | }
260 | json = yield cmd_dict
261 |
262 |
263 | def stop_sampling() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
264 | """
265 | Stop collecting native memory profile.
266 | """
267 | cmd_dict: T_JSON_DICT = {
268 | "method": "Memory.stopSampling",
269 | }
270 | json = yield cmd_dict
271 |
272 |
273 | def get_all_time_sampling_profile() -> typing.Generator[
274 | T_JSON_DICT, T_JSON_DICT, SamplingProfile
275 | ]:
276 | """
277 | Retrieve native memory allocations profile
278 | collected since renderer process startup.
279 |
280 | :returns:
281 | """
282 | cmd_dict: T_JSON_DICT = {
283 | "method": "Memory.getAllTimeSamplingProfile",
284 | }
285 | json = yield cmd_dict
286 | return SamplingProfile.from_json(json["profile"])
287 |
288 |
289 | def get_browser_sampling_profile() -> typing.Generator[
290 | T_JSON_DICT, T_JSON_DICT, SamplingProfile
291 | ]:
292 | """
293 | Retrieve native memory allocations profile
294 | collected since browser process startup.
295 |
296 | :returns:
297 | """
298 | cmd_dict: T_JSON_DICT = {
299 | "method": "Memory.getBrowserSamplingProfile",
300 | }
301 | json = yield cmd_dict
302 | return SamplingProfile.from_json(json["profile"])
303 |
304 |
305 | def get_sampling_profile() -> typing.Generator[
306 | T_JSON_DICT, T_JSON_DICT, SamplingProfile
307 | ]:
308 | """
309 | Retrieve native memory allocations profile collected since last
310 | ``startSampling`` call.
311 |
312 | :returns:
313 | """
314 | cmd_dict: T_JSON_DICT = {
315 | "method": "Memory.getSamplingProfile",
316 | }
317 | json = yield cmd_dict
318 | return SamplingProfile.from_json(json["profile"])
319 |
--------------------------------------------------------------------------------
/zendriver/cdp/performance.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Performance
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | from deprecated.sphinx import deprecated # type: ignore
16 |
17 |
18 | @dataclass
19 | class Metric:
20 | """
21 | Run-time execution metric.
22 | """
23 |
24 | #: Metric name.
25 | name: str
26 |
27 | #: Metric value.
28 | value: float
29 |
30 | def to_json(self) -> T_JSON_DICT:
31 | json: T_JSON_DICT = dict()
32 | json["name"] = self.name
33 | json["value"] = self.value
34 | return json
35 |
36 | @classmethod
37 | def from_json(cls, json: T_JSON_DICT) -> Metric:
38 | return cls(
39 | name=str(json["name"]),
40 | value=float(json["value"]),
41 | )
42 |
43 |
44 | def disable() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
45 | """
46 | Disable collecting and reporting metrics.
47 | """
48 | cmd_dict: T_JSON_DICT = {
49 | "method": "Performance.disable",
50 | }
51 | json = yield cmd_dict
52 |
53 |
54 | def enable(
55 | time_domain: typing.Optional[str] = None,
56 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
57 | """
58 | Enable collecting and reporting metrics.
59 |
60 | :param time_domain: *(Optional)* Time domain to use for collecting and reporting duration metrics.
61 | """
62 | params: T_JSON_DICT = dict()
63 | if time_domain is not None:
64 | params["timeDomain"] = time_domain
65 | cmd_dict: T_JSON_DICT = {
66 | "method": "Performance.enable",
67 | "params": params,
68 | }
69 | json = yield cmd_dict
70 |
71 |
72 | @deprecated(version="1.3")
73 | def set_time_domain(
74 | time_domain: str,
75 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
76 | """
77 | Sets time domain to use for collecting and reporting duration metrics.
78 | Note that this must be called before enabling metrics collection. Calling
79 | this method while metrics collection is enabled returns an error.
80 |
81 | .. deprecated:: 1.3
82 |
83 | **EXPERIMENTAL**
84 |
85 | :param time_domain: Time domain
86 | """
87 | params: T_JSON_DICT = dict()
88 | params["timeDomain"] = time_domain
89 | cmd_dict: T_JSON_DICT = {
90 | "method": "Performance.setTimeDomain",
91 | "params": params,
92 | }
93 | json = yield cmd_dict
94 |
95 |
96 | def get_metrics() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, typing.List[Metric]]:
97 | """
98 | Retrieve current values of run-time metrics.
99 |
100 | :returns: Current values for run-time metrics.
101 | """
102 | cmd_dict: T_JSON_DICT = {
103 | "method": "Performance.getMetrics",
104 | }
105 | json = yield cmd_dict
106 | return [Metric.from_json(i) for i in json["metrics"]]
107 |
108 |
109 | @event_class("Performance.metrics")
110 | @dataclass
111 | class Metrics:
112 | """
113 | Current values of the metrics.
114 | """
115 |
116 | #: Current values of the metrics.
117 | metrics: typing.List[Metric]
118 | #: Timestamp title.
119 | title: str
120 |
121 | @classmethod
122 | def from_json(cls, json: T_JSON_DICT) -> Metrics:
123 | return cls(
124 | metrics=[Metric.from_json(i) for i in json["metrics"]],
125 | title=str(json["title"]),
126 | )
127 |
--------------------------------------------------------------------------------
/zendriver/cdp/performance_timeline.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: PerformanceTimeline (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 | from . import dom
15 | from . import network
16 | from . import page
17 |
18 |
19 | @dataclass
20 | class LargestContentfulPaint:
21 | """
22 | See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl
23 | """
24 |
25 | render_time: network.TimeSinceEpoch
26 |
27 | load_time: network.TimeSinceEpoch
28 |
29 | #: The number of pixels being painted.
30 | size: float
31 |
32 | #: The id attribute of the element, if available.
33 | element_id: typing.Optional[str] = None
34 |
35 | #: The URL of the image (may be trimmed).
36 | url: typing.Optional[str] = None
37 |
38 | node_id: typing.Optional[dom.BackendNodeId] = None
39 |
40 | def to_json(self) -> T_JSON_DICT:
41 | json: T_JSON_DICT = dict()
42 | json["renderTime"] = self.render_time.to_json()
43 | json["loadTime"] = self.load_time.to_json()
44 | json["size"] = self.size
45 | if self.element_id is not None:
46 | json["elementId"] = self.element_id
47 | if self.url is not None:
48 | json["url"] = self.url
49 | if self.node_id is not None:
50 | json["nodeId"] = self.node_id.to_json()
51 | return json
52 |
53 | @classmethod
54 | def from_json(cls, json: T_JSON_DICT) -> LargestContentfulPaint:
55 | return cls(
56 | render_time=network.TimeSinceEpoch.from_json(json["renderTime"]),
57 | load_time=network.TimeSinceEpoch.from_json(json["loadTime"]),
58 | size=float(json["size"]),
59 | element_id=str(json["elementId"])
60 | if json.get("elementId", None) is not None
61 | else None,
62 | url=str(json["url"]) if json.get("url", None) is not None else None,
63 | node_id=dom.BackendNodeId.from_json(json["nodeId"])
64 | if json.get("nodeId", None) is not None
65 | else None,
66 | )
67 |
68 |
69 | @dataclass
70 | class LayoutShiftAttribution:
71 | previous_rect: dom.Rect
72 |
73 | current_rect: dom.Rect
74 |
75 | node_id: typing.Optional[dom.BackendNodeId] = None
76 |
77 | def to_json(self) -> T_JSON_DICT:
78 | json: T_JSON_DICT = dict()
79 | json["previousRect"] = self.previous_rect.to_json()
80 | json["currentRect"] = self.current_rect.to_json()
81 | if self.node_id is not None:
82 | json["nodeId"] = self.node_id.to_json()
83 | return json
84 |
85 | @classmethod
86 | def from_json(cls, json: T_JSON_DICT) -> LayoutShiftAttribution:
87 | return cls(
88 | previous_rect=dom.Rect.from_json(json["previousRect"]),
89 | current_rect=dom.Rect.from_json(json["currentRect"]),
90 | node_id=dom.BackendNodeId.from_json(json["nodeId"])
91 | if json.get("nodeId", None) is not None
92 | else None,
93 | )
94 |
95 |
96 | @dataclass
97 | class LayoutShift:
98 | """
99 | See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl
100 | """
101 |
102 | #: Score increment produced by this event.
103 | value: float
104 |
105 | had_recent_input: bool
106 |
107 | last_input_time: network.TimeSinceEpoch
108 |
109 | sources: typing.List[LayoutShiftAttribution]
110 |
111 | def to_json(self) -> T_JSON_DICT:
112 | json: T_JSON_DICT = dict()
113 | json["value"] = self.value
114 | json["hadRecentInput"] = self.had_recent_input
115 | json["lastInputTime"] = self.last_input_time.to_json()
116 | json["sources"] = [i.to_json() for i in self.sources]
117 | return json
118 |
119 | @classmethod
120 | def from_json(cls, json: T_JSON_DICT) -> LayoutShift:
121 | return cls(
122 | value=float(json["value"]),
123 | had_recent_input=bool(json["hadRecentInput"]),
124 | last_input_time=network.TimeSinceEpoch.from_json(json["lastInputTime"]),
125 | sources=[LayoutShiftAttribution.from_json(i) for i in json["sources"]],
126 | )
127 |
128 |
129 | @dataclass
130 | class TimelineEvent:
131 | #: Identifies the frame that this event is related to. Empty for non-frame targets.
132 | frame_id: page.FrameId
133 |
134 | #: The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype
135 | #: This determines which of the optional "details" fields is present.
136 | type_: str
137 |
138 | #: Name may be empty depending on the type.
139 | name: str
140 |
141 | #: Time in seconds since Epoch, monotonically increasing within document lifetime.
142 | time: network.TimeSinceEpoch
143 |
144 | #: Event duration, if applicable.
145 | duration: typing.Optional[float] = None
146 |
147 | lcp_details: typing.Optional[LargestContentfulPaint] = None
148 |
149 | layout_shift_details: typing.Optional[LayoutShift] = None
150 |
151 | def to_json(self) -> T_JSON_DICT:
152 | json: T_JSON_DICT = dict()
153 | json["frameId"] = self.frame_id.to_json()
154 | json["type"] = self.type_
155 | json["name"] = self.name
156 | json["time"] = self.time.to_json()
157 | if self.duration is not None:
158 | json["duration"] = self.duration
159 | if self.lcp_details is not None:
160 | json["lcpDetails"] = self.lcp_details.to_json()
161 | if self.layout_shift_details is not None:
162 | json["layoutShiftDetails"] = self.layout_shift_details.to_json()
163 | return json
164 |
165 | @classmethod
166 | def from_json(cls, json: T_JSON_DICT) -> TimelineEvent:
167 | return cls(
168 | frame_id=page.FrameId.from_json(json["frameId"]),
169 | type_=str(json["type"]),
170 | name=str(json["name"]),
171 | time=network.TimeSinceEpoch.from_json(json["time"]),
172 | duration=float(json["duration"])
173 | if json.get("duration", None) is not None
174 | else None,
175 | lcp_details=LargestContentfulPaint.from_json(json["lcpDetails"])
176 | if json.get("lcpDetails", None) is not None
177 | else None,
178 | layout_shift_details=LayoutShift.from_json(json["layoutShiftDetails"])
179 | if json.get("layoutShiftDetails", None) is not None
180 | else None,
181 | )
182 |
183 |
184 | def enable(
185 | event_types: typing.List[str],
186 | ) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
187 | """
188 | Previously buffered events would be reported before method returns.
189 | See also: timelineEventAdded
190 |
191 | :param event_types: The types of event to report, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype The specified filter overrides any previous filters, passing empty filter disables recording. Note that not all types exposed to the web platform are currently supported.
192 | """
193 | params: T_JSON_DICT = dict()
194 | params["eventTypes"] = [i for i in event_types]
195 | cmd_dict: T_JSON_DICT = {
196 | "method": "PerformanceTimeline.enable",
197 | "params": params,
198 | }
199 | json = yield cmd_dict
200 |
201 |
202 | @event_class("PerformanceTimeline.timelineEventAdded")
203 | @dataclass
204 | class TimelineEventAdded:
205 | """
206 | Sent when a performance timeline event is added. See reportPerformanceTimeline method.
207 | """
208 |
209 | event: TimelineEvent
210 |
211 | @classmethod
212 | def from_json(cls, json: T_JSON_DICT) -> TimelineEventAdded:
213 | return cls(event=TimelineEvent.from_json(json["event"]))
214 |
--------------------------------------------------------------------------------
/zendriver/cdp/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/zendriver/cdp/py.typed
--------------------------------------------------------------------------------
/zendriver/cdp/schema.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Schema
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | @dataclass
16 | class Domain:
17 | """
18 | Description of the protocol domain.
19 | """
20 |
21 | #: Domain name.
22 | name: str
23 |
24 | #: Domain version.
25 | version: str
26 |
27 | def to_json(self) -> T_JSON_DICT:
28 | json: T_JSON_DICT = dict()
29 | json["name"] = self.name
30 | json["version"] = self.version
31 | return json
32 |
33 | @classmethod
34 | def from_json(cls, json: T_JSON_DICT) -> Domain:
35 | return cls(
36 | name=str(json["name"]),
37 | version=str(json["version"]),
38 | )
39 |
40 |
41 | def get_domains() -> typing.Generator[T_JSON_DICT, T_JSON_DICT, typing.List[Domain]]:
42 | """
43 | Returns supported domains.
44 |
45 | :returns: List of supported domains.
46 | """
47 | cmd_dict: T_JSON_DICT = {
48 | "method": "Schema.getDomains",
49 | }
50 | json = yield cmd_dict
51 | return [Domain.from_json(i) for i in json["domains"]]
52 |
--------------------------------------------------------------------------------
/zendriver/cdp/tethering.py:
--------------------------------------------------------------------------------
1 | # DO NOT EDIT THIS FILE!
2 | #
3 | # This file is generated from the CDP specification. If you need to make
4 | # changes, edit the generator and regenerate all of the modules.
5 | #
6 | # CDP domain: Tethering (experimental)
7 |
8 | from __future__ import annotations
9 | import enum
10 | import typing
11 | from dataclasses import dataclass
12 | from .util import event_class, T_JSON_DICT
13 |
14 |
15 | def bind(port: int) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
16 | """
17 | Request browser port binding.
18 |
19 | :param port: Port number to bind.
20 | """
21 | params: T_JSON_DICT = dict()
22 | params["port"] = port
23 | cmd_dict: T_JSON_DICT = {
24 | "method": "Tethering.bind",
25 | "params": params,
26 | }
27 | json = yield cmd_dict
28 |
29 |
30 | def unbind(port: int) -> typing.Generator[T_JSON_DICT, T_JSON_DICT, None]:
31 | """
32 | Request browser port unbinding.
33 |
34 | :param port: Port number to unbind.
35 | """
36 | params: T_JSON_DICT = dict()
37 | params["port"] = port
38 | cmd_dict: T_JSON_DICT = {
39 | "method": "Tethering.unbind",
40 | "params": params,
41 | }
42 | json = yield cmd_dict
43 |
44 |
45 | @event_class("Tethering.accepted")
46 | @dataclass
47 | class Accepted:
48 | """
49 | Informs that port was successfully bound and got a specified connection id.
50 | """
51 |
52 | #: Port number that was successfully bound.
53 | port: int
54 | #: Connection id to be used.
55 | connection_id: str
56 |
57 | @classmethod
58 | def from_json(cls, json: T_JSON_DICT) -> Accepted:
59 | return cls(port=int(json["port"]), connection_id=str(json["connectionId"]))
60 |
--------------------------------------------------------------------------------
/zendriver/cdp/util.py:
--------------------------------------------------------------------------------
1 | import typing
2 |
3 | T_JSON_DICT = typing.Dict[str, typing.Any]
4 | _event_parsers = dict()
5 |
6 |
7 | def event_class(method):
8 | """A decorator that registers a class as an event class."""
9 |
10 | def decorate(cls):
11 | _event_parsers[method] = cls
12 | return cls
13 |
14 | return decorate
15 |
16 |
17 | def parse_json_event(json: T_JSON_DICT) -> typing.Any:
18 | """Parse a JSON dictionary into a CDP event."""
19 | return _event_parsers[json["method"]].from_json(json["params"])
20 |
--------------------------------------------------------------------------------
/zendriver/core/_contradict.py:
--------------------------------------------------------------------------------
1 | import logging
2 | import warnings as _warnings
3 | from collections.abc import Mapping as _Mapping
4 | from collections.abc import Sequence as _Sequence
5 |
6 | __logger__ = logging.getLogger(__name__)
7 |
8 |
9 | __all__ = ["cdict", "ContraDict"]
10 |
11 |
12 | def cdict(*args, **kwargs):
13 | """
14 | factory function
15 | """
16 | return ContraDict(*args, **kwargs)
17 |
18 |
19 | class ContraDict(dict):
20 | """
21 | directly inherited from dict
22 |
23 | accessible by attribute. o.x == o['x']
24 | This works also for all corner cases.
25 |
26 | native json.dumps and json.loads work with it
27 |
28 | names like "keys", "update", "values" etc won't overwrite the methods,
29 | but will just be available using dict lookup notation obj['items'] instead of obj.items
30 |
31 | all key names are converted to snake_case
32 | hyphen's (-), dot's (.) or whitespaces are replaced by underscore (_)
33 |
34 | autocomplete works even if the objects comes from a list
35 |
36 | recursive action. dict assignments will be converted too.
37 | """
38 |
39 | def __init__(self, *args, **kwargs):
40 | super().__init__()
41 | silent = kwargs.pop("silent", False)
42 | _ = dict(*args, **kwargs)
43 |
44 | # for key, val in dict(*args, **kwargs).items():
45 | # _[key] = val
46 | super().__setattr__("__dict__", self)
47 | for k, v in _.items():
48 | _check_key(k, self, False, silent)
49 | super().__setitem__(k, _wrap(self.__class__, v))
50 |
51 | def __setitem__(self, key, value):
52 | super().__setitem__(key, _wrap(self.__class__, value))
53 |
54 | def __setattr__(self, key, value):
55 | super().__setitem__(key, _wrap(self.__class__, value))
56 |
57 | def __getattribute__(self, attribute):
58 | if attribute in self:
59 | return self[attribute]
60 | if not _check_key(attribute, self, True, silent=True):
61 | return getattr(super(), attribute)
62 |
63 | return object.__getattribute__(self, attribute)
64 |
65 |
66 | def _wrap(cls, v):
67 | if isinstance(v, _Mapping):
68 | v = cls(v)
69 |
70 | elif isinstance(v, _Sequence) and not isinstance(
71 | v, (str, bytes, bytearray, set, tuple)
72 | ):
73 | v = list([_wrap(cls, x) for x in v])
74 | return v
75 |
76 |
77 | _warning_names = (
78 | "items",
79 | "keys",
80 | "values",
81 | "update",
82 | "clear",
83 | "copy",
84 | "fromkeys",
85 | "get",
86 | "items",
87 | "keys",
88 | "pop",
89 | "popitem",
90 | "setdefault",
91 | "update",
92 | "values",
93 | "class",
94 | )
95 |
96 | _warning_names_message = """\n\
97 | While creating a ContraDict object, a key offending key name '{0}' has been found, which might behave unexpected.
98 | you will only be able to look it up using key, eg. myobject['{0}']. myobject.{0} will not work with that name.
99 | """
100 |
101 |
102 | def _check_key(key: str, mapping: _Mapping, boolean: bool = False, silent=False):
103 | """checks `key` and warns if needed
104 |
105 | :param key:
106 | :param boolean: return True or False instead of passthrough
107 | :return:
108 | """
109 | e = None
110 | if not isinstance(key, (str,)):
111 | if boolean:
112 | return True
113 | return key
114 | if key.lower() in _warning_names or any(_ in key for _ in ("-", ".")):
115 | if not silent:
116 | _warnings.warn(_warning_names_message.format(key))
117 | e = True
118 | if not boolean:
119 | return key
120 | return not e
121 |
--------------------------------------------------------------------------------
/zendriver/core/expect.py:
--------------------------------------------------------------------------------
1 | import asyncio
2 | import re
3 | from typing import Union
4 |
5 | from .. import cdp
6 | from .connection import Connection
7 |
8 |
9 | class BaseRequestExpectation:
10 | """
11 | Base class for handling request and response expectations.
12 | This class provides a context manager to wait for specific network requests and responses
13 | based on a URL pattern. It sets up handlers for request and response events and provides
14 | properties to access the request, response, and response body.
15 | :param tab: The Tab instance to monitor.
16 | :type tab: Tab
17 | :param url_pattern: The URL pattern to match requests and responses.
18 | :type url_pattern: Union[str, re.Pattern[str]]
19 | """
20 |
21 | def __init__(self, tab: Connection, url_pattern: Union[str, re.Pattern[str]]):
22 | self.tab = tab
23 | self.url_pattern = url_pattern
24 | self.request_future: asyncio.Future[cdp.network.RequestWillBeSent] = (
25 | asyncio.Future()
26 | )
27 | self.response_future: asyncio.Future[cdp.network.ResponseReceived] = (
28 | asyncio.Future()
29 | )
30 | self.request_id: Union[cdp.network.RequestId, None] = None
31 |
32 | async def _request_handler(self, event: cdp.network.RequestWillBeSent):
33 | """
34 | Internal handler for request events.
35 | :param event: The request event.
36 | :type event: cdp.network.RequestWillBeSent
37 | """
38 | if re.fullmatch(self.url_pattern, event.request.url):
39 | self._remove_request_handler()
40 | self.request_id = event.request_id
41 | self.request_future.set_result(event)
42 |
43 | async def _response_handler(self, event: cdp.network.ResponseReceived):
44 | """
45 | Internal handler for response events.
46 | :param event: The response event.
47 | :type event: cdp.network.ResponseReceived
48 | """
49 | if event.request_id == self.request_id:
50 | self._remove_response_handler()
51 | self.response_future.set_result(event)
52 |
53 | def _remove_request_handler(self):
54 | """
55 | Remove the request event handler.
56 | """
57 | self.tab.remove_handlers(cdp.network.RequestWillBeSent, self._request_handler)
58 |
59 | def _remove_response_handler(self):
60 | """
61 | Remove the response event handler.
62 | """
63 | self.tab.remove_handlers(cdp.network.ResponseReceived, self._response_handler)
64 |
65 | async def __aenter__(self):
66 | """
67 | Enter the context manager, adding request and response handlers.
68 | """
69 | self.tab.add_handler(cdp.network.RequestWillBeSent, self._request_handler)
70 | self.tab.add_handler(cdp.network.ResponseReceived, self._response_handler)
71 | return self
72 |
73 | async def __aexit__(self, *args):
74 | """
75 | Exit the context manager, removing request and response handlers.
76 | """
77 | self._remove_request_handler()
78 | self._remove_response_handler()
79 |
80 | @property
81 | async def request(self):
82 | """
83 | Get the matched request.
84 | :return: The matched request.
85 | :rtype: cdp.network.Request
86 | """
87 | return (await self.request_future).request
88 |
89 | @property
90 | async def response(self):
91 | """
92 | Get the matched response.
93 | :return: The matched response.
94 | :rtype: cdp.network.Response
95 | """
96 | return (await self.response_future).response
97 |
98 | @property
99 | async def response_body(self):
100 | """
101 | Get the body of the matched response.
102 | :return: The response body.
103 | :rtype: str
104 | """
105 | request_id = (await self.request_future).request_id
106 | body = await self.tab.send(cdp.network.get_response_body(request_id=request_id))
107 | return body
108 |
109 |
110 | class RequestExpectation(BaseRequestExpectation):
111 | """
112 | Class for handling request expectations.
113 | This class extends `BaseRequestExpectation` and provides a property to access the matched request.
114 | :param tab: The Tab instance to monitor.
115 | :type tab: Tab
116 | :param url_pattern: The URL pattern to match requests.
117 | :type url_pattern: Union[str, re.Pattern[str]]
118 | """
119 |
120 | @property
121 | async def value(self) -> cdp.network.RequestWillBeSent:
122 | """
123 | Get the matched request event.
124 | :return: The matched request event.
125 | :rtype: cdp.network.RequestWillBeSent
126 | """
127 | return await self.request_future
128 |
129 |
130 | class ResponseExpectation(BaseRequestExpectation):
131 | """
132 | Class for handling response expectations.
133 | This class extends `BaseRequestExpectation` and provides a property to access the matched response.
134 | :param tab: The Tab instance to monitor.
135 | :type tab: Tab
136 | :param url_pattern: The URL pattern to match responses.
137 | :type url_pattern: Union[str, re.Pattern[str]]
138 | """
139 |
140 | @property
141 | async def value(self) -> cdp.network.ResponseReceived:
142 | """
143 | Get the matched response event.
144 | :return: The matched response event.
145 | :rtype: cdp.network.ResponseReceived
146 | """
147 | return await self.response_future
148 |
149 |
150 | class DownloadExpectation:
151 | def __init__(self, tab: Connection):
152 | self.tab = tab
153 | self.future: asyncio.Future[cdp.browser.DownloadWillBegin] = asyncio.Future()
154 | # TODO: Improve
155 | self.default_behavior = (
156 | self.tab._download_behavior[0] if self.tab._download_behavior else "default"
157 | )
158 |
159 | async def _handler(self, event: cdp.browser.DownloadWillBegin):
160 | self._remove_handler()
161 | self.future.set_result(event)
162 |
163 | def _remove_handler(self):
164 | self.tab.remove_handlers(cdp.browser.DownloadWillBegin, self._handler)
165 |
166 | async def __aenter__(self):
167 | """
168 | Enter the context manager, adding download handler, set download behavior to deny.
169 | """
170 | await self.tab.send(
171 | cdp.browser.set_download_behavior(behavior="deny", events_enabled=True)
172 | )
173 | self.tab.add_handler(cdp.browser.DownloadWillBegin, self._handler)
174 | return self
175 |
176 | async def __aexit__(self, *args):
177 | """
178 | Exit the context manager, removing handler, set download behavior to default.
179 | """
180 | await self.tab.send(
181 | cdp.browser.set_download_behavior(behavior=self.default_behavior)
182 | )
183 | self._remove_handler()
184 |
185 | @property
186 | async def value(self) -> cdp.browser.DownloadWillBegin:
187 | return await self.future
188 |
--------------------------------------------------------------------------------
/zendriver/py.typed:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/stephanlensky/zendriver/fad53c215c1cacc394a5406b2f4dbe369f52ad03/zendriver/py.typed
--------------------------------------------------------------------------------