├── trio_cdp ├── py.typed ├── generated │ ├── schema.py │ ├── inspector.py │ ├── tethering.py │ ├── media.py │ ├── device_orientation.py │ ├── event_breakpoints.py │ ├── performance_timeline.py │ ├── console.py │ ├── __init__.py │ ├── log.py │ ├── system_info.py │ ├── database.py │ ├── web_audio.py │ ├── performance.py │ ├── background_service.py │ ├── io.py │ ├── dom_storage.py │ ├── cast.py │ ├── security.py │ ├── headless_experimental.py │ ├── cache_storage.py │ ├── audits.py │ ├── memory.py │ ├── tracing.py │ ├── animation.py │ ├── service_worker.py │ ├── dom_snapshot.py │ ├── web_authn.py │ ├── profiler.py │ ├── layer_tree.py │ ├── indexed_db.py │ ├── dom_debugger.py │ ├── heap_profiler.py │ ├── storage.py │ ├── accessibility.py │ ├── fetch.py │ └── browser.py └── context.py ├── generator ├── __init__.py ├── test_generate.py └── generate.py ├── docs ├── .gitignore ├── requirements.txt ├── changelog.rst ├── index.rst ├── Makefile ├── make.bat ├── installation.rst ├── conf.py ├── api.rst └── getting_started.rst ├── pytest.ini ├── .gitignore ├── .readthedocs.yml ├── .travis.yml ├── Makefile ├── tests ├── __init__.py └── test_session_detach.py ├── .github └── workflows │ ├── auto-feature-request.yml │ ├── auto-sec-scan.yml │ ├── auto-bug-report.yml │ ├── auto-close-issues.yml │ ├── auto-label.yml │ ├── auto-label-comment-prs.yml │ ├── auto-assign-pr.yml │ ├── auto-assign-copilot.yml │ ├── auto-copilot-org-playwright-loopv2.yaml │ ├── trigger-all-repos.yml │ ├── auto-copilot-playwright-auto-test.yml │ └── auto-copilot-org-playwright-loop.yaml ├── pyproject.toml ├── LICENSE ├── examples ├── get_title.py ├── screenshot.py ├── take_heap_snapshot.py └── notebook.ipynb └── README.md /trio_cdp/py.typed: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /generator/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | _build 2 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | trio_mode = true 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .coverage 2 | .ipynb_checkpoints 3 | .mypy_cache 4 | __pycache__ 5 | .vscode 6 | *.egg-info 7 | build 8 | chrome-mac 9 | dist 10 | test.png 11 | venv 12 | TODO.md 13 | -------------------------------------------------------------------------------- /docs/requirements.txt: -------------------------------------------------------------------------------- 1 | # RTD does not support Poetry natively, so this is a temporary workaround. See: 2 | # https://github.com/readthedocs/readthedocs.org/issues/4912 3 | sphinx 4 | sphinx-autodoc-typehints 5 | sphinx-rtd-theme 6 | . 7 | -------------------------------------------------------------------------------- /.readthedocs.yml: -------------------------------------------------------------------------------- 1 | # See https://docs.readthedocs.io/en/stable/config-file/v2.html for details 2 | version: 2 3 | 4 | sphinx: 5 | configuration: docs/conf.py 6 | 7 | python: 8 | version: 3.7 9 | install: 10 | - requirements: docs/requirements.txt 11 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | dist: xenial 3 | 4 | git: 5 | depth: 1 6 | 7 | matrix: 8 | include: 9 | - python: 3.7 10 | 11 | before_install: 12 | - pip install poetry 13 | 14 | install: 15 | - poetry install 16 | 17 | script: 18 | - poetry run make 19 | -------------------------------------------------------------------------------- /docs/changelog.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.6.0 5 | ----- 6 | 7 | * Simplified calling convention for CDP commands. 8 | 9 | 0.5.0 10 | ----- 11 | 12 | * **Backwards Compability Break:** Rename `open_cdp_connection()` to `open_cdp()`. 13 | * Fix `ConnectionClosed` bug. 14 | 15 | 0.4.0 16 | ----- 17 | 18 | * Add support for passing in a nursery. (Supports usage in Jupyter notebook.) 19 | 20 | 0.3.0 21 | ----- 22 | 23 | * New APIs for enabling DOM events and Page events. 24 | 25 | 0.2.0 26 | ----- 27 | 28 | * Restructure event listeners. 29 | 30 | 0.1.0 31 | ----- 32 | 33 | * Initial version 34 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # The targets in this makefile should be executed inside Poetry, i.e. `poetry run make 2 | # docs`. 3 | 4 | .PHONY: docs 5 | 6 | default: mypy-generate test-generate generate test-import mypy-cdp test-cdp 7 | 8 | docs: 9 | $(MAKE) -C docs html 10 | 11 | generate: 12 | python generator/generate.py 13 | 14 | mypy-cdp: 15 | mypy trio_cdp/ 16 | 17 | mypy-generate: 18 | mypy generator/ 19 | 20 | test-cdp: 21 | pytest tests/ --cov=trio_cdp --cov-report=term-missing 22 | 23 | test-generate: 24 | pytest generator/ 25 | 26 | test-import: 27 | python -c 'import trio_cdp; print(trio_cdp.accessibility)' 28 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Trio CDP 2 | ======== 3 | 4 | This Python library performs remote control of any web browser that implements the 5 | Chrome DevTools Protocol. It is built using the type wrappers in 6 | `python-chrome-devtools-protocol `_ and implements I/O 7 | using `Trio `_. This library handles the WebSocket 8 | negotiation and session management, allowing you to transparently multiplex commands, 9 | responses, and events over a single connection. 10 | 11 | .. toctree:: 12 | :caption: Contents 13 | :maxdepth: 1 14 | 15 | installation 16 | getting_started 17 | api 18 | changelog 19 | -------------------------------------------------------------------------------- /trio_cdp/generated/schema.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.schema 12 | from cdp.schema import ( 13 | Domain 14 | ) 15 | 16 | 17 | async def get_domains() -> typing.List[Domain]: 18 | r''' 19 | Returns supported domains. 20 | 21 | :returns: List of supported domains. 22 | ''' 23 | session = get_session_context('schema.get_domains') 24 | return await session.execute(cdp.schema.get_domains()) 25 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line, and also 5 | # from the environment for the first two. 6 | SPHINXOPTS ?= 7 | SPHINXBUILD ?= sphinx-build 8 | SOURCEDIR = . 9 | BUILDDIR = _build 10 | 11 | # Put it first so that "make" without argument is like "make help". 12 | help: 13 | @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 14 | 15 | .PHONY: help Makefile 16 | 17 | # Catch-all target: route all unknown targets to Sphinx using the new 18 | # "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). 19 | %: Makefile 20 | @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) 21 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- 1 | from functools import wraps 2 | 3 | import pytest 4 | import trio 5 | 6 | 7 | class fail_after: 8 | ''' This decorator fails if the runtime of the decorated function (as 9 | measured by the Trio clock) exceeds the specified value. ''' 10 | def __init__(self, seconds): 11 | self._seconds = seconds 12 | 13 | def __call__(self, fn): 14 | @wraps(fn) 15 | async def wrapper(*args, **kwargs): 16 | with trio.move_on_after(self._seconds) as cancel_scope: 17 | await fn(*args, **kwargs) 18 | if cancel_scope.cancelled_caught: 19 | pytest.fail('Test runtime exceeded the maximum {} seconds' 20 | .format(self._seconds)) 21 | return wrapper -------------------------------------------------------------------------------- /.github/workflows/auto-feature-request.yml: -------------------------------------------------------------------------------- 1 | --- 2 | uto-amazonq-review.properties.json 3 | name: Feature request 4 | uto-amazonq-review.properties.json 5 | about: Suggest an idea for this project 6 | uto-amazonq-review.properties.json 7 | title: "Feature Request: " 8 | uto-amazonq-review.properties.json 9 | labels: ["enhancement", "copilot"] 10 | uto-amazonq-review.properties.json 11 | assignees: ["copilot"] # <-- TUNE ME 12 | uto-amazonq-review.properties.json 13 | --- 14 | uto-amazonq-review.properties.json 15 | 16 | uto-amazonq-review.properties.json 17 | **Describe the solution you'd like** 18 | uto-amazonq-review.properties.json 19 | A clear and concise description of what you want to happen. 20 | uto-amazonq-review.properties.json 21 | 22 | uto-amazonq-review.properties.json 23 | **Additional context** 24 | uto-amazonq-review.properties.json 25 | Add any other context or screenshots about the feature request here. 26 | uto-amazonq-review.properties.json 27 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | pushd %~dp0 4 | 5 | REM Command file for Sphinx documentation 6 | 7 | if "%SPHINXBUILD%" == "" ( 8 | set SPHINXBUILD=sphinx-build 9 | ) 10 | set SOURCEDIR=. 11 | set BUILDDIR=_build 12 | 13 | if "%1" == "" goto help 14 | 15 | %SPHINXBUILD% >NUL 2>NUL 16 | if errorlevel 9009 ( 17 | echo. 18 | echo.The 'sphinx-build' command was not found. Make sure you have Sphinx 19 | echo.installed, then set the SPHINXBUILD environment variable to point 20 | echo.to the full path of the 'sphinx-build' executable. Alternatively you 21 | echo.may add the Sphinx directory to PATH. 22 | echo. 23 | echo.If you don't have Sphinx installed, grab it from 24 | echo.http://sphinx-doc.org/ 25 | exit /b 1 26 | ) 27 | 28 | %SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 29 | goto end 30 | 31 | :help 32 | %SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% %O% 33 | 34 | :end 35 | popd 36 | -------------------------------------------------------------------------------- /trio_cdp/generated/inspector.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.inspector 12 | from cdp.inspector import ( 13 | Detached, 14 | TargetCrashed, 15 | TargetReloadedAfterCrash 16 | ) 17 | 18 | 19 | async def disable() -> None: 20 | r''' 21 | Disables inspector domain notifications. 22 | ''' 23 | session = get_session_context('inspector.disable') 24 | return await session.execute(cdp.inspector.disable()) 25 | 26 | 27 | async def enable() -> None: 28 | r''' 29 | Enables inspector domain notifications. 30 | ''' 31 | session = get_session_context('inspector.enable') 32 | return await session.execute(cdp.inspector.enable()) 33 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "trio-chrome-devtools-protocol" 3 | packages = [{include = "trio_cdp"}] 4 | version = "0.7.0" 5 | description = "Trio driver for Chrome DevTools Protocol (CDP)" 6 | authors = ["Mark E. Haase ", "Brian Mackintosh "] 7 | license = "MIT" 8 | readme = "README.md" 9 | homepage = "https://github.com/hyperiongray/python-chrome-devtools-protocol" 10 | classifiers = [ 11 | "Development Status :: 3 - Alpha", 12 | "Intended Audience :: Developers", 13 | "Topic :: Internet" 14 | ] 15 | 16 | [tool.poetry.dependencies] 17 | python = "^3.7" 18 | chrome-devtools-protocol = "^0.4.0" 19 | trio = "^0.13.0" 20 | trio_websocket = "^0.8.0" 21 | 22 | [tool.poetry.dev-dependencies] 23 | mypy = "^0.770" 24 | pytest = "^5.4.1" 25 | pytest-cov = "^2.8.1" 26 | pytest-trio = "^0.5.2" 27 | sphinx = "^3.0.1" 28 | sphinx-rtd-theme = "^0.4.3" 29 | sphinx-autodoc-typehints = "^1.10.3" 30 | 31 | [build-system] 32 | requires = ["poetry>=0.12"] 33 | build-backend = "poetry.masonry.api" 34 | -------------------------------------------------------------------------------- /trio_cdp/generated/tethering.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.tethering 12 | from cdp.tethering import ( 13 | Accepted 14 | ) 15 | 16 | 17 | async def bind( 18 | port: int 19 | ) -> None: 20 | r''' 21 | Request browser port binding. 22 | 23 | :param port: Port number to bind. 24 | ''' 25 | session = get_session_context('tethering.bind') 26 | return await session.execute(cdp.tethering.bind(port)) 27 | 28 | 29 | async def unbind( 30 | port: int 31 | ) -> None: 32 | r''' 33 | Request browser port unbinding. 34 | 35 | :param port: Port number to unbind. 36 | ''' 37 | session = get_session_context('tethering.unbind') 38 | return await session.execute(cdp.tethering.unbind(port)) 39 | -------------------------------------------------------------------------------- /trio_cdp/generated/media.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.media 12 | from cdp.media import ( 13 | PlayerError, 14 | PlayerErrorsRaised, 15 | PlayerEvent, 16 | PlayerEventsAdded, 17 | PlayerId, 18 | PlayerMessage, 19 | PlayerMessagesLogged, 20 | PlayerPropertiesChanged, 21 | PlayerProperty, 22 | PlayersCreated, 23 | Timestamp 24 | ) 25 | 26 | 27 | async def disable() -> None: 28 | r''' 29 | Disables the Media domain. 30 | ''' 31 | session = get_session_context('media.disable') 32 | return await session.execute(cdp.media.disable()) 33 | 34 | 35 | async def enable() -> None: 36 | r''' 37 | Enables the Media domain 38 | ''' 39 | session = get_session_context('media.enable') 40 | return await session.execute(cdp.media.enable()) 41 | -------------------------------------------------------------------------------- /.github/workflows/auto-sec-scan.yml: -------------------------------------------------------------------------------- 1 | name: "Security Scan on PR" 2 | uto-amazonq-review.properties.json 3 | on: 4 | uto-amazonq-review.properties.json 5 | pull_request: 6 | uto-amazonq-review.properties.json 7 | types: [opened, synchronize, reopened] 8 | uto-amazonq-review.properties.json 9 | jobs: 10 | uto-amazonq-review.properties.json 11 | security_scan: 12 | uto-amazonq-review.properties.json 13 | runs-on: self-hosted 14 | uto-amazonq-review.properties.json 15 | steps: 16 | uto-amazonq-review.properties.json 17 | - name: Checkout code 18 | uto-amazonq-review.properties.json 19 | uses: actions/checkout@main 20 | uto-amazonq-review.properties.json 21 | - name: Run CodeQL Scan 22 | uto-amazonq-review.properties.json 23 | uses: github/codeql-action/init@main 24 | uto-amazonq-review.properties.json 25 | with: 26 | uto-amazonq-review.properties.json 27 | languages: 'python,javascript' 28 | uto-amazonq-review.properties.json 29 | - name: Perform CodeQL Analysis 30 | uto-amazonq-review.properties.json 31 | uses: github/codeql-action/analyze@main 32 | uto-amazonq-review.properties.json 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2018 Hyperion Gray 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /trio_cdp/generated/device_orientation.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.device_orientation 12 | 13 | async def clear_device_orientation_override() -> None: 14 | r''' 15 | Clears the overridden Device Orientation. 16 | ''' 17 | session = get_session_context('device_orientation.clear_device_orientation_override') 18 | return await session.execute(cdp.device_orientation.clear_device_orientation_override()) 19 | 20 | 21 | async def set_device_orientation_override( 22 | alpha: float, 23 | beta: float, 24 | gamma: float 25 | ) -> None: 26 | r''' 27 | Overrides the Device Orientation. 28 | 29 | :param alpha: Mock alpha 30 | :param beta: Mock beta 31 | :param gamma: Mock gamma 32 | ''' 33 | session = get_session_context('device_orientation.set_device_orientation_override') 34 | return await session.execute(cdp.device_orientation.set_device_orientation_override(alpha, beta, gamma)) 35 | -------------------------------------------------------------------------------- /trio_cdp/generated/event_breakpoints.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.event_breakpoints 12 | 13 | async def remove_instrumentation_breakpoint( 14 | event_name: str 15 | ) -> None: 16 | r''' 17 | Removes breakpoint on particular native event. 18 | 19 | :param event_name: Instrumentation name to stop on. 20 | ''' 21 | session = get_session_context('event_breakpoints.remove_instrumentation_breakpoint') 22 | return await session.execute(cdp.event_breakpoints.remove_instrumentation_breakpoint(event_name)) 23 | 24 | 25 | async def set_instrumentation_breakpoint( 26 | event_name: str 27 | ) -> None: 28 | r''' 29 | Sets breakpoint on particular native event. 30 | 31 | :param event_name: Instrumentation name to stop on. 32 | ''' 33 | session = get_session_context('event_breakpoints.set_instrumentation_breakpoint') 34 | return await session.execute(cdp.event_breakpoints.set_instrumentation_breakpoint(event_name)) 35 | -------------------------------------------------------------------------------- /trio_cdp/generated/performance_timeline.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.performance_timeline 12 | from cdp.performance_timeline import ( 13 | LargestContentfulPaint, 14 | LayoutShift, 15 | LayoutShiftAttribution, 16 | TimelineEvent, 17 | TimelineEventAdded 18 | ) 19 | 20 | 21 | async def enable( 22 | event_types: typing.List[str] 23 | ) -> None: 24 | r''' 25 | Previously buffered events would be reported before method returns. 26 | See also: timelineEventAdded 27 | 28 | :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. 29 | ''' 30 | session = get_session_context('performance_timeline.enable') 31 | return await session.execute(cdp.performance_timeline.enable(event_types)) 32 | -------------------------------------------------------------------------------- /trio_cdp/generated/console.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.console 12 | from cdp.console import ( 13 | ConsoleMessage, 14 | MessageAdded 15 | ) 16 | 17 | 18 | async def clear_messages() -> None: 19 | r''' 20 | Does nothing. 21 | ''' 22 | session = get_session_context('console.clear_messages') 23 | return await session.execute(cdp.console.clear_messages()) 24 | 25 | 26 | async def disable() -> None: 27 | r''' 28 | Disables console domain, prevents further console messages from being reported to the client. 29 | ''' 30 | session = get_session_context('console.disable') 31 | return await session.execute(cdp.console.disable()) 32 | 33 | 34 | async def enable() -> None: 35 | r''' 36 | Enables console domain, sends the messages collected so far to the client by means of the 37 | ``messageAdded`` notification. 38 | ''' 39 | session = get_session_context('console.enable') 40 | return await session.execute(cdp.console.enable()) 41 | -------------------------------------------------------------------------------- /.github/workflows/auto-bug-report.yml: -------------------------------------------------------------------------------- 1 | --- 2 | uto-amazonq-review.properties.json 3 | name: Bug report 4 | uto-amazonq-review.properties.json 5 | about: Create a bug report to help us improve 6 | uto-amazonq-review.properties.json 7 | title: "Bug: " 8 | uto-amazonq-review.properties.json 9 | labels: ["bug", "triage", "copilot"] 10 | uto-amazonq-review.properties.json 11 | assignees: ["copilot"] # <-- TUNE ME 12 | uto-amazonq-review.properties.json 13 | --- 14 | uto-amazonq-review.properties.json 15 | 16 | uto-amazonq-review.properties.json 17 | **Describe the bug** 18 | uto-amazonq-review.properties.json 19 | A clear and concise description of what the bug is. 20 | uto-amazonq-review.properties.json 21 | 22 | uto-amazonq-review.properties.json 23 | **To Reproduce** 24 | uto-amazonq-review.properties.json 25 | Steps to reproduce the behavior. 26 | uto-amazonq-review.properties.json 27 | 28 | uto-amazonq-review.properties.json 29 | **Expected behavior** 30 | uto-amazonq-review.properties.json 31 | A clear and concise description of what you expected to happen. 32 | uto-amazonq-review.properties.json 33 | 34 | uto-amazonq-review.properties.json 35 | **Additional context** 36 | uto-amazonq-review.properties.json 37 | Add any other context or screenshots about the bug here. 38 | uto-amazonq-review.properties.json 39 | -------------------------------------------------------------------------------- /.github/workflows/auto-close-issues.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues and PRs once a week" 2 | uto-amazonq-review.properties.json 3 | on: 4 | uto-amazonq-review.properties.json 5 | schedule: 6 | uto-amazonq-review.properties.json 7 | - cron: '0 0 * * 0' 8 | uto-amazonq-review.properties.json 9 | jobs: 10 | uto-amazonq-review.properties.json 11 | close_stale: 12 | uto-amazonq-review.properties.json 13 | runs-on: self-hosted 14 | uto-amazonq-review.properties.json 15 | steps: 16 | uto-amazonq-review.properties.json 17 | - uses: actions/stale@main 18 | uto-amazonq-review.properties.json 19 | with: 20 | uto-amazonq-review.properties.json 21 | days-before-stale: 21 22 | uto-amazonq-review.properties.json 23 | days-before-close: 7 24 | uto-amazonq-review.properties.json 25 | stale-issue-message: "This issue has been marked stale and will be closed in 7 days unless updated." 26 | uto-amazonq-review.properties.json 27 | close-issue-message: "Closing as stale, feel free to reopen!" 28 | uto-amazonq-review.properties.json 29 | stale-pr-message: "This PR has been marked stale and will be closed in 7 days unless updated." 30 | uto-amazonq-review.properties.json 31 | close-pr-message: "Closing as stale, feel free to reopen!" 32 | uto-amazonq-review.properties.json 33 | exempt-issue-labels: "pinned,security" 34 | uto-amazonq-review.properties.json 35 | -------------------------------------------------------------------------------- /trio_cdp/generated/__init__.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from . import accessibility 7 | from . import animation 8 | from . import audits 9 | from . import background_service 10 | from . import browser 11 | from . import cache_storage 12 | from . import cast 13 | from . import console 14 | from . import css 15 | from . import database 16 | from . import debugger 17 | from . import device_orientation 18 | from . import dom 19 | from . import dom_debugger 20 | from . import dom_snapshot 21 | from . import dom_storage 22 | from . import emulation 23 | from . import event_breakpoints 24 | from . import fetch 25 | from . import headless_experimental 26 | from . import heap_profiler 27 | from . import indexed_db 28 | from . import input_ 29 | from . import inspector 30 | from . import io 31 | from . import layer_tree 32 | from . import log 33 | from . import media 34 | from . import memory 35 | from . import network 36 | from . import overlay 37 | from . import page 38 | from . import performance 39 | from . import performance_timeline 40 | from . import profiler 41 | from . import runtime 42 | from . import schema 43 | from . import security 44 | from . import service_worker 45 | from . import storage 46 | from . import system_info 47 | from . import target 48 | from . import tethering 49 | from . import tracing 50 | from . import web_audio 51 | from . import web_authn 52 | -------------------------------------------------------------------------------- /.github/workflows/auto-label.yml: -------------------------------------------------------------------------------- 1 | # Auto-label new issues with your default labels! 2 | uto-amazonq-review.properties.json 3 | # Set or add labels in the 'labels' list. 4 | uto-amazonq-review.properties.json 5 | 6 | uto-amazonq-review.properties.json 7 | name: Auto Label New Issues 8 | uto-amazonq-review.properties.json 9 | 10 | uto-amazonq-review.properties.json 11 | on: 12 | uto-amazonq-review.properties.json 13 | issues: 14 | uto-amazonq-review.properties.json 15 | types: [opened] 16 | uto-amazonq-review.properties.json 17 | 18 | uto-amazonq-review.properties.json 19 | jobs: 20 | uto-amazonq-review.properties.json 21 | label: 22 | uto-amazonq-review.properties.json 23 | runs-on: self-hosted 24 | uto-amazonq-review.properties.json 25 | steps: 26 | uto-amazonq-review.properties.json 27 | - name: Add labels 28 | uto-amazonq-review.properties.json 29 | uses: actions/github-script@main 30 | uto-amazonq-review.properties.json 31 | with: 32 | uto-amazonq-review.properties.json 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | uto-amazonq-review.properties.json 35 | script: | 36 | uto-amazonq-review.properties.json 37 | // Add or tweak your labels here 38 | uto-amazonq-review.properties.json 39 | const labels = ["triage", "copilot"]; // <-- TUNE ME! 40 | uto-amazonq-review.properties.json 41 | await github.rest.issues.addLabels({ 42 | uto-amazonq-review.properties.json 43 | owner: context.repo.owner, 44 | uto-amazonq-review.properties.json 45 | repo: context.repo.repo, 46 | uto-amazonq-review.properties.json 47 | issue_number: context.issue.number, 48 | uto-amazonq-review.properties.json 49 | labels 50 | uto-amazonq-review.properties.json 51 | }); 52 | uto-amazonq-review.properties.json 53 | -------------------------------------------------------------------------------- /trio_cdp/generated/log.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.log 12 | from cdp.log import ( 13 | EntryAdded, 14 | LogEntry, 15 | ViolationSetting 16 | ) 17 | 18 | 19 | async def clear() -> None: 20 | r''' 21 | Clears the log. 22 | ''' 23 | session = get_session_context('log.clear') 24 | return await session.execute(cdp.log.clear()) 25 | 26 | 27 | async def disable() -> None: 28 | r''' 29 | Disables log domain, prevents further log entries from being reported to the client. 30 | ''' 31 | session = get_session_context('log.disable') 32 | return await session.execute(cdp.log.disable()) 33 | 34 | 35 | async def enable() -> None: 36 | r''' 37 | Enables log domain, sends the entries collected so far to the client by means of the 38 | ``entryAdded`` notification. 39 | ''' 40 | session = get_session_context('log.enable') 41 | return await session.execute(cdp.log.enable()) 42 | 43 | 44 | async def start_violations_report( 45 | config: typing.List[ViolationSetting] 46 | ) -> None: 47 | r''' 48 | start violation reporting. 49 | 50 | :param config: Configuration for violations. 51 | ''' 52 | session = get_session_context('log.start_violations_report') 53 | return await session.execute(cdp.log.start_violations_report(config)) 54 | 55 | 56 | async def stop_violations_report() -> None: 57 | r''' 58 | Stop violation reporting. 59 | ''' 60 | session = get_session_context('log.stop_violations_report') 61 | return await session.execute(cdp.log.stop_violations_report()) 62 | -------------------------------------------------------------------------------- /trio_cdp/generated/system_info.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.system_info 12 | from cdp.system_info import ( 13 | GPUDevice, 14 | GPUInfo, 15 | ImageDecodeAcceleratorCapability, 16 | ImageType, 17 | ProcessInfo, 18 | Size, 19 | SubsamplingFormat, 20 | VideoDecodeAcceleratorCapability, 21 | VideoEncodeAcceleratorCapability 22 | ) 23 | 24 | 25 | async def get_info() -> typing.Tuple[GPUInfo, str, str, str]: 26 | r''' 27 | Returns information about the system. 28 | 29 | :returns: A tuple with the following items: 30 | 31 | 0. **gpu** - Information about the GPUs on the system. 32 | 1. **modelName** - A platform-dependent description of the model of the machine. On Mac OS, this is, for example, 'MacBookPro'. Will be the empty string if not supported. 33 | 2. **modelVersion** - A platform-dependent description of the version of the machine. On Mac OS, this is, for example, '10.1'. Will be the empty string if not supported. 34 | 3. **commandLine** - The command line string used to launch the browser. Will be the empty string if not supported. 35 | ''' 36 | session = get_session_context('system_info.get_info') 37 | return await session.execute(cdp.system_info.get_info()) 38 | 39 | 40 | async def get_process_info() -> typing.List[ProcessInfo]: 41 | r''' 42 | Returns information about all running processes. 43 | 44 | :returns: An array of process info blocks. 45 | ''' 46 | session = get_session_context('system_info.get_process_info') 47 | return await session.execute(cdp.system_info.get_process_info()) 48 | -------------------------------------------------------------------------------- /trio_cdp/generated/database.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.database 12 | from cdp.database import ( 13 | AddDatabase, 14 | Database, 15 | DatabaseId, 16 | Error 17 | ) 18 | 19 | 20 | async def disable() -> None: 21 | r''' 22 | Disables database tracking, prevents database events from being sent to the client. 23 | ''' 24 | session = get_session_context('database.disable') 25 | return await session.execute(cdp.database.disable()) 26 | 27 | 28 | async def enable() -> None: 29 | r''' 30 | Enables database tracking, database events will now be delivered to the client. 31 | ''' 32 | session = get_session_context('database.enable') 33 | return await session.execute(cdp.database.enable()) 34 | 35 | 36 | async def execute_sql( 37 | database_id: DatabaseId, 38 | query: str 39 | ) -> typing.Tuple[typing.Optional[typing.List[str]], typing.Optional[typing.List[typing.Any]], typing.Optional[Error]]: 40 | r''' 41 | :param database_id: 42 | :param query: 43 | :returns: A tuple with the following items: 44 | 45 | 0. **columnNames** - 46 | 1. **values** - 47 | 2. **sqlError** - 48 | ''' 49 | session = get_session_context('database.execute_sql') 50 | return await session.execute(cdp.database.execute_sql(database_id, query)) 51 | 52 | 53 | async def get_database_table_names( 54 | database_id: DatabaseId 55 | ) -> typing.List[str]: 56 | r''' 57 | :param database_id: 58 | :returns: 59 | ''' 60 | session = get_session_context('database.get_database_table_names') 61 | return await session.execute(cdp.database.get_database_table_names(database_id)) 62 | -------------------------------------------------------------------------------- /trio_cdp/generated/web_audio.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.web_audio 12 | from cdp.web_audio import ( 13 | AudioListener, 14 | AudioListenerCreated, 15 | AudioListenerWillBeDestroyed, 16 | AudioNode, 17 | AudioNodeCreated, 18 | AudioNodeWillBeDestroyed, 19 | AudioParam, 20 | AudioParamCreated, 21 | AudioParamWillBeDestroyed, 22 | AutomationRate, 23 | BaseAudioContext, 24 | ChannelCountMode, 25 | ChannelInterpretation, 26 | ContextChanged, 27 | ContextCreated, 28 | ContextRealtimeData, 29 | ContextState, 30 | ContextType, 31 | ContextWillBeDestroyed, 32 | GraphObjectId, 33 | NodeParamConnected, 34 | NodeParamDisconnected, 35 | NodeType, 36 | NodesConnected, 37 | NodesDisconnected, 38 | ParamType 39 | ) 40 | 41 | 42 | async def disable() -> None: 43 | r''' 44 | Disables the WebAudio domain. 45 | ''' 46 | session = get_session_context('web_audio.disable') 47 | return await session.execute(cdp.web_audio.disable()) 48 | 49 | 50 | async def enable() -> None: 51 | r''' 52 | Enables the WebAudio domain and starts sending context lifetime events. 53 | ''' 54 | session = get_session_context('web_audio.enable') 55 | return await session.execute(cdp.web_audio.enable()) 56 | 57 | 58 | async def get_realtime_data( 59 | context_id: GraphObjectId 60 | ) -> ContextRealtimeData: 61 | r''' 62 | Fetch the realtime data from the registered contexts. 63 | 64 | :param context_id: 65 | :returns: 66 | ''' 67 | session = get_session_context('web_audio.get_realtime_data') 68 | return await session.execute(cdp.web_audio.get_realtime_data(context_id)) 69 | -------------------------------------------------------------------------------- /trio_cdp/generated/performance.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.performance 12 | from cdp.performance import ( 13 | Metric, 14 | Metrics 15 | ) 16 | 17 | 18 | async def disable() -> None: 19 | r''' 20 | Disable collecting and reporting metrics. 21 | ''' 22 | session = get_session_context('performance.disable') 23 | return await session.execute(cdp.performance.disable()) 24 | 25 | 26 | async def enable( 27 | time_domain: typing.Optional[str] = None 28 | ) -> None: 29 | r''' 30 | Enable collecting and reporting metrics. 31 | 32 | :param time_domain: *(Optional)* Time domain to use for collecting and reporting duration metrics. 33 | ''' 34 | session = get_session_context('performance.enable') 35 | return await session.execute(cdp.performance.enable(time_domain)) 36 | 37 | 38 | async def get_metrics() -> typing.List[Metric]: 39 | r''' 40 | Retrieve current values of run-time metrics. 41 | 42 | :returns: Current values for run-time metrics. 43 | ''' 44 | session = get_session_context('performance.get_metrics') 45 | return await session.execute(cdp.performance.get_metrics()) 46 | 47 | 48 | async def set_time_domain( 49 | time_domain: str 50 | ) -> None: 51 | r''' 52 | Sets time domain to use for collecting and reporting duration metrics. 53 | Note that this must be called before enabling metrics collection. Calling 54 | this method while metrics collection is enabled returns an error. 55 | 56 | .. deprecated:: 1.3 57 | 58 | **EXPERIMENTAL** 59 | 60 | :param time_domain: Time domain 61 | 62 | .. deprecated:: 1.3 63 | ''' 64 | session = get_session_context('performance.set_time_domain') 65 | return await session.execute(cdp.performance.set_time_domain(time_domain)) 66 | -------------------------------------------------------------------------------- /trio_cdp/generated/background_service.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.background_service 12 | from cdp.background_service import ( 13 | BackgroundServiceEvent, 14 | BackgroundServiceEventReceived, 15 | EventMetadata, 16 | RecordingStateChanged, 17 | ServiceName 18 | ) 19 | 20 | 21 | async def clear_events( 22 | service: ServiceName 23 | ) -> None: 24 | r''' 25 | Clears all stored data for the service. 26 | 27 | :param service: 28 | ''' 29 | session = get_session_context('background_service.clear_events') 30 | return await session.execute(cdp.background_service.clear_events(service)) 31 | 32 | 33 | async def set_recording( 34 | should_record: bool, 35 | service: ServiceName 36 | ) -> None: 37 | r''' 38 | Set the recording state for the service. 39 | 40 | :param should_record: 41 | :param service: 42 | ''' 43 | session = get_session_context('background_service.set_recording') 44 | return await session.execute(cdp.background_service.set_recording(should_record, service)) 45 | 46 | 47 | async def start_observing( 48 | service: ServiceName 49 | ) -> None: 50 | r''' 51 | Enables event updates for the service. 52 | 53 | :param service: 54 | ''' 55 | session = get_session_context('background_service.start_observing') 56 | return await session.execute(cdp.background_service.start_observing(service)) 57 | 58 | 59 | async def stop_observing( 60 | service: ServiceName 61 | ) -> None: 62 | r''' 63 | Disables event updates for the service. 64 | 65 | :param service: 66 | ''' 67 | session = get_session_context('background_service.stop_observing') 68 | return await session.execute(cdp.background_service.stop_observing(service)) 69 | -------------------------------------------------------------------------------- /.github/workflows/auto-label-comment-prs.yml: -------------------------------------------------------------------------------- 1 | name: "Label PRs and auto-comment" 2 | uto-amazonq-review.properties.json 3 | on: 4 | uto-amazonq-review.properties.json 5 | pull_request: 6 | uto-amazonq-review.properties.json 7 | types: [opened, reopened, synchronize] 8 | uto-amazonq-review.properties.json 9 | jobs: 10 | uto-amazonq-review.properties.json 11 | pr_label_comment: 12 | uto-amazonq-review.properties.json 13 | runs-on: self-hosted 14 | uto-amazonq-review.properties.json 15 | steps: 16 | uto-amazonq-review.properties.json 17 | - uses: actions/github-script@main 18 | uto-amazonq-review.properties.json 19 | with: 20 | uto-amazonq-review.properties.json 21 | github-token: ${{ secrets.GITHUB_TOKEN }} 22 | uto-amazonq-review.properties.json 23 | script: | 24 | uto-amazonq-review.properties.json 25 | const pr_number = context.payload.pull_request.number; 26 | uto-amazonq-review.properties.json 27 | // Add label 28 | uto-amazonq-review.properties.json 29 | await github.rest.issues.addLabels({ 30 | uto-amazonq-review.properties.json 31 | owner: context.repo.owner, 32 | uto-amazonq-review.properties.json 33 | repo: context.repo.repo, 34 | uto-amazonq-review.properties.json 35 | issue_number: pr_number, 36 | uto-amazonq-review.properties.json 37 | labels: ["needs-review", "copilot"] // <-- TUNE ME 38 | uto-amazonq-review.properties.json 39 | }); 40 | uto-amazonq-review.properties.json 41 | // Add automated comment 42 | uto-amazonq-review.properties.json 43 | await github.rest.issues.createComment({ 44 | uto-amazonq-review.properties.json 45 | owner: context.repo.owner, 46 | uto-amazonq-review.properties.json 47 | repo: context.repo.repo, 48 | uto-amazonq-review.properties.json 49 | issue_number: pr_number, 50 | uto-amazonq-review.properties.json 51 | body: "Thanks for the PR! Copilot will assist with review." 52 | uto-amazonq-review.properties.json 53 | }); 54 | uto-amazonq-review.properties.json 55 | -------------------------------------------------------------------------------- /docs/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | PyPI 5 | ---- 6 | 7 | The library itself can be installed from PyPI: 8 | 9 | .. code:: 10 | 11 | $ pip install trio-chrome-devtools-protocol 12 | 13 | Browser 14 | ------- 15 | 16 | You also need a compatible browser. Here are instructions for getting the correct 17 | browser version and running the example scripts. 18 | 19 | MacOS 20 | ^^^^^ 21 | 22 | **Terminal 1** 23 | 24 | This sets up the chrome browser in a specific version, and runs it in debug mode with 25 | Tor proxy for network traffic. 26 | 27 | .. code:: 28 | 29 | $ wget https://www.googleapis.com/download/storage/v1/b/chromium-browser-snapshots/o/Mac%2F678035%2Fchrome-mac.zip?generation=1563322360871926&alt=media 30 | $ unzip chrome-mac.zip && rm chrome-mac.zip 31 | $ ./chrome-mac/Chromium.app/Contents/MacOS/Chromium --remote-debugging-port=9000 32 | > DevTools listening on ws://127.0.0.1:9000/devtools/browser/ 33 | 34 | When Chromium starts, it will display a random URL that it is listening on. Copy this 35 | URL for use in the next step. 36 | 37 | **Terminal 2** 38 | 39 | This runs the example browser automation script on the instantiated browser window. 40 | 41 | .. code:: 42 | 43 | $ python examples/get_title.py https://hyperiongray.com 44 | 45 | Linux 46 | ^^^^^ 47 | 48 | **Terminal 1** 49 | 50 | This sets up the chrome browser in a specific version, and runs it in debug mode with Tor proxy for network traffic. 51 | 52 | .. code:: 53 | 54 | $ wget https://storage.googleapis.com/chromium-browser-snapshots/Linux_x64/678025/chrome-linux.zip 55 | $ unzip chrome-linux.zip && rm chrome-linux.zip 56 | $ ./chrome-linux/chrome --remote-debugging-port=9000 57 | > DevTools listening on ws://127.0.0.1:9000/devtools/browser/ 58 | 59 | When Chromium starts, it will display a random URL that it is listening on. Copy this 60 | URL for use in the next step. 61 | 62 | **Terminal 2** 63 | 64 | This runs the example browser automation script on the instantiated browser window. 65 | 66 | .. code:: 67 | 68 | python examples/get_title.py https://hyperiongray.com 69 | 70 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # Configuration file for the Sphinx documentation builder. 2 | # 3 | # This file only contains a selection of the most common options. For a full 4 | # list see the documentation: 5 | # https://www.sphinx-doc.org/en/master/usage/configuration.html 6 | 7 | # -- Path setup -------------------------------------------------------------- 8 | 9 | # If extensions (or modules to document with autodoc) are in another directory, 10 | # add these directories to sys.path here. If the directory is relative to the 11 | # documentation root, use os.path.abspath to make it absolute, like shown here. 12 | # 13 | # import os 14 | # import sys 15 | # sys.path.insert(0, os.path.abspath('.')) 16 | 17 | 18 | # -- Project information ----------------------------------------------------- 19 | 20 | project = 'Trio CDP' 21 | copyright = '2020, Mark Haase, Brian Mackintosh' 22 | author = 'Mark Haase, Brian Mackintosh' 23 | master_doc = 'index' 24 | 25 | # -- General configuration --------------------------------------------------- 26 | 27 | # Add any Sphinx extension module names here, as strings. They can be 28 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 29 | # ones. 30 | extensions = [ 31 | 'sphinx.ext.autodoc', 32 | 'sphinx_autodoc_typehints', 33 | 'sphinx_rtd_theme', 34 | ] 35 | 36 | 37 | # Add any paths that contain templates here, relative to this directory. 38 | templates_path = ['_templates'] 39 | 40 | # List of patterns, relative to source directory, that match files and 41 | # directories to ignore when looking for source files. 42 | # This pattern also affects html_static_path and html_extra_path. 43 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 44 | 45 | 46 | # -- Options for HTML output ------------------------------------------------- 47 | 48 | # The theme to use for HTML and HTML Help pages. See the documentation for 49 | # a list of builtin themes. 50 | # 51 | html_theme = 'sphinx_rtd_theme' 52 | 53 | # Add any paths that contain custom static files (such as style sheets) here, 54 | # relative to this directory. They are copied after the builtin static files, 55 | # so a file named "default.css" will overwrite the builtin "default.css". 56 | html_static_path = ['_static'] 57 | -------------------------------------------------------------------------------- /trio_cdp/generated/io.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.io 12 | from cdp.io import ( 13 | StreamHandle 14 | ) 15 | 16 | 17 | async def close( 18 | handle: StreamHandle 19 | ) -> None: 20 | r''' 21 | Close the stream, discard any temporary backing storage. 22 | 23 | :param handle: Handle of the stream to close. 24 | ''' 25 | session = get_session_context('io.close') 26 | return await session.execute(cdp.io.close(handle)) 27 | 28 | 29 | async def read( 30 | handle: StreamHandle, 31 | offset: typing.Optional[int] = None, 32 | size: typing.Optional[int] = None 33 | ) -> typing.Tuple[typing.Optional[bool], str, bool]: 34 | r''' 35 | Read a chunk of the stream 36 | 37 | :param handle: Handle of the stream to read. 38 | :param offset: *(Optional)* Seek to the specified offset before reading (if not specificed, proceed with offset following the last read). Some types of streams may only support sequential reads. 39 | :param size: *(Optional)* Maximum number of bytes to read (left upon the agent discretion if not specified). 40 | :returns: A tuple with the following items: 41 | 42 | 0. **base64Encoded** - *(Optional)* Set if the data is base64-encoded 43 | 1. **data** - Data that were read. 44 | 2. **eof** - Set if the end-of-file condition occurred while reading. 45 | ''' 46 | session = get_session_context('io.read') 47 | return await session.execute(cdp.io.read(handle, offset, size)) 48 | 49 | 50 | async def resolve_blob( 51 | object_id: cdp.runtime.RemoteObjectId 52 | ) -> str: 53 | r''' 54 | Return UUID of Blob object specified by a remote object id. 55 | 56 | :param object_id: Object id of a Blob object wrapper. 57 | :returns: UUID of the specified Blob. 58 | ''' 59 | session = get_session_context('io.resolve_blob') 60 | return await session.execute(cdp.io.resolve_blob(object_id)) 61 | -------------------------------------------------------------------------------- /examples/get_title.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Get the title of a target web page. 3 | 4 | To use this example, start Chrome (or any other browser that supports CDP) with 5 | the option `--remote-debugging-port=9000`. The URL that Chrome is listening on 6 | is displayed in the terminal after Chrome starts up. 7 | 8 | Then run this script with the Chrome URL as the first argument and the target 9 | website URL as the second argument: 10 | 11 | $ python examples/get_title.py \ 12 | ws://localhost:9000/devtools/browser/facfb2295-... \ 13 | https://www.hyperiongray.com 14 | ''' 15 | import logging 16 | import os 17 | import sys 18 | 19 | import trio 20 | from trio_cdp import open_cdp, dom, page, target 21 | 22 | 23 | log_level = os.environ.get('LOG_LEVEL', 'info').upper() 24 | logging.basicConfig(level=getattr(logging, log_level)) 25 | logger = logging.getLogger('get_title') 26 | logging.getLogger('trio-websocket').setLevel(logging.WARNING) 27 | 28 | 29 | async def main(): 30 | logger.info('Connecting to browser: %s', sys.argv[1]) 31 | async with open_cdp(sys.argv[1]) as conn: 32 | logger.info('Listing targets') 33 | targets = await target.get_targets() 34 | 35 | for t in targets: 36 | if (t.type == 'page' and 37 | not t.url.startswith('devtools://') and 38 | not t.attached): 39 | target_id = t.target_id 40 | break 41 | 42 | logger.info('Attaching to target id=%s', target_id) 43 | async with conn.open_session(target_id) as session: 44 | 45 | logger.info('Navigating to %s', sys.argv[2]) 46 | await page.enable() 47 | async with session.wait_for(page.LoadEventFired): 48 | await page.navigate(sys.argv[2]) 49 | 50 | logger.info('Extracting page title') 51 | root_node = await dom.get_document() 52 | title_node_id = await dom.query_selector(root_node.node_id, 'title') 53 | html = await dom.get_outer_html(title_node_id) 54 | print(html) 55 | 56 | 57 | if __name__ == '__main__': 58 | if len(sys.argv) != 3: 59 | sys.stderr.write('Usage: get_title.py ') 60 | sys.exit(1) 61 | trio.run(main, restrict_keyboard_interrupt_to_checkpoints=True) 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Trio CDP 2 | 3 | [![PyPI](https://img.shields.io/pypi/v/trio-chrome-devtools-protocol.svg)](https://pypi.org/project/trio-chrome-devtools-protocol/) 4 | ![Python Versions](https://img.shields.io/pypi/pyversions/trio-chrome-devtools-protocol) 5 | ![MIT License](https://img.shields.io/github/license/HyperionGray/trio-chrome-devtools-protocol.svg) 6 | [![Build Status](https://img.shields.io/travis/com/HyperionGray/trio-chrome-devtools-protocol.svg?branch=master)](https://travis-ci.com/HyperionGray/trio-chrome-devtools-protocol) 7 | [![Read the Docs](https://img.shields.io/readthedocs/trio-cdp.svg)](https://trio-cdp.readthedocs.io) 8 | 9 | This Python library performs remote control of any web browser that implements 10 | the Chrome DevTools Protocol. It is built using the type wrappers in 11 | [python-chrome-devtools-protocol](https://py-cdp.readthedocs.io) and implements 12 | I/O using [Trio](https://trio.readthedocs.io/). This library handles the 13 | WebSocket negotiation and session management, allowing you to transparently 14 | multiplex commands, responses, and events over a single connection. 15 | 16 | The example below demonstrates the salient features of the library by navigating to a 17 | web page and extracting the document title. 18 | 19 | ```python 20 | from trio_cdp import open_cdp, page, dom 21 | 22 | async with open_cdp(cdp_url) as conn: 23 | # Find the first available target (usually a browser tab). 24 | targets = await target.get_targets() 25 | target_id = targets[0].id 26 | 27 | # Create a new session with the chosen target. 28 | async with conn.open_session(target_id) as session: 29 | 30 | # Navigate to a website. 31 | async with session.page_enable() 32 | async with session.wait_for(page.LoadEventFired): 33 | await session.execute(page.navigate(target_url)) 34 | 35 | # Extract the page title. 36 | root_node = await session.execute(dom.get_document()) 37 | title_node_id = await session.execute(dom.query_selector(root_node.node_id, 38 | 'title')) 39 | html = await session.execute(dom.get_outer_html(title_node_id)) 40 | print(html) 41 | ``` 42 | 43 | This example code is explained [in the documentation](https://trio-cdp.readthedocs.io) 44 | and more example code can be found in the `examples/` directory of this repository. 45 | -------------------------------------------------------------------------------- /trio_cdp/generated/dom_storage.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.dom_storage 12 | from cdp.dom_storage import ( 13 | DomStorageItemAdded, 14 | DomStorageItemRemoved, 15 | DomStorageItemUpdated, 16 | DomStorageItemsCleared, 17 | Item, 18 | StorageId 19 | ) 20 | 21 | 22 | async def clear( 23 | storage_id: StorageId 24 | ) -> None: 25 | r''' 26 | :param storage_id: 27 | ''' 28 | session = get_session_context('dom_storage.clear') 29 | return await session.execute(cdp.dom_storage.clear(storage_id)) 30 | 31 | 32 | async def disable() -> None: 33 | r''' 34 | Disables storage tracking, prevents storage events from being sent to the client. 35 | ''' 36 | session = get_session_context('dom_storage.disable') 37 | return await session.execute(cdp.dom_storage.disable()) 38 | 39 | 40 | async def enable() -> None: 41 | r''' 42 | Enables storage tracking, storage events will now be delivered to the client. 43 | ''' 44 | session = get_session_context('dom_storage.enable') 45 | return await session.execute(cdp.dom_storage.enable()) 46 | 47 | 48 | async def get_dom_storage_items( 49 | storage_id: StorageId 50 | ) -> typing.List[Item]: 51 | r''' 52 | :param storage_id: 53 | :returns: 54 | ''' 55 | session = get_session_context('dom_storage.get_dom_storage_items') 56 | return await session.execute(cdp.dom_storage.get_dom_storage_items(storage_id)) 57 | 58 | 59 | async def remove_dom_storage_item( 60 | storage_id: StorageId, 61 | key: str 62 | ) -> None: 63 | r''' 64 | :param storage_id: 65 | :param key: 66 | ''' 67 | session = get_session_context('dom_storage.remove_dom_storage_item') 68 | return await session.execute(cdp.dom_storage.remove_dom_storage_item(storage_id, key)) 69 | 70 | 71 | async def set_dom_storage_item( 72 | storage_id: StorageId, 73 | key: str, 74 | value: str 75 | ) -> None: 76 | r''' 77 | :param storage_id: 78 | :param key: 79 | :param value: 80 | ''' 81 | session = get_session_context('dom_storage.set_dom_storage_item') 82 | return await session.execute(cdp.dom_storage.set_dom_storage_item(storage_id, key, value)) 83 | -------------------------------------------------------------------------------- /trio_cdp/context.py: -------------------------------------------------------------------------------- 1 | from contextlib import contextmanager 2 | import contextvars 3 | 4 | 5 | _connection_context: contextvars.ContextVar = contextvars.ContextVar('connection_context') 6 | _session_context: contextvars.ContextVar = contextvars.ContextVar('session_context') 7 | 8 | 9 | 10 | 11 | def get_connection_context(fn_name): 12 | ''' 13 | Look up the current connection. If there is no current connection, raise a 14 | ``RuntimeError`` with a helpful message. 15 | ''' 16 | try: 17 | return _connection_context.get() 18 | except LookupError: 19 | raise RuntimeError(f'{fn_name}() must be called in a connection context.') 20 | 21 | 22 | def get_session_context(fn_name): 23 | ''' 24 | Look up the current session. If there is no current session, raise a 25 | ``RuntimeError`` with a helpful message. 26 | ''' 27 | try: 28 | return _session_context.get() 29 | except LookupError: 30 | raise RuntimeError(f'{fn_name}() must be called in a session context.') 31 | 32 | 33 | @contextmanager 34 | def connection_context(connection): 35 | ''' This context manager installs ``connection`` as the session context for the current 36 | Trio task. ''' 37 | token = _connection_context.set(connection) 38 | try: 39 | yield 40 | finally: 41 | _connection_context.reset(token) 42 | 43 | 44 | @contextmanager 45 | def session_context(session): 46 | ''' This context manager installs ``session`` as the session context for the current 47 | Trio task. ''' 48 | token = _session_context.set(session) 49 | try: 50 | yield 51 | finally: 52 | _session_context.reset(token) 53 | 54 | 55 | def set_global_connection(connection): 56 | ''' 57 | Install ``connection`` in the root context so that it will become the default 58 | connection for all tasks. This is generally not recommended, except it may be 59 | necessary in certain use cases such as running inside Jupyter notebook. 60 | ''' 61 | global _connection_context 62 | _connection_context = contextvars.ContextVar('_connection_context', 63 | default=connection) 64 | 65 | 66 | def set_global_session(session): 67 | ''' 68 | Install ``session`` in the root context so that it will become the default 69 | session for all tasks. This is generally not recommended, except it may be 70 | necessary in certain use cases such as running inside Jupyter notebook. 71 | ''' 72 | global _session_context 73 | _session_context = contextvars.ContextVar('_session_context', default=session) 74 | -------------------------------------------------------------------------------- /examples/screenshot.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Make a screenshot of a target web page. 3 | 4 | To use this example, start Chrome (or any other browser that supports CDP) with 5 | the option `--remote-debugging-port=9000`. The URL that Chrome is listening on 6 | is displayed in the terminal after Chrome starts up. 7 | 8 | Then run this script with the Chrome URL as the first argument and the target 9 | website URL as the second argument: 10 | 11 | $ python examples/screenshot.py \ 12 | ws://localhost:9000/devtools/browser/facfb2295-... \ 13 | https://www.hyperiongray.com 14 | ''' 15 | from base64 import b64decode 16 | import logging 17 | import os 18 | import sys 19 | 20 | import trio 21 | from trio_cdp import open_cdp, emulation, page, target 22 | 23 | 24 | log_level = os.environ.get('LOG_LEVEL', 'info').upper() 25 | logging.basicConfig(level=getattr(logging, log_level)) 26 | logger = logging.getLogger('screenshot') 27 | logging.getLogger('trio-websocket').setLevel(logging.WARNING) 28 | 29 | 30 | async def main(): 31 | logger.info('Connecting to browser: %s', sys.argv[1]) 32 | async with open_cdp(sys.argv[1]) as conn: 33 | logger.info('Listing targets') 34 | targets = await target.get_targets() 35 | 36 | for t in targets: 37 | if (t.type == 'page' and 38 | not t.url.startswith('devtools://') and 39 | not t.attached): 40 | target_id = t.target_id 41 | break 42 | 43 | logger.info('Attaching to target id=%s', target_id) 44 | async with conn.open_session(target_id) as session: 45 | 46 | logger.info('Setting device emulation') 47 | await emulation.set_device_metrics_override( 48 | width=800, height=600, device_scale_factor=1, mobile=False 49 | ) 50 | 51 | logger.info('Enabling page events') 52 | await page.enable() 53 | 54 | logger.info('Navigating to %s', sys.argv[2]) 55 | async with session.wait_for(page.LoadEventFired): 56 | await page.navigate(url=sys.argv[2]) 57 | 58 | logger.info('Making a screenshot') 59 | img_data = await page.capture_screenshot(format='png') 60 | logger.info('Saving to file') 61 | screenshot_file = await trio.open_file('test.png', 'wb') 62 | async with screenshot_file: 63 | await screenshot_file.write(b64decode(img_data)) 64 | 65 | 66 | if __name__ == '__main__': 67 | if len(sys.argv) != 3: 68 | sys.stderr.write('Usage: screenshot.py ') 69 | sys.exit(1) 70 | trio.run(main, restrict_keyboard_interrupt_to_checkpoints=True) 71 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-pr.yml: -------------------------------------------------------------------------------- 1 | # Auto Assign Copilot (or any username) to every new pull request. 2 | uto-amazonq-review.properties.json 3 | # Tweak the username(s) below as needed! 4 | uto-amazonq-review.properties.json 5 | 6 | uto-amazonq-review.properties.json 7 | name: Auto Assign Copilot to PRs 8 | uto-amazonq-review.properties.json 9 | 10 | uto-amazonq-review.properties.json 11 | on: 12 | uto-amazonq-review.properties.json 13 | pull_request: 14 | uto-amazonq-review.properties.json 15 | types: [opened] 16 | uto-amazonq-review.properties.json 17 | 18 | uto-amazonq-review.properties.json 19 | jobs: 20 | uto-amazonq-review.properties.json 21 | auto-assign: 22 | uto-amazonq-review.properties.json 23 | runs-on: self-hosted 24 | uto-amazonq-review.properties.json 25 | steps: 26 | uto-amazonq-review.properties.json 27 | - name: Assign Copilot (or others) to new PRs 28 | uto-amazonq-review.properties.json 29 | uses: actions/github-script@main 30 | uto-amazonq-review.properties.json 31 | with: 32 | uto-amazonq-review.properties.json 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | uto-amazonq-review.properties.json 35 | script: | 36 | uto-amazonq-review.properties.json 37 | // Assign PRs to Copilot or other users 38 | uto-amazonq-review.properties.json 39 | const copilotUsername = "copilot"; // <-- TUNE ME! 40 | uto-amazonq-review.properties.json 41 | const assignees = [copilotUsername]; // Or: ["copilot","anotheruser"] 42 | uto-amazonq-review.properties.json 43 | const currentAssignees = context.payload.pull_request.assignees.map(u => u.login); 44 | uto-amazonq-review.properties.json 45 | if (!assignees.every(a => currentAssignees.includes(a))) { 46 | uto-amazonq-review.properties.json 47 | await github.rest.issues.addAssignees({ 48 | uto-amazonq-review.properties.json 49 | owner: context.repo.owner, 50 | uto-amazonq-review.properties.json 51 | repo: context.repo.repo, 52 | uto-amazonq-review.properties.json 53 | issue_number: context.payload.pull_request.number, 54 | uto-amazonq-review.properties.json 55 | assignees 56 | uto-amazonq-review.properties.json 57 | }); 58 | uto-amazonq-review.properties.json 59 | console.log(`Assigned ${assignees.join(", ")} to PR #${context.payload.pull_request.number}`); 60 | uto-amazonq-review.properties.json 61 | } else { 62 | uto-amazonq-review.properties.json 63 | console.log(`Already assigned: ${assignees.join(", ")} on PR #${context.payload.pull_request.number}`); 64 | uto-amazonq-review.properties.json 65 | } 66 | uto-amazonq-review.properties.json 67 | -------------------------------------------------------------------------------- /trio_cdp/generated/cast.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.cast 12 | from cdp.cast import ( 13 | IssueUpdated, 14 | Sink, 15 | SinksUpdated 16 | ) 17 | 18 | 19 | async def disable() -> None: 20 | r''' 21 | Stops observing for sinks and issues. 22 | ''' 23 | session = get_session_context('cast.disable') 24 | return await session.execute(cdp.cast.disable()) 25 | 26 | 27 | async def enable( 28 | presentation_url: typing.Optional[str] = None 29 | ) -> None: 30 | r''' 31 | Starts observing for sinks that can be used for tab mirroring, and if set, 32 | sinks compatible with ``presentationUrl`` as well. When sinks are found, a 33 | ``sinksUpdated`` event is fired. 34 | Also starts observing for issue messages. When an issue is added or removed, 35 | an ``issueUpdated`` event is fired. 36 | 37 | :param presentation_url: *(Optional)* 38 | ''' 39 | session = get_session_context('cast.enable') 40 | return await session.execute(cdp.cast.enable(presentation_url)) 41 | 42 | 43 | async def set_sink_to_use( 44 | sink_name: str 45 | ) -> None: 46 | r''' 47 | Sets a sink to be used when the web page requests the browser to choose a 48 | sink via Presentation API, Remote Playback API, or Cast SDK. 49 | 50 | :param sink_name: 51 | ''' 52 | session = get_session_context('cast.set_sink_to_use') 53 | return await session.execute(cdp.cast.set_sink_to_use(sink_name)) 54 | 55 | 56 | async def start_desktop_mirroring( 57 | sink_name: str 58 | ) -> None: 59 | r''' 60 | Starts mirroring the desktop to the sink. 61 | 62 | :param sink_name: 63 | ''' 64 | session = get_session_context('cast.start_desktop_mirroring') 65 | return await session.execute(cdp.cast.start_desktop_mirroring(sink_name)) 66 | 67 | 68 | async def start_tab_mirroring( 69 | sink_name: str 70 | ) -> None: 71 | r''' 72 | Starts mirroring the tab to the sink. 73 | 74 | :param sink_name: 75 | ''' 76 | session = get_session_context('cast.start_tab_mirroring') 77 | return await session.execute(cdp.cast.start_tab_mirroring(sink_name)) 78 | 79 | 80 | async def stop_casting( 81 | sink_name: str 82 | ) -> None: 83 | r''' 84 | Stops the active Cast session on the sink. 85 | 86 | :param sink_name: 87 | ''' 88 | session = get_session_context('cast.stop_casting') 89 | return await session.execute(cdp.cast.stop_casting(sink_name)) 90 | -------------------------------------------------------------------------------- /trio_cdp/generated/security.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.security 12 | from cdp.security import ( 13 | CertificateError, 14 | CertificateErrorAction, 15 | CertificateId, 16 | CertificateSecurityState, 17 | InsecureContentStatus, 18 | MixedContentType, 19 | SafetyTipInfo, 20 | SafetyTipStatus, 21 | SecurityState, 22 | SecurityStateChanged, 23 | SecurityStateExplanation, 24 | VisibleSecurityState, 25 | VisibleSecurityStateChanged 26 | ) 27 | 28 | 29 | async def disable() -> None: 30 | r''' 31 | Disables tracking security state changes. 32 | ''' 33 | session = get_session_context('security.disable') 34 | return await session.execute(cdp.security.disable()) 35 | 36 | 37 | async def enable() -> None: 38 | r''' 39 | Enables tracking security state changes. 40 | ''' 41 | session = get_session_context('security.enable') 42 | return await session.execute(cdp.security.enable()) 43 | 44 | 45 | async def handle_certificate_error( 46 | event_id: int, 47 | action: CertificateErrorAction 48 | ) -> None: 49 | r''' 50 | Handles a certificate error that fired a certificateError event. 51 | 52 | .. deprecated:: 1.3 53 | 54 | :param event_id: The ID of the event. 55 | :param action: The action to take on the certificate error. 56 | 57 | .. deprecated:: 1.3 58 | ''' 59 | session = get_session_context('security.handle_certificate_error') 60 | return await session.execute(cdp.security.handle_certificate_error(event_id, action)) 61 | 62 | 63 | async def set_ignore_certificate_errors( 64 | ignore: bool 65 | ) -> None: 66 | r''' 67 | Enable/disable whether all certificate errors should be ignored. 68 | 69 | **EXPERIMENTAL** 70 | 71 | :param ignore: If true, all certificate errors will be ignored. 72 | ''' 73 | session = get_session_context('security.set_ignore_certificate_errors') 74 | return await session.execute(cdp.security.set_ignore_certificate_errors(ignore)) 75 | 76 | 77 | async def set_override_certificate_errors( 78 | override: bool 79 | ) -> None: 80 | r''' 81 | Enable/disable overriding certificate errors. If enabled, all certificate error events need to 82 | be handled by the DevTools client and should be answered with ``handleCertificateError`` commands. 83 | 84 | .. deprecated:: 1.3 85 | 86 | :param override: If true, certificate errors will be overridden. 87 | 88 | .. deprecated:: 1.3 89 | ''' 90 | session = get_session_context('security.set_override_certificate_errors') 91 | return await session.execute(cdp.security.set_override_certificate_errors(override)) 92 | -------------------------------------------------------------------------------- /generator/test_generate.py: -------------------------------------------------------------------------------- 1 | from textwrap import dedent 2 | 3 | import cdp 4 | 5 | from .generate import generate_command 6 | 7 | 8 | def test_dom_query_selector(): 9 | 10 | expected = dedent("""\ 11 | async def query_selector( 12 | node_id: NodeId, 13 | selector: str 14 | ) -> NodeId: 15 | r''' 16 | Executes ``querySelector`` on a given node. 17 | 18 | :param node_id: Id of the node to query upon. 19 | :param selector: Selector string. 20 | :returns: Query selector result. 21 | ''' 22 | session = get_session_context('dom.query_selector') 23 | return await session.execute(cdp.dom.query_selector(node_id, selector)) 24 | """) 25 | 26 | assert expected == generate_command(cdp.dom, 'dom', cdp.dom.query_selector) 27 | 28 | 29 | def test_accessibility_disable(): 30 | expected = dedent("""\ 31 | async def disable() -> None: 32 | r''' 33 | Disables the accessibility domain. 34 | ''' 35 | session = get_session_context('accessibility.disable') 36 | return await session.execute(cdp.accessibility.disable()) 37 | """) 38 | 39 | assert expected == generate_command(cdp.accessibility, 'accessibility', 40 | cdp.accessibility.disable) 41 | 42 | 43 | def test_accessibility_get_partial_ax_tree(): 44 | expected = dedent("""\ 45 | async def get_partial_ax_tree( 46 | node_id: typing.Optional[cdp.dom.NodeId] = None, 47 | backend_node_id: typing.Optional[cdp.dom.BackendNodeId] = None, 48 | object_id: typing.Optional[cdp.runtime.RemoteObjectId] = None, 49 | fetch_relatives: typing.Optional[bool] = None 50 | ) -> typing.List[AXNode]: 51 | r''' 52 | Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists. 53 | 54 | **EXPERIMENTAL** 55 | 56 | :param node_id: *(Optional)* Identifier of the node to get the partial accessibility tree for. 57 | :param backend_node_id: *(Optional)* Identifier of the backend node to get the partial accessibility tree for. 58 | :param object_id: *(Optional)* JavaScript object id of the node wrapper to get the partial accessibility tree for. 59 | :param fetch_relatives: *(Optional)* Whether to fetch this nodes ancestors, siblings and children. Defaults to true. 60 | :returns: The ``Accessibility.AXNode`` for this DOM node, if it exists, plus its ancestors, siblings and children, if requested. 61 | ''' 62 | session = get_session_context('accessibility.get_partial_ax_tree') 63 | return await session.execute(cdp.accessibility.get_partial_ax_tree(node_id, backend_node_id, object_id, fetch_relatives)) 64 | """) 65 | 66 | assert expected == generate_command(cdp.accessibility, 'accessibility', 67 | cdp.accessibility.get_partial_ax_tree) 68 | -------------------------------------------------------------------------------- /examples/take_heap_snapshot.py: -------------------------------------------------------------------------------- 1 | ''' 2 | Take a heap snapshot. 3 | 4 | To use this example, start Chrome (or any other browser that supports CDP) with 5 | the option `--remote-debugging-port=9000`. The URL that Chrome is listening on 6 | is displayed in the terminal after Chrome starts up. 7 | 8 | Then run this script with the Chrome URL as the first argument 9 | 10 | $ python examples/take_heap_snapshot.py \ 11 | ws://localhost:9000/devtools/browser/facfb2295-... 12 | ''' 13 | from datetime import datetime 14 | import logging 15 | import os 16 | import sys 17 | 18 | import trio 19 | from trio_cdp import open_cdp, browser, dom, heap_profiler, page, target 20 | 21 | 22 | log_level = os.environ.get('LOG_LEVEL', 'info').upper() 23 | logging.basicConfig(level=getattr(logging, log_level)) 24 | logger = logging.getLogger('monitor') 25 | logging.getLogger('trio-websocket').setLevel(logging.WARNING) 26 | 27 | 28 | # Profiler starts taking snapshot immediately when the command 29 | # arrives (as before) and if `reportProgress` param is `true` it 30 | # will send `HeapProfiler.reportHeapSnapshotProgress` events. 31 | # After that a series of snapshot chunks is sent to the frontend 32 | # as `HeapProfiler.addHeapSnapshotChunk` events. *When whole 33 | # snapshot is sent the backend will sent response to 34 | # `HeapProfiler.takeHeapSnapshot` command.* 35 | # ([ref](https://codereview.chromium.org/98273008)) 36 | 37 | async def _take_heap_snapshot(session, outfile, report_progress=False): 38 | async def chunk_helper(): 39 | async for event in session.listen(heap_profiler.AddHeapSnapshotChunk): 40 | await outfile.write(event.chunk) 41 | async def progress_helper(): 42 | async for event in session.listen(heap_profiler.ReportHeapSnapshotProgress): 43 | logger.info('Heap snapshot: {} ({:0.1f}%) {}'.format(event.done, event.done*100 / event.total, 'finished' if event.finished else '')) 44 | async with trio.open_nursery() as nursery: 45 | nursery.start_soon(chunk_helper) 46 | if report_progress: 47 | nursery.start_soon(progress_helper) 48 | await heap_profiler.take_heap_snapshot(report_progress) 49 | nursery.cancel_scope.cancel() 50 | 51 | 52 | async def main(): 53 | cdp_uri = sys.argv[1] 54 | async with open_cdp(cdp_uri) as conn: 55 | logger.info('Connecting') 56 | targets = await target.get_targets() 57 | target_id = targets[0].target_id 58 | 59 | # First page 60 | logger.info('Attaching to target id=%s', target_id) 61 | async with conn.open_session(target_id) as session: 62 | 63 | logger.info('Started heap snapshot') 64 | outfile_path = trio.Path('%s.heapsnapshot' % datetime.today().isoformat()) 65 | async with await outfile_path.open('a') as outfile: 66 | logger.info('Started writing heap snapshot') 67 | await _take_heap_snapshot(session, outfile, report_progress=True) 68 | 69 | 70 | if __name__ == '__main__': 71 | if len(sys.argv) != 2: 72 | sys.stderr.write('Usage: take_heap_snapshot.py ') 73 | sys.exit(1) 74 | trio.run(main, restrict_keyboard_interrupt_to_checkpoints=True) 75 | -------------------------------------------------------------------------------- /.github/workflows/auto-assign-copilot.yml: -------------------------------------------------------------------------------- 1 | name: Auto Assign Copilot to Issues 2 | uto-amazonq-review.properties.json 3 | 4 | uto-amazonq-review.properties.json 5 | on: 6 | uto-amazonq-review.properties.json 7 | issues: 8 | uto-amazonq-review.properties.json 9 | types: 10 | uto-amazonq-review.properties.json 11 | - opened 12 | uto-amazonq-review.properties.json 13 | - labeled 14 | uto-amazonq-review.properties.json 15 | 16 | uto-amazonq-review.properties.json 17 | jobs: 18 | uto-amazonq-review.properties.json 19 | auto-assign: 20 | uto-amazonq-review.properties.json 21 | runs-on: self-hosted 22 | uto-amazonq-review.properties.json 23 | if: contains(github.event.issue.labels.*.name, 'copilot') 24 | uto-amazonq-review.properties.json 25 | steps: 26 | uto-amazonq-review.properties.json 27 | - name: Assign Copilot to new issues 28 | uto-amazonq-review.properties.json 29 | uses: actions/github-script@main 30 | uto-amazonq-review.properties.json 31 | with: 32 | uto-amazonq-review.properties.json 33 | github-token: ${{ secrets.GITHUB_TOKEN }} 34 | uto-amazonq-review.properties.json 35 | script: | 36 | uto-amazonq-review.properties.json 37 | const copilotUsername = "copilot"; 38 | uto-amazonq-review.properties.json 39 | 40 | uto-amazonq-review.properties.json 41 | // Check if issue is already assigned to copilot 42 | uto-amazonq-review.properties.json 43 | const currentAssignees = context.payload.issue.assignees.map(u => u.login); 44 | uto-amazonq-review.properties.json 45 | 46 | uto-amazonq-review.properties.json 47 | if (!currentAssignees.includes(copilotUsername)) { 48 | uto-amazonq-review.properties.json 49 | console.log(`Issue has 'copilot' label. Assigning @${copilotUsername}...`); 50 | uto-amazonq-review.properties.json 51 | 52 | uto-amazonq-review.properties.json 53 | try { 54 | uto-amazonq-review.properties.json 55 | await github.rest.issues.addAssignees({ 56 | uto-amazonq-review.properties.json 57 | owner: context.repo.owner, 58 | uto-amazonq-review.properties.json 59 | repo: context.repo.repo, 60 | uto-amazonq-review.properties.json 61 | issue_number: context.issue.number, 62 | uto-amazonq-review.properties.json 63 | assignees: [copilotUsername] 64 | uto-amazonq-review.properties.json 65 | }); 66 | uto-amazonq-review.properties.json 67 | console.log(`✅ Assigned @${copilotUsername} to issue #${context.issue.number}`); 68 | uto-amazonq-review.properties.json 69 | } catch (error) { 70 | uto-amazonq-review.properties.json 71 | console.log(`⚠️ Failed to assign Copilot: ${error.message}`); 72 | uto-amazonq-review.properties.json 73 | console.log("Note: You must have a Copilot seat assigned to your account/org for this to work."); 74 | uto-amazonq-review.properties.json 75 | } 76 | uto-amazonq-review.properties.json 77 | } else { 78 | uto-amazonq-review.properties.json 79 | console.log(`ℹ️ @${copilotUsername} is already assigned to issue #${context.issue.number}`); 80 | uto-amazonq-review.properties.json 81 | } 82 | uto-amazonq-review.properties.json 83 | -------------------------------------------------------------------------------- /trio_cdp/generated/headless_experimental.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.headless_experimental 12 | from cdp.headless_experimental import ( 13 | NeedsBeginFramesChanged, 14 | ScreenshotParams 15 | ) 16 | 17 | 18 | async def begin_frame( 19 | frame_time_ticks: typing.Optional[float] = None, 20 | interval: typing.Optional[float] = None, 21 | no_display_updates: typing.Optional[bool] = None, 22 | screenshot: typing.Optional[ScreenshotParams] = None 23 | ) -> typing.Tuple[bool, typing.Optional[str]]: 24 | r''' 25 | Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a 26 | screenshot from the resulting frame. Requires that the target was created with enabled 27 | BeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also 28 | https://goo.gl/3zHXhB for more background. 29 | 30 | :param frame_time_ticks: *(Optional)* Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set, the current time will be used. 31 | :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. 32 | :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. 33 | :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. 34 | :returns: A tuple with the following items: 35 | 36 | 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. 37 | 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) 38 | ''' 39 | session = get_session_context('headless_experimental.begin_frame') 40 | return await session.execute(cdp.headless_experimental.begin_frame(frame_time_ticks, interval, no_display_updates, screenshot)) 41 | 42 | 43 | async def disable() -> None: 44 | r''' 45 | Disables headless events for the target. 46 | ''' 47 | session = get_session_context('headless_experimental.disable') 48 | return await session.execute(cdp.headless_experimental.disable()) 49 | 50 | 51 | async def enable() -> None: 52 | r''' 53 | Enables headless events for the target. 54 | ''' 55 | session = get_session_context('headless_experimental.enable') 56 | return await session.execute(cdp.headless_experimental.enable()) 57 | -------------------------------------------------------------------------------- /trio_cdp/generated/cache_storage.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.cache_storage 12 | from cdp.cache_storage import ( 13 | Cache, 14 | CacheId, 15 | CachedResponse, 16 | CachedResponseType, 17 | DataEntry, 18 | Header 19 | ) 20 | 21 | 22 | async def delete_cache( 23 | cache_id: CacheId 24 | ) -> None: 25 | r''' 26 | Deletes a cache. 27 | 28 | :param cache_id: Id of cache for deletion. 29 | ''' 30 | session = get_session_context('cache_storage.delete_cache') 31 | return await session.execute(cdp.cache_storage.delete_cache(cache_id)) 32 | 33 | 34 | async def delete_entry( 35 | cache_id: CacheId, 36 | request: str 37 | ) -> None: 38 | r''' 39 | Deletes a cache entry. 40 | 41 | :param cache_id: Id of cache where the entry will be deleted. 42 | :param request: URL spec of the request. 43 | ''' 44 | session = get_session_context('cache_storage.delete_entry') 45 | return await session.execute(cdp.cache_storage.delete_entry(cache_id, request)) 46 | 47 | 48 | async def request_cache_names( 49 | security_origin: str 50 | ) -> typing.List[Cache]: 51 | r''' 52 | Requests cache names. 53 | 54 | :param security_origin: Security origin. 55 | :returns: Caches for the security origin. 56 | ''' 57 | session = get_session_context('cache_storage.request_cache_names') 58 | return await session.execute(cdp.cache_storage.request_cache_names(security_origin)) 59 | 60 | 61 | async def request_cached_response( 62 | cache_id: CacheId, 63 | request_url: str, 64 | request_headers: typing.List[Header] 65 | ) -> CachedResponse: 66 | r''' 67 | Fetches cache entry. 68 | 69 | :param cache_id: Id of cache that contains the entry. 70 | :param request_url: URL spec of the request. 71 | :param request_headers: headers of the request. 72 | :returns: Response read from the cache. 73 | ''' 74 | session = get_session_context('cache_storage.request_cached_response') 75 | return await session.execute(cdp.cache_storage.request_cached_response(cache_id, request_url, request_headers)) 76 | 77 | 78 | async def request_entries( 79 | cache_id: CacheId, 80 | skip_count: typing.Optional[int] = None, 81 | page_size: typing.Optional[int] = None, 82 | path_filter: typing.Optional[str] = None 83 | ) -> typing.Tuple[typing.List[DataEntry], float]: 84 | r''' 85 | Requests data from cache. 86 | 87 | :param cache_id: ID of cache to get entries from. 88 | :param skip_count: *(Optional)* Number of records to skip. 89 | :param page_size: *(Optional)* Number of records to fetch. 90 | :param path_filter: *(Optional)* If present, only return the entries containing this substring in the path 91 | :returns: A tuple with the following items: 92 | 93 | 0. **cacheDataEntries** - Array of object store data entries. 94 | 1. **returnCount** - Count of returned entries from this storage. If pathFilter is empty, it is the count of all entries from this storage. 95 | ''' 96 | session = get_session_context('cache_storage.request_entries') 97 | return await session.execute(cdp.cache_storage.request_entries(cache_id, skip_count, page_size, path_filter)) 98 | -------------------------------------------------------------------------------- /docs/api.rst: -------------------------------------------------------------------------------- 1 | API 2 | === 3 | 4 | Trio CDP replicates the entire API of PyCDP. For example, PyCDP has a ``cdp.dom`` module, 5 | while in Trio CDP, we have ``trio_cdp.dom``. The Trio CDP version has all of the same data 6 | types, commands, and events as the PyCDP version. 7 | 8 | The only difference between the two libraries is that the Trio CDP commands are ``async 9 | def`` functions that can be called from Trio, whereas the PyCDP commands are generators 10 | that must be executed in a special way. This document explains both flavors and how to 11 | use them. 12 | 13 | You should consult the `PyCDP documentation 14 | `_ for a complete reference of the 15 | data types, commands, and events that are available. 16 | 17 | Simplified API 18 | -------------- 19 | 20 | .. highlight: python 21 | 22 | The simplified API allows you to await CDP commands directly. For example, to run a CSS 23 | query to get a blockquote element, the code is: 24 | 25 | .. code:: 26 | 27 | from trio_cdp import dom 28 | 29 | ...other code... 30 | 31 | node_id = await dom.query(root, 'blockquote') 32 | 33 | As you can see in `the PyCDP documentation 34 | `_, the 35 | command takes a ``NodeId`` as its first argument and a string as its second argument. 36 | The arguments in Trio CDP are always the same as in PyCDP. 37 | 38 | The return types in Trio CDP are different from PyCDP, however. The PyCDP command is a 39 | generator, where as the Trio CDP command is an ``async def``. The PyCDP command 40 | signature shows ``Generator[Dict[str, Any], Dict[str, Any], NodeId]]``. The generator 41 | contains 3 parts. The first two parts are always the same: ``Dict[str, Any]``. The third 42 | part indicates the real return type, and that is the type that the Trio CDP command will 43 | return. In this case, it returns a ``NodeId``. 44 | 45 | If you have code completion in your Python IDE, it will help you see what the return 46 | type is for each Trio CDP command. 47 | 48 | .. note:: 49 | 50 | In order for this calling style to work, you must be inside a "session 51 | context", i.e. your code must be nested in (or called from inside of) an ``async with 52 | conn.open_session()`` block. If you try calling this from outside of a session context, 53 | you will get an exception. 54 | 55 | Low-level API 56 | ------------- 57 | 58 | The low-level API is a bit more verbose, but you may find it necessary or preferable in 59 | some situations. With this API, you import commands from PyCDP and pass them into the 60 | session ``execute()`` method. Taking the same example as the previous section, here is 61 | how you would execute a CSS query: 62 | 63 | .. code:: 64 | 65 | from cdp import dom 66 | 67 | ...other code... 68 | 69 | node_id = await session.execute(dom.query(root, 'blockquote')) 70 | 71 | If you compare this example to the example in the previous section, there are two big changes. 72 | First, ``dom`` is imported from PyCDP instead of Trio CDP. This means that is a generator, 73 | not an ``async def``. 74 | 75 | Second, in order to run the command on a given session, we have to call that session's 76 | ``execute()`` method and pass in the PyCDP generator. 77 | 78 | Other than being a little more verbose (calling ``session.execute(...)`` for every CDP 79 | command), the low-level API is otherwise very similar to the simplified API described in 80 | the previous section. It still takes the same arguments and returns the same type (here 81 | a ``NodeId``). 82 | -------------------------------------------------------------------------------- /trio_cdp/generated/audits.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.audits 12 | from cdp.audits import ( 13 | AffectedCookie, 14 | AffectedFrame, 15 | AffectedRequest, 16 | AttributionReportingIssueDetails, 17 | AttributionReportingIssueType, 18 | BlockedByResponseIssueDetails, 19 | BlockedByResponseReason, 20 | ClientHintIssueDetails, 21 | ClientHintIssueReason, 22 | ContentSecurityPolicyIssueDetails, 23 | ContentSecurityPolicyViolationType, 24 | CorsIssueDetails, 25 | DeprecationIssueDetails, 26 | GenericIssueDetails, 27 | GenericIssueErrorType, 28 | HeavyAdIssueDetails, 29 | HeavyAdReason, 30 | HeavyAdResolutionStatus, 31 | InspectorIssue, 32 | InspectorIssueCode, 33 | InspectorIssueDetails, 34 | IssueAdded, 35 | IssueId, 36 | LowTextContrastIssueDetails, 37 | MixedContentIssueDetails, 38 | MixedContentResolutionStatus, 39 | MixedContentResourceType, 40 | NavigatorUserAgentIssueDetails, 41 | QuirksModeIssueDetails, 42 | SameSiteCookieExclusionReason, 43 | SameSiteCookieIssueDetails, 44 | SameSiteCookieOperation, 45 | SameSiteCookieWarningReason, 46 | SharedArrayBufferIssueDetails, 47 | SharedArrayBufferIssueType, 48 | SourceCodeLocation, 49 | TrustedWebActivityIssueDetails, 50 | TwaQualityEnforcementViolationType, 51 | WasmCrossOriginModuleSharingIssueDetails 52 | ) 53 | 54 | 55 | async def check_contrast( 56 | report_aaa: typing.Optional[bool] = None 57 | ) -> None: 58 | r''' 59 | Runs the contrast check for the target page. Found issues are reported 60 | using Audits.issueAdded event. 61 | 62 | :param report_aaa: *(Optional)* Whether to report WCAG AAA level issues. Default is false. 63 | ''' 64 | session = get_session_context('audits.check_contrast') 65 | return await session.execute(cdp.audits.check_contrast(report_aaa)) 66 | 67 | 68 | async def disable() -> None: 69 | r''' 70 | Disables issues domain, prevents further issues from being reported to the client. 71 | ''' 72 | session = get_session_context('audits.disable') 73 | return await session.execute(cdp.audits.disable()) 74 | 75 | 76 | async def enable() -> None: 77 | r''' 78 | Enables issues domain, sends the issues collected so far to the client by means of the 79 | ``issueAdded`` event. 80 | ''' 81 | session = get_session_context('audits.enable') 82 | return await session.execute(cdp.audits.enable()) 83 | 84 | 85 | async def get_encoded_response( 86 | request_id: cdp.network.RequestId, 87 | encoding: str, 88 | quality: typing.Optional[float] = None, 89 | size_only: typing.Optional[bool] = None 90 | ) -> typing.Tuple[typing.Optional[str], int, int]: 91 | r''' 92 | Returns the response body and size if it were re-encoded with the specified settings. Only 93 | applies to images. 94 | 95 | :param request_id: Identifier of the network request to get content for. 96 | :param encoding: The encoding to use. 97 | :param quality: *(Optional)* The quality of the encoding (0-1). (defaults to 1) 98 | :param size_only: *(Optional)* Whether to only return the size information (defaults to false). 99 | :returns: A tuple with the following items: 100 | 101 | 0. **body** - *(Optional)* The encoded body as a base64 string. Omitted if sizeOnly is true. (Encoded as a base64 string when passed over JSON) 102 | 1. **originalSize** - Size before re-encoding. 103 | 2. **encodedSize** - Size after re-encoding. 104 | ''' 105 | session = get_session_context('audits.get_encoded_response') 106 | return await session.execute(cdp.audits.get_encoded_response(request_id, encoding, quality, size_only)) 107 | -------------------------------------------------------------------------------- /trio_cdp/generated/memory.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.memory 12 | from cdp.memory import ( 13 | Module, 14 | PressureLevel, 15 | SamplingProfile, 16 | SamplingProfileNode 17 | ) 18 | 19 | 20 | async def forcibly_purge_java_script_memory() -> None: 21 | r''' 22 | Simulate OomIntervention by purging V8 memory. 23 | ''' 24 | session = get_session_context('memory.forcibly_purge_java_script_memory') 25 | return await session.execute(cdp.memory.forcibly_purge_java_script_memory()) 26 | 27 | 28 | async def get_all_time_sampling_profile() -> SamplingProfile: 29 | r''' 30 | Retrieve native memory allocations profile 31 | collected since renderer process startup. 32 | 33 | :returns: 34 | ''' 35 | session = get_session_context('memory.get_all_time_sampling_profile') 36 | return await session.execute(cdp.memory.get_all_time_sampling_profile()) 37 | 38 | 39 | async def get_browser_sampling_profile() -> SamplingProfile: 40 | r''' 41 | Retrieve native memory allocations profile 42 | collected since browser process startup. 43 | 44 | :returns: 45 | ''' 46 | session = get_session_context('memory.get_browser_sampling_profile') 47 | return await session.execute(cdp.memory.get_browser_sampling_profile()) 48 | 49 | 50 | async def get_dom_counters() -> typing.Tuple[int, int, int]: 51 | r''' 52 | 53 | 54 | :returns: A tuple with the following items: 55 | 56 | 0. **documents** - 57 | 1. **nodes** - 58 | 2. **jsEventListeners** - 59 | ''' 60 | session = get_session_context('memory.get_dom_counters') 61 | return await session.execute(cdp.memory.get_dom_counters()) 62 | 63 | 64 | async def get_sampling_profile() -> SamplingProfile: 65 | r''' 66 | Retrieve native memory allocations profile collected since last 67 | ``startSampling`` call. 68 | 69 | :returns: 70 | ''' 71 | session = get_session_context('memory.get_sampling_profile') 72 | return await session.execute(cdp.memory.get_sampling_profile()) 73 | 74 | 75 | async def prepare_for_leak_detection() -> None: 76 | session = get_session_context('memory.prepare_for_leak_detection') 77 | return await session.execute(cdp.memory.prepare_for_leak_detection()) 78 | 79 | 80 | async def set_pressure_notifications_suppressed( 81 | suppressed: bool 82 | ) -> None: 83 | r''' 84 | Enable/disable suppressing memory pressure notifications in all processes. 85 | 86 | :param suppressed: If true, memory pressure notifications will be suppressed. 87 | ''' 88 | session = get_session_context('memory.set_pressure_notifications_suppressed') 89 | return await session.execute(cdp.memory.set_pressure_notifications_suppressed(suppressed)) 90 | 91 | 92 | async def simulate_pressure_notification( 93 | level: PressureLevel 94 | ) -> None: 95 | r''' 96 | Simulate a memory pressure notification in all processes. 97 | 98 | :param level: Memory pressure level of the notification. 99 | ''' 100 | session = get_session_context('memory.simulate_pressure_notification') 101 | return await session.execute(cdp.memory.simulate_pressure_notification(level)) 102 | 103 | 104 | async def start_sampling( 105 | sampling_interval: typing.Optional[int] = None, 106 | suppress_randomness: typing.Optional[bool] = None 107 | ) -> None: 108 | r''' 109 | Start collecting native memory profile. 110 | 111 | :param sampling_interval: *(Optional)* Average number of bytes between samples. 112 | :param suppress_randomness: *(Optional)* Do not randomize intervals between samples. 113 | ''' 114 | session = get_session_context('memory.start_sampling') 115 | return await session.execute(cdp.memory.start_sampling(sampling_interval, suppress_randomness)) 116 | 117 | 118 | async def stop_sampling() -> None: 119 | r''' 120 | Stop collecting native memory profile. 121 | ''' 122 | session = get_session_context('memory.stop_sampling') 123 | return await session.execute(cdp.memory.stop_sampling()) 124 | -------------------------------------------------------------------------------- /trio_cdp/generated/tracing.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.tracing 12 | from cdp.tracing import ( 13 | BufferUsage, 14 | DataCollected, 15 | MemoryDumpConfig, 16 | MemoryDumpLevelOfDetail, 17 | StreamCompression, 18 | StreamFormat, 19 | TraceConfig, 20 | TracingBackend, 21 | TracingComplete 22 | ) 23 | 24 | 25 | async def end() -> None: 26 | r''' 27 | Stop trace events collection. 28 | ''' 29 | session = get_session_context('tracing.end') 30 | return await session.execute(cdp.tracing.end()) 31 | 32 | 33 | async def get_categories() -> typing.List[str]: 34 | r''' 35 | Gets supported tracing categories. 36 | 37 | :returns: A list of supported tracing categories. 38 | ''' 39 | session = get_session_context('tracing.get_categories') 40 | return await session.execute(cdp.tracing.get_categories()) 41 | 42 | 43 | async def record_clock_sync_marker( 44 | sync_id: str 45 | ) -> None: 46 | r''' 47 | Record a clock sync marker in the trace. 48 | 49 | :param sync_id: The ID of this clock sync marker 50 | ''' 51 | session = get_session_context('tracing.record_clock_sync_marker') 52 | return await session.execute(cdp.tracing.record_clock_sync_marker(sync_id)) 53 | 54 | 55 | async def request_memory_dump( 56 | deterministic: typing.Optional[bool] = None, 57 | level_of_detail: typing.Optional[MemoryDumpLevelOfDetail] = None 58 | ) -> typing.Tuple[str, bool]: 59 | r''' 60 | Request a global memory dump. 61 | 62 | :param deterministic: *(Optional)* Enables more deterministic results by forcing garbage collection 63 | :param level_of_detail: *(Optional)* Specifies level of details in memory dump. Defaults to "detailed". 64 | :returns: A tuple with the following items: 65 | 66 | 0. **dumpGuid** - GUID of the resulting global memory dump. 67 | 1. **success** - True iff the global memory dump succeeded. 68 | ''' 69 | session = get_session_context('tracing.request_memory_dump') 70 | return await session.execute(cdp.tracing.request_memory_dump(deterministic, level_of_detail)) 71 | 72 | 73 | async def start( 74 | categories: typing.Optional[str] = None, 75 | options: typing.Optional[str] = None, 76 | buffer_usage_reporting_interval: typing.Optional[float] = None, 77 | transfer_mode: typing.Optional[str] = None, 78 | stream_format: typing.Optional[StreamFormat] = None, 79 | stream_compression: typing.Optional[StreamCompression] = None, 80 | trace_config: typing.Optional[TraceConfig] = None, 81 | perfetto_config: typing.Optional[str] = None, 82 | tracing_backend: typing.Optional[TracingBackend] = None 83 | ) -> None: 84 | r''' 85 | Start trace events collection. 86 | 87 | :param categories: **(DEPRECATED)** *(Optional)* Category/tag filter 88 | :param options: **(DEPRECATED)** *(Optional)* Tracing options 89 | :param buffer_usage_reporting_interval: *(Optional)* If set, the agent will issue bufferUsage events at this interval, specified in milliseconds 90 | :param transfer_mode: *(Optional)* Whether to report trace events as series of dataCollected events or to save trace to a stream (defaults to ```ReportEvents````). 91 | :param stream_format: *(Optional)* Trace data format to use. This only applies when using ````ReturnAsStream```` transfer mode (defaults to ````json````). 92 | :param stream_compression: *(Optional)* Compression format to use. This only applies when using ````ReturnAsStream```` transfer mode (defaults to ````none````) 93 | :param trace_config: *(Optional)* 94 | :param perfetto_config: *(Optional)* Base64-encoded serialized perfetto.protos.TraceConfig protobuf message When specified, the parameters ````categories````, ````options````, ````traceConfig```` are ignored. (Encoded as a base64 string when passed over JSON) 95 | :param tracing_backend: *(Optional)* Backend type (defaults to ````auto```) 96 | ''' 97 | session = get_session_context('tracing.start') 98 | return await session.execute(cdp.tracing.start(categories, options, buffer_usage_reporting_interval, transfer_mode, stream_format, stream_compression, trace_config, perfetto_config, tracing_backend)) 99 | -------------------------------------------------------------------------------- /.github/workflows/auto-copilot-org-playwright-loopv2.yaml: -------------------------------------------------------------------------------- 1 | name: "Org-wide: Copilot Playwright Test, Review, Auto-fix, PR, Merge" 2 | uto-amazonq-review.properties.json 3 | 4 | uto-amazonq-review.properties.json 5 | on: 6 | uto-amazonq-review.properties.json 7 | push: 8 | uto-amazonq-review.properties.json 9 | branches: 10 | uto-amazonq-review.properties.json 11 | - main 12 | uto-amazonq-review.properties.json 13 | - master 14 | uto-amazonq-review.properties.json 15 | 16 | uto-amazonq-review.properties.json 17 | jobs: 18 | uto-amazonq-review.properties.json 19 | playwright-review-fix: 20 | uto-amazonq-review.properties.json 21 | runs-on: self-hosted 22 | uto-amazonq-review.properties.json 23 | steps: 24 | uto-amazonq-review.properties.json 25 | - name: Checkout code 26 | uto-amazonq-review.properties.json 27 | uses: actions/checkout@main 28 | uto-amazonq-review.properties.json 29 | 30 | uto-amazonq-review.properties.json 31 | - name: Setup Python 32 | uto-amazonq-review.properties.json 33 | uses: actions/setup-python@main 34 | uto-amazonq-review.properties.json 35 | with: 36 | uto-amazonq-review.properties.json 37 | python-version: "3.11" 38 | uto-amazonq-review.properties.json 39 | 40 | uto-amazonq-review.properties.json 41 | - name: Install dependencies 42 | uto-amazonq-review.properties.json 43 | run: | 44 | uto-amazonq-review.properties.json 45 | pip install -r requirements.txt 46 | uto-amazonq-review.properties.json 47 | pip install pytest playwright pytest-playwright 48 | uto-amazonq-review.properties.json 49 | 50 | uto-amazonq-review.properties.json 51 | - name: Install Playwright browsers 52 | uto-amazonq-review.properties.json 53 | run: | 54 | uto-amazonq-review.properties.json 55 | python -m playwright install 56 | uto-amazonq-review.properties.json 57 | 58 | uto-amazonq-review.properties.json 59 | - name: Run Playwright Tests 60 | uto-amazonq-review.properties.json 61 | run: | 62 | uto-amazonq-review.properties.json 63 | pytest tests/ || exit 1 64 | uto-amazonq-review.properties.json 65 | continue-on-error: true 66 | uto-amazonq-review.properties.json 67 | 68 | uto-amazonq-review.properties.json 69 | - name: Copilot PR Agent Review 70 | uto-amazonq-review.properties.json 71 | uses: github/copilot-agent/pr@main 72 | uto-amazonq-review.properties.json 73 | with: 74 | uto-amazonq-review.properties.json 75 | github-token: ${{ secrets.GITHUB_TOKEN }} 76 | uto-amazonq-review.properties.json 77 | continue-on-error: true 78 | uto-amazonq-review.properties.json 79 | 80 | uto-amazonq-review.properties.json 81 | - name: Copilot Auto-fix Failing Playwright Tests 82 | uto-amazonq-review.properties.json 83 | uses: github/copilot-agent/fix@main 84 | uto-amazonq-review.properties.json 85 | with: 86 | uto-amazonq-review.properties.json 87 | github-token: ${{ secrets.GITHUB_TOKEN }} 88 | uto-amazonq-review.properties.json 89 | max_attempts: 3 90 | uto-amazonq-review.properties.json 91 | continue-on-error: true 92 | uto-amazonq-review.properties.json 93 | 94 | uto-amazonq-review.properties.json 95 | - name: Create Pull Request for Automated Fixes 96 | uto-amazonq-review.properties.json 97 | uses: peter-evans/create-pull-request@main 98 | uto-amazonq-review.properties.json 99 | with: 100 | uto-amazonq-review.properties.json 101 | branch: "copilot/playwright-fixes" 102 | uto-amazonq-review.properties.json 103 | title: "Copilot: Auto-fix Playwright Tests" 104 | uto-amazonq-review.properties.json 105 | body: "Automated Playwright test fixes by Copilot Agent." 106 | uto-amazonq-review.properties.json 107 | commit-message: "Copilot agent Playwright bugfixes" 108 | uto-amazonq-review.properties.json 109 | continue-on-error: true 110 | uto-amazonq-review.properties.json 111 | 112 | uto-amazonq-review.properties.json 113 | - name: Automerge PR if checks pass 114 | uto-amazonq-review.properties.json 115 | uses: pascalgn/automerge-action@main 116 | uto-amazonq-review.properties.json 117 | with: 118 | uto-amazonq-review.properties.json 119 | merge-method: squash 120 | uto-amazonq-review.properties.json 121 | github-token: ${{ secrets.GITHUB_TOKEN }} 122 | uto-amazonq-review.properties.json 123 | continue-on-error: true 124 | uto-amazonq-review.properties.json 125 | -------------------------------------------------------------------------------- /trio_cdp/generated/animation.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.animation 12 | from cdp.animation import ( 13 | Animation, 14 | AnimationCanceled, 15 | AnimationCreated, 16 | AnimationEffect, 17 | AnimationStarted, 18 | KeyframeStyle, 19 | KeyframesRule 20 | ) 21 | 22 | 23 | async def disable() -> None: 24 | r''' 25 | Disables animation domain notifications. 26 | ''' 27 | session = get_session_context('animation.disable') 28 | return await session.execute(cdp.animation.disable()) 29 | 30 | 31 | async def enable() -> None: 32 | r''' 33 | Enables animation domain notifications. 34 | ''' 35 | session = get_session_context('animation.enable') 36 | return await session.execute(cdp.animation.enable()) 37 | 38 | 39 | async def get_current_time( 40 | id_: str 41 | ) -> float: 42 | r''' 43 | Returns the current time of the an animation. 44 | 45 | :param id_: Id of animation. 46 | :returns: Current time of the page. 47 | ''' 48 | session = get_session_context('animation.get_current_time') 49 | return await session.execute(cdp.animation.get_current_time(id_)) 50 | 51 | 52 | async def get_playback_rate() -> float: 53 | r''' 54 | Gets the playback rate of the document timeline. 55 | 56 | :returns: Playback rate for animations on page. 57 | ''' 58 | session = get_session_context('animation.get_playback_rate') 59 | return await session.execute(cdp.animation.get_playback_rate()) 60 | 61 | 62 | async def release_animations( 63 | animations: typing.List[str] 64 | ) -> None: 65 | r''' 66 | Releases a set of animations to no longer be manipulated. 67 | 68 | :param animations: List of animation ids to seek. 69 | ''' 70 | session = get_session_context('animation.release_animations') 71 | return await session.execute(cdp.animation.release_animations(animations)) 72 | 73 | 74 | async def resolve_animation( 75 | animation_id: str 76 | ) -> cdp.runtime.RemoteObject: 77 | r''' 78 | Gets the remote object of the Animation. 79 | 80 | :param animation_id: Animation id. 81 | :returns: Corresponding remote object. 82 | ''' 83 | session = get_session_context('animation.resolve_animation') 84 | return await session.execute(cdp.animation.resolve_animation(animation_id)) 85 | 86 | 87 | async def seek_animations( 88 | animations: typing.List[str], 89 | current_time: float 90 | ) -> None: 91 | r''' 92 | Seek a set of animations to a particular time within each animation. 93 | 94 | :param animations: List of animation ids to seek. 95 | :param current_time: Set the current time of each animation. 96 | ''' 97 | session = get_session_context('animation.seek_animations') 98 | return await session.execute(cdp.animation.seek_animations(animations, current_time)) 99 | 100 | 101 | async def set_paused( 102 | animations: typing.List[str], 103 | paused: bool 104 | ) -> None: 105 | r''' 106 | Sets the paused state of a set of animations. 107 | 108 | :param animations: Animations to set the pause state of. 109 | :param paused: Paused state to set to. 110 | ''' 111 | session = get_session_context('animation.set_paused') 112 | return await session.execute(cdp.animation.set_paused(animations, paused)) 113 | 114 | 115 | async def set_playback_rate( 116 | playback_rate: float 117 | ) -> None: 118 | r''' 119 | Sets the playback rate of the document timeline. 120 | 121 | :param playback_rate: Playback rate for animations on page 122 | ''' 123 | session = get_session_context('animation.set_playback_rate') 124 | return await session.execute(cdp.animation.set_playback_rate(playback_rate)) 125 | 126 | 127 | async def set_timing( 128 | animation_id: str, 129 | duration: float, 130 | delay: float 131 | ) -> None: 132 | r''' 133 | Sets the timing of an animation node. 134 | 135 | :param animation_id: Animation id. 136 | :param duration: Duration of the animation. 137 | :param delay: Delay of the animation. 138 | ''' 139 | session = get_session_context('animation.set_timing') 140 | return await session.execute(cdp.animation.set_timing(animation_id, duration, delay)) 141 | -------------------------------------------------------------------------------- /.github/workflows/trigger-all-repos.yml: -------------------------------------------------------------------------------- 1 | name: Trigger Workflow on All Repos 2 | uto-amazonq-review.properties.json 3 | 4 | uto-amazonq-review.properties.json 5 | on: 6 | uto-amazonq-review.properties.json 7 | workflow_dispatch: 8 | uto-amazonq-review.properties.json 9 | inputs: 10 | uto-amazonq-review.properties.json 11 | workflow_file: 12 | uto-amazonq-review.properties.json 13 | description: 'Workflow file name to trigger (e.g., workflows-sync.yml)' 14 | uto-amazonq-review.properties.json 15 | required: true 16 | uto-amazonq-review.properties.json 17 | type: string 18 | uto-amazonq-review.properties.json 19 | ref: 20 | uto-amazonq-review.properties.json 21 | description: 'Git reference (branch/tag/SHA) to run workflow from' 22 | uto-amazonq-review.properties.json 23 | required: false 24 | uto-amazonq-review.properties.json 25 | default: 'main' 26 | uto-amazonq-review.properties.json 27 | type: string 28 | uto-amazonq-review.properties.json 29 | include_archived: 30 | uto-amazonq-review.properties.json 31 | description: 'Include archived repositories' 32 | uto-amazonq-review.properties.json 33 | required: false 34 | uto-amazonq-review.properties.json 35 | default: false 36 | uto-amazonq-review.properties.json 37 | type: boolean 38 | uto-amazonq-review.properties.json 39 | check_only: 40 | uto-amazonq-review.properties.json 41 | description: 'Only check which repos have the workflow (do not trigger)' 42 | uto-amazonq-review.properties.json 43 | required: false 44 | uto-amazonq-review.properties.json 45 | default: false 46 | uto-amazonq-review.properties.json 47 | type: boolean 48 | uto-amazonq-review.properties.json 49 | 50 | uto-amazonq-review.properties.json 51 | jobs: 52 | uto-amazonq-review.properties.json 53 | trigger-all: 54 | uto-amazonq-review.properties.json 55 | runs-on: self-hosted 56 | uto-amazonq-review.properties.json 57 | steps: 58 | uto-amazonq-review.properties.json 59 | - name: Checkout repository 60 | uto-amazonq-review.properties.json 61 | uses: actions/checkout@main 62 | uto-amazonq-review.properties.json 63 | 64 | uto-amazonq-review.properties.json 65 | - name: Set up Python 66 | uto-amazonq-review.properties.json 67 | uses: actions/setup-python@main 68 | uto-amazonq-review.properties.json 69 | with: 70 | uto-amazonq-review.properties.json 71 | python-version: '3.11' 72 | uto-amazonq-review.properties.json 73 | 74 | uto-amazonq-review.properties.json 75 | - name: Install dependencies 76 | uto-amazonq-review.properties.json 77 | run: | 78 | uto-amazonq-review.properties.json 79 | pip install requests 80 | uto-amazonq-review.properties.json 81 | 82 | uto-amazonq-review.properties.json 83 | - name: Trigger workflow on all repositories 84 | uto-amazonq-review.properties.json 85 | env: 86 | uto-amazonq-review.properties.json 87 | GITHUB_TOKEN: ${{ secrets.GH_PAT }} 88 | uto-amazonq-review.properties.json 89 | run: | 90 | uto-amazonq-review.properties.json 91 | python trigger_workflow_all_repos.py \ 92 | uto-amazonq-review.properties.json 93 | P4X-ng \ 94 | uto-amazonq-review.properties.json 95 | "${{ inputs.workflow_file }}" \ 96 | uto-amazonq-review.properties.json 97 | --ref "${{ inputs.ref }}" \ 98 | uto-amazonq-review.properties.json 99 | ${{ inputs.include_archived && '--include-archived' || '' }} \ 100 | uto-amazonq-review.properties.json 101 | ${{ inputs.check_only && '--check-only' || '' }} \ 102 | uto-amazonq-review.properties.json 103 | --delay 1.5 104 | uto-amazonq-review.properties.json 105 | 106 | uto-amazonq-review.properties.json 107 | - name: Summary 108 | uto-amazonq-review.properties.json 109 | run: | 110 | uto-amazonq-review.properties.json 111 | echo "## Workflow Dispatch Summary" >> $GITHUB_STEP_SUMMARY 112 | uto-amazonq-review.properties.json 113 | echo "" >> $GITHUB_STEP_SUMMARY 114 | uto-amazonq-review.properties.json 115 | echo "**Workflow:** ${{ inputs.workflow_file }}" >> $GITHUB_STEP_SUMMARY 116 | uto-amazonq-review.properties.json 117 | echo "**Reference:** ${{ inputs.ref }}" >> $GITHUB_STEP_SUMMARY 118 | uto-amazonq-review.properties.json 119 | echo "**Include archived:** ${{ inputs.include_archived }}" >> $GITHUB_STEP_SUMMARY 120 | uto-amazonq-review.properties.json 121 | echo "**Check only:** ${{ inputs.check_only }}" >> $GITHUB_STEP_SUMMARY 122 | uto-amazonq-review.properties.json 123 | echo "" >> $GITHUB_STEP_SUMMARY 124 | uto-amazonq-review.properties.json 125 | echo "See logs above for detailed results." >> $GITHUB_STEP_SUMMARY 126 | uto-amazonq-review.properties.json 127 | -------------------------------------------------------------------------------- /.github/workflows/auto-copilot-playwright-auto-test.yml: -------------------------------------------------------------------------------- 1 | name: "Copilot: Generate and Run Playwright Tests Until Passing" 2 | uto-amazonq-review.properties.json 3 | 4 | uto-amazonq-review.properties.json 5 | on: 6 | uto-amazonq-review.properties.json 7 | push: 8 | uto-amazonq-review.properties.json 9 | branches: 10 | uto-amazonq-review.properties.json 11 | - main 12 | uto-amazonq-review.properties.json 13 | - master 14 | uto-amazonq-review.properties.json 15 | 16 | uto-amazonq-review.properties.json 17 | jobs: 18 | uto-amazonq-review.properties.json 19 | generate-and-test: 20 | uto-amazonq-review.properties.json 21 | runs-on: self-hosted 22 | uto-amazonq-review.properties.json 23 | steps: 24 | uto-amazonq-review.properties.json 25 | - name: Checkout code 26 | uto-amazonq-review.properties.json 27 | uses: actions/checkout@main 28 | uto-amazonq-review.properties.json 29 | 30 | uto-amazonq-review.properties.json 31 | - name: Setup Python 32 | uto-amazonq-review.properties.json 33 | uses: actions/setup-python@main 34 | uto-amazonq-review.properties.json 35 | with: 36 | uto-amazonq-review.properties.json 37 | python-version: "3.11" 38 | uto-amazonq-review.properties.json 39 | 40 | uto-amazonq-review.properties.json 41 | - name: Install dependencies 42 | uto-amazonq-review.properties.json 43 | run: | 44 | uto-amazonq-review.properties.json 45 | pip install -r requirements.txt 46 | uto-amazonq-review.properties.json 47 | pip install pytest playwright pytest-playwright 48 | uto-amazonq-review.properties.json 49 | 50 | uto-amazonq-review.properties.json 51 | - name: Install Playwright browsers 52 | uto-amazonq-review.properties.json 53 | run: | 54 | uto-amazonq-review.properties.json 55 | python -m playwright install 56 | uto-amazonq-review.properties.json 57 | 58 | uto-amazonq-review.properties.json 59 | - name: Copilot Generate Playwright Scripts 60 | uto-amazonq-review.properties.json 61 | uses: github/copilot-agent/playwright-generate@main # Example, customize for Python; or use Chat to generate script 62 | uto-amazonq-review.properties.json 63 | with: 64 | uto-amazonq-review.properties.json 65 | github-token: ${{ secrets.GITHUB_TOKEN }} 66 | uto-amazonq-review.properties.json 67 | prompt: "Generate Playwright test scripts covering every user action on this web app." 68 | uto-amazonq-review.properties.json 69 | continue-on-error: true # If your agent doesn't support, replace with python script generation using Copilot Chat 70 | uto-amazonq-review.properties.json 71 | 72 | uto-amazonq-review.properties.json 73 | - name: Run Playwright Tests 74 | uto-amazonq-review.properties.json 75 | run: | 76 | uto-amazonq-review.properties.json 77 | pytest tests/ # Or the path to your Playwright scripts 78 | uto-amazonq-review.properties.json 79 | 80 | uto-amazonq-review.properties.json 81 | - name: If Tests Fail, Copilot Attempts Fix & Repeats 82 | uto-amazonq-review.properties.json 83 | uses: github/copilot-agent/playwright-fix-and-loop@main # Example, requires agent loop feature 84 | uto-amazonq-review.properties.json 85 | with: 86 | uto-amazonq-review.properties.json 87 | github-token: ${{ secrets.GITHUB_TOKEN }} 88 | uto-amazonq-review.properties.json 89 | max_attempts: 5 90 | uto-amazonq-review.properties.json 91 | continue-on-error: true 92 | uto-amazonq-review.properties.json 93 | 94 | uto-amazonq-review.properties.json 95 | - name: Create PR with passing tests or attempted fixes 96 | uto-amazonq-review.properties.json 97 | uses: peter-evans/create-pull-request@main 98 | uto-amazonq-review.properties.json 99 | with: 100 | uto-amazonq-review.properties.json 101 | branch: "copilot/playwright-auto-tests" 102 | uto-amazonq-review.properties.json 103 | title: "Copilot generated Playwright tests (auto-fixed)" 104 | uto-amazonq-review.properties.json 105 | body: "Automated Playwright test generation/fix by Copilot agent." 106 | uto-amazonq-review.properties.json 107 | commit-message: "Copilot agent Playwright tests and fixes" 108 | uto-amazonq-review.properties.json 109 | continue-on-error: true 110 | uto-amazonq-review.properties.json 111 | 112 | uto-amazonq-review.properties.json 113 | - name: Auto-merge if passing 114 | uto-amazonq-review.properties.json 115 | uses: pascalgn/automerge-action@main 116 | uto-amazonq-review.properties.json 117 | with: 118 | uto-amazonq-review.properties.json 119 | merge-method: squash 120 | uto-amazonq-review.properties.json 121 | github-token: ${{ secrets.GITHUB_TOKEN }} 122 | uto-amazonq-review.properties.json 123 | continue-on-error: true 124 | uto-amazonq-review.properties.json 125 | -------------------------------------------------------------------------------- /trio_cdp/generated/service_worker.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.service_worker 12 | from cdp.service_worker import ( 13 | RegistrationID, 14 | ServiceWorkerErrorMessage, 15 | ServiceWorkerRegistration, 16 | ServiceWorkerVersion, 17 | ServiceWorkerVersionRunningStatus, 18 | ServiceWorkerVersionStatus, 19 | WorkerErrorReported, 20 | WorkerRegistrationUpdated, 21 | WorkerVersionUpdated 22 | ) 23 | 24 | 25 | async def deliver_push_message( 26 | origin: str, 27 | registration_id: RegistrationID, 28 | data: str 29 | ) -> None: 30 | r''' 31 | :param origin: 32 | :param registration_id: 33 | :param data: 34 | ''' 35 | session = get_session_context('service_worker.deliver_push_message') 36 | return await session.execute(cdp.service_worker.deliver_push_message(origin, registration_id, data)) 37 | 38 | 39 | async def disable() -> None: 40 | session = get_session_context('service_worker.disable') 41 | return await session.execute(cdp.service_worker.disable()) 42 | 43 | 44 | async def dispatch_periodic_sync_event( 45 | origin: str, 46 | registration_id: RegistrationID, 47 | tag: str 48 | ) -> None: 49 | r''' 50 | :param origin: 51 | :param registration_id: 52 | :param tag: 53 | ''' 54 | session = get_session_context('service_worker.dispatch_periodic_sync_event') 55 | return await session.execute(cdp.service_worker.dispatch_periodic_sync_event(origin, registration_id, tag)) 56 | 57 | 58 | async def dispatch_sync_event( 59 | origin: str, 60 | registration_id: RegistrationID, 61 | tag: str, 62 | last_chance: bool 63 | ) -> None: 64 | r''' 65 | :param origin: 66 | :param registration_id: 67 | :param tag: 68 | :param last_chance: 69 | ''' 70 | session = get_session_context('service_worker.dispatch_sync_event') 71 | return await session.execute(cdp.service_worker.dispatch_sync_event(origin, registration_id, tag, last_chance)) 72 | 73 | 74 | async def enable() -> None: 75 | session = get_session_context('service_worker.enable') 76 | return await session.execute(cdp.service_worker.enable()) 77 | 78 | 79 | async def inspect_worker( 80 | version_id: str 81 | ) -> None: 82 | r''' 83 | :param version_id: 84 | ''' 85 | session = get_session_context('service_worker.inspect_worker') 86 | return await session.execute(cdp.service_worker.inspect_worker(version_id)) 87 | 88 | 89 | async def set_force_update_on_page_load( 90 | force_update_on_page_load: bool 91 | ) -> None: 92 | r''' 93 | :param force_update_on_page_load: 94 | ''' 95 | session = get_session_context('service_worker.set_force_update_on_page_load') 96 | return await session.execute(cdp.service_worker.set_force_update_on_page_load(force_update_on_page_load)) 97 | 98 | 99 | async def skip_waiting( 100 | scope_url: str 101 | ) -> None: 102 | r''' 103 | :param scope_url: 104 | ''' 105 | session = get_session_context('service_worker.skip_waiting') 106 | return await session.execute(cdp.service_worker.skip_waiting(scope_url)) 107 | 108 | 109 | async def start_worker( 110 | scope_url: str 111 | ) -> None: 112 | r''' 113 | :param scope_url: 114 | ''' 115 | session = get_session_context('service_worker.start_worker') 116 | return await session.execute(cdp.service_worker.start_worker(scope_url)) 117 | 118 | 119 | async def stop_all_workers() -> None: 120 | session = get_session_context('service_worker.stop_all_workers') 121 | return await session.execute(cdp.service_worker.stop_all_workers()) 122 | 123 | 124 | async def stop_worker( 125 | version_id: str 126 | ) -> None: 127 | r''' 128 | :param version_id: 129 | ''' 130 | session = get_session_context('service_worker.stop_worker') 131 | return await session.execute(cdp.service_worker.stop_worker(version_id)) 132 | 133 | 134 | async def unregister( 135 | scope_url: str 136 | ) -> None: 137 | r''' 138 | :param scope_url: 139 | ''' 140 | session = get_session_context('service_worker.unregister') 141 | return await session.execute(cdp.service_worker.unregister(scope_url)) 142 | 143 | 144 | async def update_registration( 145 | scope_url: str 146 | ) -> None: 147 | r''' 148 | :param scope_url: 149 | ''' 150 | session = get_session_context('service_worker.update_registration') 151 | return await session.execute(cdp.service_worker.update_registration(scope_url)) 152 | -------------------------------------------------------------------------------- /tests/test_session_detach.py: -------------------------------------------------------------------------------- 1 | """ 2 | Test that detachFromTarget is called when closing a session. 3 | """ 4 | import json 5 | import logging 6 | import sys 7 | import os 8 | 9 | # Add the source directory to path 10 | sys.path.insert(0, os.path.join(os.path.dirname(__file__), '..')) 11 | 12 | from cdp import target 13 | import pytest 14 | import trio 15 | from trio_websocket import serve_websocket 16 | 17 | # Import directly from the main module to avoid generated imports 18 | from trio_cdp import CdpConnection, open_cdp 19 | 20 | # Import test utilities 21 | from . import fail_after 22 | 23 | 24 | HOST = '127.0.0.1' 25 | 26 | 27 | async def start_server(nursery, handler): 28 | ''' A helper that starts a WebSocket server and runs ``handler`` for each 29 | connection. Returns the server URL. ''' 30 | server = await nursery.start(serve_websocket, handler, HOST, 0, None) 31 | return f'ws://{HOST}:{server.port}/devtools/browser/uuid' 32 | 33 | 34 | @fail_after(5) 35 | async def test_session_detach_on_exit(nursery): 36 | """Test that detachFromTarget is called when exiting open_session context.""" 37 | detach_called = False 38 | 39 | async def handler(request): 40 | nonlocal detach_called 41 | try: 42 | ws = await request.accept() 43 | 44 | # Handle "attachToTarget" command 45 | command = json.loads(await ws.get_message()) 46 | assert command['method'] == 'Target.attachToTarget' 47 | assert command['params']['targetId'] == 'target1' 48 | logging.info('Server received attachToTarget: %r', command) 49 | response = { 50 | 'id': command['id'], 51 | 'result': { 52 | 'sessionId': 'session1', 53 | } 54 | } 55 | logging.info('Server sending: %r', response) 56 | await ws.send_message(json.dumps(response)) 57 | 58 | # Handle "detachFromTarget" command 59 | command = json.loads(await ws.get_message()) 60 | assert command['method'] == 'Target.detachFromTarget' 61 | assert command['params']['sessionId'] == 'session1' 62 | detach_called = True 63 | logging.info('Server received detachFromTarget: %r', command) 64 | response = { 65 | 'id': command['id'], 66 | 'result': {} 67 | } 68 | logging.info('Server sending: %r', response) 69 | await ws.send_message(json.dumps(response)) 70 | except Exception: 71 | logging.exception('Server exception') 72 | 73 | server = await start_server(nursery, handler) 74 | 75 | async with open_cdp(server) as conn: 76 | async with conn.open_session(target.TargetID('target1')) as session: 77 | assert session.session_id == 'session1' 78 | # Don't do anything else in the session 79 | 80 | # After exiting the session context, detach should have been called 81 | assert detach_called, "detachFromTarget was not called when session closed" 82 | 83 | 84 | @fail_after(5) 85 | async def test_session_detach_on_exception(nursery): 86 | """Test that detachFromTarget is called even when an exception occurs in the session.""" 87 | detach_called = False 88 | 89 | async def handler(request): 90 | nonlocal detach_called 91 | try: 92 | ws = await request.accept() 93 | 94 | # Handle "attachToTarget" command 95 | command = json.loads(await ws.get_message()) 96 | assert command['method'] == 'Target.attachToTarget' 97 | logging.info('Server received attachToTarget: %r', command) 98 | response = { 99 | 'id': command['id'], 100 | 'result': { 101 | 'sessionId': 'session1', 102 | } 103 | } 104 | await ws.send_message(json.dumps(response)) 105 | 106 | # Handle "detachFromTarget" command 107 | command = json.loads(await ws.get_message()) 108 | assert command['method'] == 'Target.detachFromTarget' 109 | assert command['params']['sessionId'] == 'session1' 110 | detach_called = True 111 | logging.info('Server received detachFromTarget: %r', command) 112 | response = { 113 | 'id': command['id'], 114 | 'result': {} 115 | } 116 | await ws.send_message(json.dumps(response)) 117 | except Exception: 118 | logging.exception('Server exception') 119 | 120 | server = await start_server(nursery, handler) 121 | 122 | async with open_cdp(server) as conn: 123 | with pytest.raises(RuntimeError): 124 | async with conn.open_session(target.TargetID('target1')) as session: 125 | assert session.session_id == 'session1' 126 | # Raise an exception in the session 127 | raise RuntimeError("Test exception") 128 | 129 | # After exiting the session context, detach should still have been called 130 | assert detach_called, "detachFromTarget was not called when session closed with exception" 131 | -------------------------------------------------------------------------------- /trio_cdp/generated/dom_snapshot.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.dom_snapshot 12 | from cdp.dom_snapshot import ( 13 | ArrayOfStrings, 14 | ComputedStyle, 15 | DOMNode, 16 | DocumentSnapshot, 17 | InlineTextBox, 18 | LayoutTreeNode, 19 | LayoutTreeSnapshot, 20 | NameValue, 21 | NodeTreeSnapshot, 22 | RareBooleanData, 23 | RareIntegerData, 24 | RareStringData, 25 | Rectangle, 26 | StringIndex, 27 | TextBoxSnapshot 28 | ) 29 | 30 | 31 | async def capture_snapshot( 32 | computed_styles: typing.List[str], 33 | include_paint_order: typing.Optional[bool] = None, 34 | include_dom_rects: typing.Optional[bool] = None, 35 | include_blended_background_colors: typing.Optional[bool] = None, 36 | include_text_color_opacities: typing.Optional[bool] = None 37 | ) -> typing.Tuple[typing.List[DocumentSnapshot], typing.List[str]]: 38 | r''' 39 | Returns a document snapshot, including the full DOM tree of the root node (including iframes, 40 | template contents, and imported documents) in a flattened array, as well as layout and 41 | white-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is 42 | flattened. 43 | 44 | :param computed_styles: Whitelist of computed styles to return. 45 | :param include_paint_order: *(Optional)* Whether to include layout object paint orders into the snapshot. 46 | :param include_dom_rects: *(Optional)* Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot 47 | :param include_blended_background_colors: **(EXPERIMENTAL)** *(Optional)* Whether to include blended background colors in the snapshot (default: false). Blended background color is achieved by blending background colors of all elements that overlap with the current element. 48 | :param include_text_color_opacities: **(EXPERIMENTAL)** *(Optional)* Whether to include text color opacity in the snapshot (default: false). An element might have the opacity property set that affects the text color of the element. The final text color opacity is computed based on the opacity of all overlapping elements. 49 | :returns: A tuple with the following items: 50 | 51 | 0. **documents** - The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document. 52 | 1. **strings** - Shared string table that all string properties refer to with indexes. 53 | ''' 54 | session = get_session_context('dom_snapshot.capture_snapshot') 55 | return await session.execute(cdp.dom_snapshot.capture_snapshot(computed_styles, include_paint_order, include_dom_rects, include_blended_background_colors, include_text_color_opacities)) 56 | 57 | 58 | async def disable() -> None: 59 | r''' 60 | Disables DOM snapshot agent for the given page. 61 | ''' 62 | session = get_session_context('dom_snapshot.disable') 63 | return await session.execute(cdp.dom_snapshot.disable()) 64 | 65 | 66 | async def enable() -> None: 67 | r''' 68 | Enables DOM snapshot agent for the given page. 69 | ''' 70 | session = get_session_context('dom_snapshot.enable') 71 | return await session.execute(cdp.dom_snapshot.enable()) 72 | 73 | 74 | async def get_snapshot( 75 | computed_style_whitelist: typing.List[str], 76 | include_event_listeners: typing.Optional[bool] = None, 77 | include_paint_order: typing.Optional[bool] = None, 78 | include_user_agent_shadow_tree: typing.Optional[bool] = None 79 | ) -> typing.Tuple[typing.List[DOMNode], typing.List[LayoutTreeNode], typing.List[ComputedStyle]]: 80 | r''' 81 | Returns a document snapshot, including the full DOM tree of the root node (including iframes, 82 | template contents, and imported documents) in a flattened array, as well as layout and 83 | white-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is 84 | flattened. 85 | 86 | .. deprecated:: 1.3 87 | 88 | :param computed_style_whitelist: Whitelist of computed styles to return. 89 | :param include_event_listeners: *(Optional)* Whether or not to retrieve details of DOM listeners (default false). 90 | :param include_paint_order: *(Optional)* Whether to determine and include the paint order index of LayoutTreeNodes (default false). 91 | :param include_user_agent_shadow_tree: *(Optional)* Whether to include UA shadow tree in the snapshot (default false). 92 | :returns: A tuple with the following items: 93 | 94 | 0. **domNodes** - The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document. 95 | 1. **layoutTreeNodes** - The nodes in the layout tree. 96 | 2. **computedStyles** - Whitelisted ComputedStyle properties for each node in the layout tree. 97 | 98 | .. deprecated:: 1.3 99 | ''' 100 | session = get_session_context('dom_snapshot.get_snapshot') 101 | return await session.execute(cdp.dom_snapshot.get_snapshot(computed_style_whitelist, include_event_listeners, include_paint_order, include_user_agent_shadow_tree)) 102 | -------------------------------------------------------------------------------- /.github/workflows/auto-copilot-org-playwright-loop.yaml: -------------------------------------------------------------------------------- 1 | name: "Org-wide: Copilot Playwright Test, Review, Auto-fix, PR, Merge" 2 | uto-amazonq-review.properties.json 3 | 4 | uto-amazonq-review.properties.json 5 | on: 6 | uto-amazonq-review.properties.json 7 | push: 8 | uto-amazonq-review.properties.json 9 | branches: 10 | uto-amazonq-review.properties.json 11 | - main 12 | uto-amazonq-review.properties.json 13 | - master 14 | uto-amazonq-review.properties.json 15 | 16 | uto-amazonq-review.properties.json 17 | jobs: 18 | uto-amazonq-review.properties.json 19 | playwright-review-fix: 20 | uto-amazonq-review.properties.json 21 | runs-on: self-hosted 22 | uto-amazonq-review.properties.json 23 | steps: 24 | uto-amazonq-review.properties.json 25 | # Checkout repository code 26 | uto-amazonq-review.properties.json 27 | - name: Checkout code 28 | uto-amazonq-review.properties.json 29 | uses: actions/checkout@main 30 | uto-amazonq-review.properties.json 31 | 32 | uto-amazonq-review.properties.json 33 | # Set up Python (change/add for other stacks!) 34 | uto-amazonq-review.properties.json 35 | - name: Setup Python 36 | uto-amazonq-review.properties.json 37 | uses: actions/setup-python@main 38 | uto-amazonq-review.properties.json 39 | with: 40 | uto-amazonq-review.properties.json 41 | python-version: "3.11" 42 | uto-amazonq-review.properties.json 43 | 44 | uto-amazonq-review.properties.json 45 | # Install dependencies (Python example) 46 | uto-amazonq-review.properties.json 47 | - name: Install dependencies 48 | uto-amazonq-review.properties.json 49 | run: | 50 | uto-amazonq-review.properties.json 51 | pip install -r requirements.txt 52 | uto-amazonq-review.properties.json 53 | pip install pytest playwright pytest-playwright 54 | uto-amazonq-review.properties.json 55 | 56 | uto-amazonq-review.properties.json 57 | # Install Playwright browsers 58 | uto-amazonq-review.properties.json 59 | - name: Install Playwright browsers 60 | uto-amazonq-review.properties.json 61 | run: | 62 | uto-amazonq-review.properties.json 63 | python -m playwright install 64 | uto-amazonq-review.properties.json 65 | 66 | uto-amazonq-review.properties.json 67 | # Run Playwright tests 68 | uto-amazonq-review.properties.json 69 | - name: Run Playwright Tests 70 | uto-amazonq-review.properties.json 71 | run: | 72 | uto-amazonq-review.properties.json 73 | pytest tests/ || exit 1 74 | uto-amazonq-review.properties.json 75 | continue-on-error: true 76 | uto-amazonq-review.properties.json 77 | 78 | uto-amazonq-review.properties.json 79 | # Copilot PR Agent auto-review (if available for org) 80 | uto-amazonq-review.properties.json 81 | - name: Copilot PR Agent Review 82 | uto-amazonq-review.properties.json 83 | uses: github/copilot-agent/pr@main 84 | uto-amazonq-review.properties.json 85 | with: 86 | uto-amazonq-review.properties.json 87 | github-token: ${{ secrets.GITHUB_TOKEN }} 88 | uto-amazonq-review.properties.json 89 | continue-on-error: true 90 | uto-amazonq-review.properties.json 91 | 92 | uto-amazonq-review.properties.json 93 | # Copilot Agent auto-fix (can loop up to N attempts if tests fail) 94 | uto-amazonq-review.properties.json 95 | - name: Copilot Auto-fix Failing Playwright Tests 96 | uto-amazonq-review.properties.json 97 | uses: github/copilot-agent/fix@main 98 | uto-amazonq-review.properties.json 99 | with: 100 | uto-amazonq-review.properties.json 101 | github-token: ${{ secrets.GITHUB_TOKEN }} 102 | uto-amazonq-review.properties.json 103 | max_attempts: 3 # Try up to 3 auto-fix loops! 104 | uto-amazonq-review.properties.json 105 | continue-on-error: true 106 | uto-amazonq-review.properties.json 107 | 108 | uto-amazonq-review.properties.json 109 | # Create PR with fixes (if any) 110 | uto-amazonq-review.properties.json 111 | - name: Create Pull Request for Automated Fixes 112 | uto-amazonq-review.properties.json 113 | uses: peter-evans/create-pull-request@main 114 | uto-amazonq-review.properties.json 115 | with: 116 | uto-amazonq-review.properties.json 117 | branch: "copilot/playwright-fixes" 118 | uto-amazonq-review.properties.json 119 | title: "Copilot: Auto-fix Playwright Tests" 120 | uto-amazonq-review.properties.json 121 | body: "Automated Playwright test fixes by Copilot Agent." 122 | uto-amazonq-review.properties.json 123 | commit-message: "Copilot agent Playwright bugfixes" 124 | uto-amazonq-review.properties.json 125 | continue-on-error: true 126 | uto-amazonq-review.properties.json 127 | 128 | uto-amazonq-review.properties.json 129 | # Automerge PR if passing 130 | uto-amazonq-review.properties.json 131 | - name: Automerge PR if checks pass 132 | uto-amazonq-review.properties.json 133 | uses: pascalgn/automerge-action@main 134 | uto-amazonq-review.properties.json 135 | with: 136 | uto-amazonq-review.properties.json 137 | merge-method: squash 138 | uto-amazonq-review.properties.json 139 | github-token: ${{ secrets.GITHUB_TOKEN }} 140 | uto-amazonq-review.properties.json 141 | continue-on-error: true 142 | uto-amazonq-review.properties.json 143 | -------------------------------------------------------------------------------- /examples/notebook.ipynb: -------------------------------------------------------------------------------- 1 | { 2 | "cells": [ 3 | { 4 | "cell_type": "markdown", 5 | "metadata": {}, 6 | "source": [ 7 | "# Using Trio CDP Inside Jupyter Notebook\n", 8 | "\n", 9 | "Trio CDP can be used in Jupyter notebook, but some caveats are warranted. First, Trio support in Jupyter is experimental. Read [the instructions](https://github.com/ipython/ipykernel/pull/479) for setting up Trio inside Jupyter.\n", 10 | "\n", 11 | "The second caveat is that Trio CDP uses [context variables](https://docs.python.org/3.7/library/contextvars.html?highlight=contextvar#contextvars.ContextVar) to keep track of which connection and/or session are associated with each task. In Jupyter notebook, however, each cell executes in a separate task, so connections and sessions are not automatically shared between cells. This notebook contains a workaround that allows a single connection and session to be shared across cells." 12 | ] 13 | }, 14 | { 15 | "cell_type": "code", 16 | "execution_count": 1, 17 | "metadata": {}, 18 | "outputs": [], 19 | "source": [ 20 | "from trio_cdp import connect_cdp, dom, page, target" 21 | ] 22 | }, 23 | { 24 | "cell_type": "code", 25 | "execution_count": 2, 26 | "metadata": {}, 27 | "outputs": [], 28 | "source": [ 29 | "conn = await connect_cdp(GLOBAL_NURSERY,\n", 30 | " 'ws://127.0.0.1:9000/devtools/browser/f01f56ba-993e-4c83-adc0-10a1feb56449')" 31 | ] 32 | }, 33 | { 34 | "cell_type": "code", 35 | "execution_count": 3, 36 | "metadata": {}, 37 | "outputs": [], 38 | "source": [ 39 | "# Here is where we have to do some magic to make the connection from the\n", 40 | "# previous cell available to other cells.\n", 41 | "import trio_cdp.context\n", 42 | "trio_cdp.context.set_global_connection(conn)" 43 | ] 44 | }, 45 | { 46 | "cell_type": "code", 47 | "execution_count": 4, 48 | "metadata": {}, 49 | "outputs": [], 50 | "source": [ 51 | "# Now we can run Trio CDP commands and they execute on the connection\n", 52 | "# automatically.\n", 53 | "targets = await target.get_targets()" 54 | ] 55 | }, 56 | { 57 | "cell_type": "code", 58 | "execution_count": 5, 59 | "metadata": {}, 60 | "outputs": [ 61 | { 62 | "data": { 63 | "text/plain": [ 64 | "[TargetInfo(target_id=TargetID('0AF1752B5834E8ED9F36D7BC3D1AEF51'), type='page', title='Hyperion Gray', url='https://www.hyperiongray.com/', attached=False, opener_id=None, browser_context_id=BrowserContextID('B2A138B23272D6E4920555C2DE424E05')),\n", 65 | " TargetInfo(target_id=TargetID('A0CF2CC043BDF2E4F8F8C0514B9A2FD2'), type='page', title='Hyperion Gray', url='https://www.hyperiongray.com/', attached=False, opener_id=None, browser_context_id=BrowserContextID('B2A138B23272D6E4920555C2DE424E05'))]" 66 | ] 67 | }, 68 | "execution_count": 5, 69 | "metadata": {}, 70 | "output_type": "execute_result" 71 | } 72 | ], 73 | "source": [ 74 | "targets" 75 | ] 76 | }, 77 | { 78 | "cell_type": "code", 79 | "execution_count": 6, 80 | "metadata": {}, 81 | "outputs": [], 82 | "source": [ 83 | "session = await conn.connect_session(targets[0].target_id)" 84 | ] 85 | }, 86 | { 87 | "cell_type": "code", 88 | "execution_count": 7, 89 | "metadata": {}, 90 | "outputs": [], 91 | "source": [ 92 | "# We have to do something similar for the session so that it can be\n", 93 | "# reused across multiple cells.\n", 94 | "trio_cdp.context.set_global_session(session)" 95 | ] 96 | }, 97 | { 98 | "cell_type": "code", 99 | "execution_count": 8, 100 | "metadata": {}, 101 | "outputs": [], 102 | "source": [ 103 | "async with session.page_enable():\n", 104 | " async with session.wait_for(page.LoadEventFired):\n", 105 | " await page.navigate('https://www.hyperiongray.com')" 106 | ] 107 | }, 108 | { 109 | "cell_type": "code", 110 | "execution_count": 9, 111 | "metadata": {}, 112 | "outputs": [], 113 | "source": [ 114 | "doc = await dom.get_document()" 115 | ] 116 | }, 117 | { 118 | "cell_type": "code", 119 | "execution_count": 10, 120 | "metadata": {}, 121 | "outputs": [], 122 | "source": [ 123 | "title = await dom.query_selector(doc.node_id, 'title')" 124 | ] 125 | }, 126 | { 127 | "cell_type": "code", 128 | "execution_count": 11, 129 | "metadata": {}, 130 | "outputs": [ 131 | { 132 | "data": { 133 | "text/plain": [ 134 | "'Hyperion Gray'" 135 | ] 136 | }, 137 | "execution_count": 11, 138 | "metadata": {}, 139 | "output_type": "execute_result" 140 | } 141 | ], 142 | "source": [ 143 | "await dom.get_outer_html(title)" 144 | ] 145 | } 146 | ], 147 | "metadata": { 148 | "kernelspec": { 149 | "display_name": "Python 3 Trio", 150 | "language": "python", 151 | "name": "python3-trio" 152 | }, 153 | "language_info": { 154 | "codemirror_mode": { 155 | "name": "ipython", 156 | "version": 3 157 | }, 158 | "file_extension": ".py", 159 | "mimetype": "text/x-python", 160 | "name": "python", 161 | "nbconvert_exporter": "python", 162 | "pygments_lexer": "ipython3", 163 | "version": "3.7.5" 164 | } 165 | }, 166 | "nbformat": 4, 167 | "nbformat_minor": 4 168 | } 169 | -------------------------------------------------------------------------------- /trio_cdp/generated/web_authn.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.web_authn 12 | from cdp.web_authn import ( 13 | AuthenticatorId, 14 | AuthenticatorProtocol, 15 | AuthenticatorTransport, 16 | Credential, 17 | Ctap2Version, 18 | VirtualAuthenticatorOptions 19 | ) 20 | 21 | 22 | async def add_credential( 23 | authenticator_id: AuthenticatorId, 24 | credential: Credential 25 | ) -> None: 26 | r''' 27 | Adds the credential to the specified authenticator. 28 | 29 | :param authenticator_id: 30 | :param credential: 31 | ''' 32 | session = get_session_context('web_authn.add_credential') 33 | return await session.execute(cdp.web_authn.add_credential(authenticator_id, credential)) 34 | 35 | 36 | async def add_virtual_authenticator( 37 | options: VirtualAuthenticatorOptions 38 | ) -> AuthenticatorId: 39 | r''' 40 | Creates and adds a virtual authenticator. 41 | 42 | :param options: 43 | :returns: 44 | ''' 45 | session = get_session_context('web_authn.add_virtual_authenticator') 46 | return await session.execute(cdp.web_authn.add_virtual_authenticator(options)) 47 | 48 | 49 | async def clear_credentials( 50 | authenticator_id: AuthenticatorId 51 | ) -> None: 52 | r''' 53 | Clears all the credentials from the specified device. 54 | 55 | :param authenticator_id: 56 | ''' 57 | session = get_session_context('web_authn.clear_credentials') 58 | return await session.execute(cdp.web_authn.clear_credentials(authenticator_id)) 59 | 60 | 61 | async def disable() -> None: 62 | r''' 63 | Disable the WebAuthn domain. 64 | ''' 65 | session = get_session_context('web_authn.disable') 66 | return await session.execute(cdp.web_authn.disable()) 67 | 68 | 69 | async def enable() -> None: 70 | r''' 71 | Enable the WebAuthn domain and start intercepting credential storage and 72 | retrieval with a virtual authenticator. 73 | ''' 74 | session = get_session_context('web_authn.enable') 75 | return await session.execute(cdp.web_authn.enable()) 76 | 77 | 78 | async def get_credential( 79 | authenticator_id: AuthenticatorId, 80 | credential_id: str 81 | ) -> Credential: 82 | r''' 83 | Returns a single credential stored in the given virtual authenticator that 84 | matches the credential ID. 85 | 86 | :param authenticator_id: 87 | :param credential_id: 88 | :returns: 89 | ''' 90 | session = get_session_context('web_authn.get_credential') 91 | return await session.execute(cdp.web_authn.get_credential(authenticator_id, credential_id)) 92 | 93 | 94 | async def get_credentials( 95 | authenticator_id: AuthenticatorId 96 | ) -> typing.List[Credential]: 97 | r''' 98 | Returns all the credentials stored in the given virtual authenticator. 99 | 100 | :param authenticator_id: 101 | :returns: 102 | ''' 103 | session = get_session_context('web_authn.get_credentials') 104 | return await session.execute(cdp.web_authn.get_credentials(authenticator_id)) 105 | 106 | 107 | async def remove_credential( 108 | authenticator_id: AuthenticatorId, 109 | credential_id: str 110 | ) -> None: 111 | r''' 112 | Removes a credential from the authenticator. 113 | 114 | :param authenticator_id: 115 | :param credential_id: 116 | ''' 117 | session = get_session_context('web_authn.remove_credential') 118 | return await session.execute(cdp.web_authn.remove_credential(authenticator_id, credential_id)) 119 | 120 | 121 | async def remove_virtual_authenticator( 122 | authenticator_id: AuthenticatorId 123 | ) -> None: 124 | r''' 125 | Removes the given authenticator. 126 | 127 | :param authenticator_id: 128 | ''' 129 | session = get_session_context('web_authn.remove_virtual_authenticator') 130 | return await session.execute(cdp.web_authn.remove_virtual_authenticator(authenticator_id)) 131 | 132 | 133 | async def set_automatic_presence_simulation( 134 | authenticator_id: AuthenticatorId, 135 | enabled: bool 136 | ) -> None: 137 | r''' 138 | Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator. 139 | The default is true. 140 | 141 | :param authenticator_id: 142 | :param enabled: 143 | ''' 144 | session = get_session_context('web_authn.set_automatic_presence_simulation') 145 | return await session.execute(cdp.web_authn.set_automatic_presence_simulation(authenticator_id, enabled)) 146 | 147 | 148 | async def set_user_verified( 149 | authenticator_id: AuthenticatorId, 150 | is_user_verified: bool 151 | ) -> None: 152 | r''' 153 | Sets whether User Verification succeeds or fails for an authenticator. 154 | The default is true. 155 | 156 | :param authenticator_id: 157 | :param is_user_verified: 158 | ''' 159 | session = get_session_context('web_authn.set_user_verified') 160 | return await session.execute(cdp.web_authn.set_user_verified(authenticator_id, is_user_verified)) 161 | -------------------------------------------------------------------------------- /trio_cdp/generated/profiler.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.profiler 12 | from cdp.profiler import ( 13 | ConsoleProfileFinished, 14 | ConsoleProfileStarted, 15 | CoverageRange, 16 | FunctionCoverage, 17 | PositionTickInfo, 18 | PreciseCoverageDeltaUpdate, 19 | Profile, 20 | ProfileNode, 21 | ScriptCoverage, 22 | ScriptTypeProfile, 23 | TypeObject, 24 | TypeProfileEntry 25 | ) 26 | 27 | 28 | async def disable() -> None: 29 | session = get_session_context('profiler.disable') 30 | return await session.execute(cdp.profiler.disable()) 31 | 32 | 33 | async def enable() -> None: 34 | session = get_session_context('profiler.enable') 35 | return await session.execute(cdp.profiler.enable()) 36 | 37 | 38 | async def get_best_effort_coverage() -> typing.List[ScriptCoverage]: 39 | r''' 40 | Collect coverage data for the current isolate. The coverage data may be incomplete due to 41 | garbage collection. 42 | 43 | :returns: Coverage data for the current isolate. 44 | ''' 45 | session = get_session_context('profiler.get_best_effort_coverage') 46 | return await session.execute(cdp.profiler.get_best_effort_coverage()) 47 | 48 | 49 | async def set_sampling_interval( 50 | interval: int 51 | ) -> None: 52 | r''' 53 | Changes CPU profiler sampling interval. Must be called before CPU profiles recording started. 54 | 55 | :param interval: New sampling interval in microseconds. 56 | ''' 57 | session = get_session_context('profiler.set_sampling_interval') 58 | return await session.execute(cdp.profiler.set_sampling_interval(interval)) 59 | 60 | 61 | async def start() -> None: 62 | session = get_session_context('profiler.start') 63 | return await session.execute(cdp.profiler.start()) 64 | 65 | 66 | async def start_precise_coverage( 67 | call_count: typing.Optional[bool] = None, 68 | detailed: typing.Optional[bool] = None, 69 | allow_triggered_updates: typing.Optional[bool] = None 70 | ) -> float: 71 | r''' 72 | Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code 73 | coverage may be incomplete. Enabling prevents running optimized code and resets execution 74 | counters. 75 | 76 | :param call_count: *(Optional)* Collect accurate call counts beyond simple 'covered' or 'not covered'. 77 | :param detailed: *(Optional)* Collect block-based coverage. 78 | :param allow_triggered_updates: *(Optional)* Allow the backend to send updates on its own initiative 79 | :returns: Monotonically increasing time (in seconds) when the coverage update was taken in the backend. 80 | ''' 81 | session = get_session_context('profiler.start_precise_coverage') 82 | return await session.execute(cdp.profiler.start_precise_coverage(call_count, detailed, allow_triggered_updates)) 83 | 84 | 85 | async def start_type_profile() -> None: 86 | r''' 87 | Enable type profile. 88 | 89 | **EXPERIMENTAL** 90 | ''' 91 | session = get_session_context('profiler.start_type_profile') 92 | return await session.execute(cdp.profiler.start_type_profile()) 93 | 94 | 95 | async def stop() -> Profile: 96 | r''' 97 | 98 | 99 | :returns: Recorded profile. 100 | ''' 101 | session = get_session_context('profiler.stop') 102 | return await session.execute(cdp.profiler.stop()) 103 | 104 | 105 | async def stop_precise_coverage() -> None: 106 | r''' 107 | Disable precise code coverage. Disabling releases unnecessary execution count records and allows 108 | executing optimized code. 109 | ''' 110 | session = get_session_context('profiler.stop_precise_coverage') 111 | return await session.execute(cdp.profiler.stop_precise_coverage()) 112 | 113 | 114 | async def stop_type_profile() -> None: 115 | r''' 116 | Disable type profile. Disabling releases type profile data collected so far. 117 | 118 | **EXPERIMENTAL** 119 | ''' 120 | session = get_session_context('profiler.stop_type_profile') 121 | return await session.execute(cdp.profiler.stop_type_profile()) 122 | 123 | 124 | async def take_precise_coverage() -> typing.Tuple[typing.List[ScriptCoverage], float]: 125 | r''' 126 | Collect coverage data for the current isolate, and resets execution counters. Precise code 127 | coverage needs to have started. 128 | 129 | :returns: A tuple with the following items: 130 | 131 | 0. **result** - Coverage data for the current isolate. 132 | 1. **timestamp** - Monotonically increasing time (in seconds) when the coverage update was taken in the backend. 133 | ''' 134 | session = get_session_context('profiler.take_precise_coverage') 135 | return await session.execute(cdp.profiler.take_precise_coverage()) 136 | 137 | 138 | async def take_type_profile() -> typing.List[ScriptTypeProfile]: 139 | r''' 140 | Collect type profile. 141 | 142 | **EXPERIMENTAL** 143 | 144 | :returns: Type profile for all scripts since startTypeProfile() was turned on. 145 | ''' 146 | session = get_session_context('profiler.take_type_profile') 147 | return await session.execute(cdp.profiler.take_type_profile()) 148 | -------------------------------------------------------------------------------- /trio_cdp/generated/layer_tree.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.layer_tree 12 | from cdp.layer_tree import ( 13 | Layer, 14 | LayerId, 15 | LayerPainted, 16 | LayerTreeDidChange, 17 | PaintProfile, 18 | PictureTile, 19 | ScrollRect, 20 | SnapshotId, 21 | StickyPositionConstraint 22 | ) 23 | 24 | 25 | async def compositing_reasons( 26 | layer_id: LayerId 27 | ) -> typing.Tuple[typing.List[str], typing.List[str]]: 28 | r''' 29 | Provides the reasons why the given layer was composited. 30 | 31 | :param layer_id: The id of the layer for which we want to get the reasons it was composited. 32 | :returns: A tuple with the following items: 33 | 34 | 0. **compositingReasons** - A list of strings specifying reasons for the given layer to become composited. 35 | 1. **compositingReasonIds** - A list of strings specifying reason IDs for the given layer to become composited. 36 | ''' 37 | session = get_session_context('layer_tree.compositing_reasons') 38 | return await session.execute(cdp.layer_tree.compositing_reasons(layer_id)) 39 | 40 | 41 | async def disable() -> None: 42 | r''' 43 | Disables compositing tree inspection. 44 | ''' 45 | session = get_session_context('layer_tree.disable') 46 | return await session.execute(cdp.layer_tree.disable()) 47 | 48 | 49 | async def enable() -> None: 50 | r''' 51 | Enables compositing tree inspection. 52 | ''' 53 | session = get_session_context('layer_tree.enable') 54 | return await session.execute(cdp.layer_tree.enable()) 55 | 56 | 57 | async def load_snapshot( 58 | tiles: typing.List[PictureTile] 59 | ) -> SnapshotId: 60 | r''' 61 | Returns the snapshot identifier. 62 | 63 | :param tiles: An array of tiles composing the snapshot. 64 | :returns: The id of the snapshot. 65 | ''' 66 | session = get_session_context('layer_tree.load_snapshot') 67 | return await session.execute(cdp.layer_tree.load_snapshot(tiles)) 68 | 69 | 70 | async def make_snapshot( 71 | layer_id: LayerId 72 | ) -> SnapshotId: 73 | r''' 74 | Returns the layer snapshot identifier. 75 | 76 | :param layer_id: The id of the layer. 77 | :returns: The id of the layer snapshot. 78 | ''' 79 | session = get_session_context('layer_tree.make_snapshot') 80 | return await session.execute(cdp.layer_tree.make_snapshot(layer_id)) 81 | 82 | 83 | async def profile_snapshot( 84 | snapshot_id: SnapshotId, 85 | min_repeat_count: typing.Optional[int] = None, 86 | min_duration: typing.Optional[float] = None, 87 | clip_rect: typing.Optional[cdp.dom.Rect] = None 88 | ) -> typing.List[PaintProfile]: 89 | r''' 90 | :param snapshot_id: The id of the layer snapshot. 91 | :param min_repeat_count: *(Optional)* The maximum number of times to replay the snapshot (1, if not specified). 92 | :param min_duration: *(Optional)* The minimum duration (in seconds) to replay the snapshot. 93 | :param clip_rect: *(Optional)* The clip rectangle to apply when replaying the snapshot. 94 | :returns: The array of paint profiles, one per run. 95 | ''' 96 | session = get_session_context('layer_tree.profile_snapshot') 97 | return await session.execute(cdp.layer_tree.profile_snapshot(snapshot_id, min_repeat_count, min_duration, clip_rect)) 98 | 99 | 100 | async def release_snapshot( 101 | snapshot_id: SnapshotId 102 | ) -> None: 103 | r''' 104 | Releases layer snapshot captured by the back-end. 105 | 106 | :param snapshot_id: The id of the layer snapshot. 107 | ''' 108 | session = get_session_context('layer_tree.release_snapshot') 109 | return await session.execute(cdp.layer_tree.release_snapshot(snapshot_id)) 110 | 111 | 112 | async def replay_snapshot( 113 | snapshot_id: SnapshotId, 114 | from_step: typing.Optional[int] = None, 115 | to_step: typing.Optional[int] = None, 116 | scale: typing.Optional[float] = None 117 | ) -> str: 118 | r''' 119 | Replays the layer snapshot and returns the resulting bitmap. 120 | 121 | :param snapshot_id: The id of the layer snapshot. 122 | :param from_step: *(Optional)* The first step to replay from (replay from the very start if not specified). 123 | :param to_step: *(Optional)* The last step to replay to (replay till the end if not specified). 124 | :param scale: *(Optional)* The scale to apply while replaying (defaults to 1). 125 | :returns: A data: URL for resulting image. 126 | ''' 127 | session = get_session_context('layer_tree.replay_snapshot') 128 | return await session.execute(cdp.layer_tree.replay_snapshot(snapshot_id, from_step, to_step, scale)) 129 | 130 | 131 | async def snapshot_command_log( 132 | snapshot_id: SnapshotId 133 | ) -> typing.List[dict]: 134 | r''' 135 | Replays the layer snapshot and returns canvas log. 136 | 137 | :param snapshot_id: The id of the layer snapshot. 138 | :returns: The array of canvas function calls. 139 | ''' 140 | session = get_session_context('layer_tree.snapshot_command_log') 141 | return await session.execute(cdp.layer_tree.snapshot_command_log(snapshot_id)) 142 | -------------------------------------------------------------------------------- /docs/getting_started.rst: -------------------------------------------------------------------------------- 1 | Getting Started 2 | =============== 3 | 4 | .. highlight:: python 5 | 6 | The following example shows how to connect to browser, navigate to a specified web page, 7 | and then extract the page title. 8 | 9 | .. code:: 10 | 11 | from trio_cdp import open_cdp, page, dom 12 | 13 | async with open_cdp(cdp_url) as conn: 14 | # Find the first available target (usually a browser tab). 15 | targets = await target.get_targets() 16 | target_id = targets[0].id 17 | 18 | # Create a new session with the chosen target. 19 | async with conn.open_session(target_id) as session: 20 | 21 | # Navigate to a website. 22 | async with session.page_enable() 23 | async with session.wait_for(page.LoadEventFired): 24 | await session.execute(page.navigate(target_url)) 25 | 26 | # Extract the page title. 27 | root_node = await session.execute(dom.get_document()) 28 | title_node_id = await session.execute(dom.query_selector(root_node.node_id, 29 | 'title')) 30 | html = await session.execute(dom.get_outer_html(title_node_id)) 31 | print(html) 32 | 33 | We'll go through this example bit by bit. First, it starts with a context 34 | manager. 35 | 36 | .. code:: 37 | 38 | async with open_cdp(cdp_url) as conn: 39 | ... 40 | 41 | This context manager opens a connection to the browser when the block is entered and 42 | closes the connection automatically when the block exits. 43 | 44 | Although we are now connected to the browser, The browser has multiple targets that can 45 | be operated independently. For example, each browser tab is a separate target. In order 46 | to interact with one of them, we have to create a session for it. 47 | 48 | .. code:: 49 | 50 | # Find the first available target (usually a browser tab). 51 | targets = await target.get_targets() 52 | target_id = targets[0].id 53 | 54 | The first line here executes the ``get_targets()`` command in the browser. Trio CDP 55 | multiplexes commands and responses on a single connection, so we can send commands from 56 | multiple Trio tasks, and the responses will be routed back to the correct task. 57 | 58 | .. note:: 59 | 60 | We didn't actually specify *which* connection to run the 61 | ``get_targets()`` command on. The ``async with open_cdp(...)`` context manager sets the 62 | connection as the default connection for all commands executed within this Trio task. 63 | When you run any CDP command inside this task, it will automatically be sent on that 64 | connection. 65 | 66 | The command returns a list of ``TargetInfo`` objects. We grab the first object and 67 | extract its ``target_id``. 68 | 69 | .. code:: 70 | 71 | # Create a new session with the chosen target. 72 | async with conn.open_session(target_id) as session: 73 | ... 74 | 75 | In order to connect to a target, we open a session based on the target ID. As with the 76 | connection, we do this inside a context manager so that the session is automatically 77 | cleaned up for us when we are done with it. 78 | 79 | .. code:: 80 | 81 | async with session.page_enable(): 82 | async with session.wait_for(page.LoadEventFired): 83 | await page.navigate(target_url) 84 | 85 | This code block is where we actually start to manipulate the browser, but there's a lot 86 | going on here so we'll take it one line at time, starting from the bottom and moving 87 | upward. 88 | 89 | On the last line, we call ``await page.navigate(...)`` to tell the browser to go to a 90 | specific URL. However, we don't want our script to continue executing until the browser 91 | actually finishes loading this new page. For example, if we try to extract the page 92 | title before the page has loaded, it will fail! 93 | 94 | The solution is to wait for the browser to send us an event indicating that the page is 95 | loaded. In this case, we want to wait for ``page.LoadEventFired``. The ``async with 96 | session.wait_for(...)`` block means that the block will not exit until the event has 97 | been received. 98 | 99 | However, the browser does not send page events unless we enable them. In raw CDP there 100 | are commands ``page.enable`` and ``page.disable`` that turn page events on and off, 101 | respectively. But in Trio CDP, we make this a little cleaner by once again using a 102 | context manager. The ``async with session.page_enable()`` block will turn on page 103 | events, run the code inside, and then turn off page events automatically. 104 | 105 | .. note:: 106 | 107 | If another task is using page events concurrently, the context manager is smart enough 108 | not to disable page events until all tasks are finished with it. 109 | 110 | .. code:: 111 | 112 | root_node = await dom.get_document() 113 | title_node_id = await dom.query_selector(root_node.node_id, 'title') 114 | html = await dom.get_outer_html(title_node_id) 115 | print(html) 116 | 117 | The last part of the script navigates the DOM to find the ```` element. 118 | First we get the document's root node, then we query for a CSS selector, then 119 | we get the outer HTML of the node. This snippet shows some new APIs, but the 120 | mechanics of sending commands and getting responses are the same as the previous 121 | snippets. 122 | 123 | A more complete version of this example can be found in ``examples/get_title.py`` in 124 | the repository. There is also a screenshot example in ``examples/screenshot.py``. The 125 | unit tests in ``tests/`` also provide some helpful sample code. 126 | -------------------------------------------------------------------------------- /trio_cdp/generated/indexed_db.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.indexed_db 12 | from cdp.indexed_db import ( 13 | DataEntry, 14 | DatabaseWithObjectStores, 15 | Key, 16 | KeyPath, 17 | KeyRange, 18 | ObjectStore, 19 | ObjectStoreIndex 20 | ) 21 | 22 | 23 | async def clear_object_store( 24 | security_origin: str, 25 | database_name: str, 26 | object_store_name: str 27 | ) -> None: 28 | r''' 29 | Clears all entries from an object store. 30 | 31 | :param security_origin: Security origin. 32 | :param database_name: Database name. 33 | :param object_store_name: Object store name. 34 | ''' 35 | session = get_session_context('indexed_db.clear_object_store') 36 | return await session.execute(cdp.indexed_db.clear_object_store(security_origin, database_name, object_store_name)) 37 | 38 | 39 | async def delete_database( 40 | security_origin: str, 41 | database_name: str 42 | ) -> None: 43 | r''' 44 | Deletes a database. 45 | 46 | :param security_origin: Security origin. 47 | :param database_name: Database name. 48 | ''' 49 | session = get_session_context('indexed_db.delete_database') 50 | return await session.execute(cdp.indexed_db.delete_database(security_origin, database_name)) 51 | 52 | 53 | async def delete_object_store_entries( 54 | security_origin: str, 55 | database_name: str, 56 | object_store_name: str, 57 | key_range: KeyRange 58 | ) -> None: 59 | r''' 60 | Delete a range of entries from an object store 61 | 62 | :param security_origin: 63 | :param database_name: 64 | :param object_store_name: 65 | :param key_range: Range of entry keys to delete 66 | ''' 67 | session = get_session_context('indexed_db.delete_object_store_entries') 68 | return await session.execute(cdp.indexed_db.delete_object_store_entries(security_origin, database_name, object_store_name, key_range)) 69 | 70 | 71 | async def disable() -> None: 72 | r''' 73 | Disables events from backend. 74 | ''' 75 | session = get_session_context('indexed_db.disable') 76 | return await session.execute(cdp.indexed_db.disable()) 77 | 78 | 79 | async def enable() -> None: 80 | r''' 81 | Enables events from backend. 82 | ''' 83 | session = get_session_context('indexed_db.enable') 84 | return await session.execute(cdp.indexed_db.enable()) 85 | 86 | 87 | async def get_metadata( 88 | security_origin: str, 89 | database_name: str, 90 | object_store_name: str 91 | ) -> typing.Tuple[float, float]: 92 | r''' 93 | Gets metadata of an object store 94 | 95 | :param security_origin: Security origin. 96 | :param database_name: Database name. 97 | :param object_store_name: Object store name. 98 | :returns: A tuple with the following items: 99 | 100 | 0. **entriesCount** - the entries count 101 | 1. **keyGeneratorValue** - the current value of key generator, to become the next inserted key into the object store. Valid if objectStore.autoIncrement is true. 102 | ''' 103 | session = get_session_context('indexed_db.get_metadata') 104 | return await session.execute(cdp.indexed_db.get_metadata(security_origin, database_name, object_store_name)) 105 | 106 | 107 | async def request_data( 108 | security_origin: str, 109 | database_name: str, 110 | object_store_name: str, 111 | index_name: str, 112 | skip_count: int, 113 | page_size: int, 114 | key_range: typing.Optional[KeyRange] = None 115 | ) -> typing.Tuple[typing.List[DataEntry], bool]: 116 | r''' 117 | Requests data from object store or index. 118 | 119 | :param security_origin: Security origin. 120 | :param database_name: Database name. 121 | :param object_store_name: Object store name. 122 | :param index_name: Index name, empty string for object store data requests. 123 | :param skip_count: Number of records to skip. 124 | :param page_size: Number of records to fetch. 125 | :param key_range: *(Optional)* Key range. 126 | :returns: A tuple with the following items: 127 | 128 | 0. **objectStoreDataEntries** - Array of object store data entries. 129 | 1. **hasMore** - If true, there are more entries to fetch in the given range. 130 | ''' 131 | session = get_session_context('indexed_db.request_data') 132 | return await session.execute(cdp.indexed_db.request_data(security_origin, database_name, object_store_name, index_name, skip_count, page_size, key_range)) 133 | 134 | 135 | async def request_database( 136 | security_origin: str, 137 | database_name: str 138 | ) -> DatabaseWithObjectStores: 139 | r''' 140 | Requests database with given name in given frame. 141 | 142 | :param security_origin: Security origin. 143 | :param database_name: Database name. 144 | :returns: Database with an array of object stores. 145 | ''' 146 | session = get_session_context('indexed_db.request_database') 147 | return await session.execute(cdp.indexed_db.request_database(security_origin, database_name)) 148 | 149 | 150 | async def request_database_names( 151 | security_origin: str 152 | ) -> typing.List[str]: 153 | r''' 154 | Requests database names for given security origin. 155 | 156 | :param security_origin: Security origin. 157 | :returns: Database names for origin. 158 | ''' 159 | session = get_session_context('indexed_db.request_database_names') 160 | return await session.execute(cdp.indexed_db.request_database_names(security_origin)) 161 | -------------------------------------------------------------------------------- /trio_cdp/generated/dom_debugger.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.dom_debugger 12 | from cdp.dom_debugger import ( 13 | CSPViolationType, 14 | DOMBreakpointType, 15 | EventListener 16 | ) 17 | 18 | 19 | async def get_event_listeners( 20 | object_id: cdp.runtime.RemoteObjectId, 21 | depth: typing.Optional[int] = None, 22 | pierce: typing.Optional[bool] = None 23 | ) -> typing.List[EventListener]: 24 | r''' 25 | Returns event listeners of the given object. 26 | 27 | :param object_id: Identifier of the object to return listeners for. 28 | :param depth: *(Optional)* The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the entire subtree or provide an integer larger than 0. 29 | :param pierce: *(Optional)* Whether or not iframes and shadow roots should be traversed when returning the subtree (default is false). Reports listeners for all contexts if pierce is enabled. 30 | :returns: Array of relevant listeners. 31 | ''' 32 | session = get_session_context('dom_debugger.get_event_listeners') 33 | return await session.execute(cdp.dom_debugger.get_event_listeners(object_id, depth, pierce)) 34 | 35 | 36 | async def remove_dom_breakpoint( 37 | node_id: cdp.dom.NodeId, 38 | type_: DOMBreakpointType 39 | ) -> None: 40 | r''' 41 | Removes DOM breakpoint that was set using ``setDOMBreakpoint``. 42 | 43 | :param node_id: Identifier of the node to remove breakpoint from. 44 | :param type_: Type of the breakpoint to remove. 45 | ''' 46 | session = get_session_context('dom_debugger.remove_dom_breakpoint') 47 | return await session.execute(cdp.dom_debugger.remove_dom_breakpoint(node_id, type_)) 48 | 49 | 50 | async def remove_event_listener_breakpoint( 51 | event_name: str, 52 | target_name: typing.Optional[str] = None 53 | ) -> None: 54 | r''' 55 | Removes breakpoint on particular DOM event. 56 | 57 | :param event_name: Event name. 58 | :param target_name: **(EXPERIMENTAL)** *(Optional)* EventTarget interface name. 59 | ''' 60 | session = get_session_context('dom_debugger.remove_event_listener_breakpoint') 61 | return await session.execute(cdp.dom_debugger.remove_event_listener_breakpoint(event_name, target_name)) 62 | 63 | 64 | async def remove_instrumentation_breakpoint( 65 | event_name: str 66 | ) -> None: 67 | r''' 68 | Removes breakpoint on particular native event. 69 | 70 | **EXPERIMENTAL** 71 | 72 | :param event_name: Instrumentation name to stop on. 73 | ''' 74 | session = get_session_context('dom_debugger.remove_instrumentation_breakpoint') 75 | return await session.execute(cdp.dom_debugger.remove_instrumentation_breakpoint(event_name)) 76 | 77 | 78 | async def remove_xhr_breakpoint( 79 | url: str 80 | ) -> None: 81 | r''' 82 | Removes breakpoint from XMLHttpRequest. 83 | 84 | :param url: Resource URL substring. 85 | ''' 86 | session = get_session_context('dom_debugger.remove_xhr_breakpoint') 87 | return await session.execute(cdp.dom_debugger.remove_xhr_breakpoint(url)) 88 | 89 | 90 | async def set_break_on_csp_violation( 91 | violation_types: typing.List[CSPViolationType] 92 | ) -> None: 93 | r''' 94 | Sets breakpoint on particular CSP violations. 95 | 96 | **EXPERIMENTAL** 97 | 98 | :param violation_types: CSP Violations to stop upon. 99 | ''' 100 | session = get_session_context('dom_debugger.set_break_on_csp_violation') 101 | return await session.execute(cdp.dom_debugger.set_break_on_csp_violation(violation_types)) 102 | 103 | 104 | async def set_dom_breakpoint( 105 | node_id: cdp.dom.NodeId, 106 | type_: DOMBreakpointType 107 | ) -> None: 108 | r''' 109 | Sets breakpoint on particular operation with DOM. 110 | 111 | :param node_id: Identifier of the node to set breakpoint on. 112 | :param type_: Type of the operation to stop upon. 113 | ''' 114 | session = get_session_context('dom_debugger.set_dom_breakpoint') 115 | return await session.execute(cdp.dom_debugger.set_dom_breakpoint(node_id, type_)) 116 | 117 | 118 | async def set_event_listener_breakpoint( 119 | event_name: str, 120 | target_name: typing.Optional[str] = None 121 | ) -> None: 122 | r''' 123 | Sets breakpoint on particular DOM event. 124 | 125 | :param event_name: DOM Event name to stop on (any DOM event will do). 126 | :param target_name: **(EXPERIMENTAL)** *(Optional)* EventTarget interface name to stop on. If equal to ```"*"``` or not provided, will stop on any EventTarget. 127 | ''' 128 | session = get_session_context('dom_debugger.set_event_listener_breakpoint') 129 | return await session.execute(cdp.dom_debugger.set_event_listener_breakpoint(event_name, target_name)) 130 | 131 | 132 | async def set_instrumentation_breakpoint( 133 | event_name: str 134 | ) -> None: 135 | r''' 136 | Sets breakpoint on particular native event. 137 | 138 | **EXPERIMENTAL** 139 | 140 | :param event_name: Instrumentation name to stop on. 141 | ''' 142 | session = get_session_context('dom_debugger.set_instrumentation_breakpoint') 143 | return await session.execute(cdp.dom_debugger.set_instrumentation_breakpoint(event_name)) 144 | 145 | 146 | async def set_xhr_breakpoint( 147 | url: str 148 | ) -> None: 149 | r''' 150 | Sets breakpoint on XMLHttpRequest. 151 | 152 | :param url: Resource URL substring. All XHRs having this substring in the URL will get stopped upon. 153 | ''' 154 | session = get_session_context('dom_debugger.set_xhr_breakpoint') 155 | return await session.execute(cdp.dom_debugger.set_xhr_breakpoint(url)) 156 | -------------------------------------------------------------------------------- /trio_cdp/generated/heap_profiler.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.heap_profiler 12 | from cdp.heap_profiler import ( 13 | AddHeapSnapshotChunk, 14 | HeapSnapshotObjectId, 15 | HeapStatsUpdate, 16 | LastSeenObjectId, 17 | ReportHeapSnapshotProgress, 18 | ResetProfiles, 19 | SamplingHeapProfile, 20 | SamplingHeapProfileNode, 21 | SamplingHeapProfileSample 22 | ) 23 | 24 | 25 | async def add_inspected_heap_object( 26 | heap_object_id: HeapSnapshotObjectId 27 | ) -> None: 28 | r''' 29 | Enables console to refer to the node with given id via $x (see Command Line API for more details 30 | $x functions). 31 | 32 | :param heap_object_id: Heap snapshot object id to be accessible by means of $x command line API. 33 | ''' 34 | session = get_session_context('heap_profiler.add_inspected_heap_object') 35 | return await session.execute(cdp.heap_profiler.add_inspected_heap_object(heap_object_id)) 36 | 37 | 38 | async def collect_garbage() -> None: 39 | session = get_session_context('heap_profiler.collect_garbage') 40 | return await session.execute(cdp.heap_profiler.collect_garbage()) 41 | 42 | 43 | async def disable() -> None: 44 | session = get_session_context('heap_profiler.disable') 45 | return await session.execute(cdp.heap_profiler.disable()) 46 | 47 | 48 | async def enable() -> None: 49 | session = get_session_context('heap_profiler.enable') 50 | return await session.execute(cdp.heap_profiler.enable()) 51 | 52 | 53 | async def get_heap_object_id( 54 | object_id: cdp.runtime.RemoteObjectId 55 | ) -> HeapSnapshotObjectId: 56 | r''' 57 | :param object_id: Identifier of the object to get heap object id for. 58 | :returns: Id of the heap snapshot object corresponding to the passed remote object id. 59 | ''' 60 | session = get_session_context('heap_profiler.get_heap_object_id') 61 | return await session.execute(cdp.heap_profiler.get_heap_object_id(object_id)) 62 | 63 | 64 | async def get_object_by_heap_object_id( 65 | object_id: HeapSnapshotObjectId, 66 | object_group: typing.Optional[str] = None 67 | ) -> cdp.runtime.RemoteObject: 68 | r''' 69 | :param object_id: 70 | :param object_group: *(Optional)* Symbolic group name that can be used to release multiple objects. 71 | :returns: Evaluation result. 72 | ''' 73 | session = get_session_context('heap_profiler.get_object_by_heap_object_id') 74 | return await session.execute(cdp.heap_profiler.get_object_by_heap_object_id(object_id, object_group)) 75 | 76 | 77 | async def get_sampling_profile() -> SamplingHeapProfile: 78 | r''' 79 | 80 | 81 | :returns: Return the sampling profile being collected. 82 | ''' 83 | session = get_session_context('heap_profiler.get_sampling_profile') 84 | return await session.execute(cdp.heap_profiler.get_sampling_profile()) 85 | 86 | 87 | async def start_sampling( 88 | sampling_interval: typing.Optional[float] = None 89 | ) -> None: 90 | r''' 91 | :param sampling_interval: *(Optional)* Average sample interval in bytes. Poisson distribution is used for the intervals. The default value is 32768 bytes. 92 | ''' 93 | session = get_session_context('heap_profiler.start_sampling') 94 | return await session.execute(cdp.heap_profiler.start_sampling(sampling_interval)) 95 | 96 | 97 | async def start_tracking_heap_objects( 98 | track_allocations: typing.Optional[bool] = None 99 | ) -> None: 100 | r''' 101 | :param track_allocations: *(Optional)* 102 | ''' 103 | session = get_session_context('heap_profiler.start_tracking_heap_objects') 104 | return await session.execute(cdp.heap_profiler.start_tracking_heap_objects(track_allocations)) 105 | 106 | 107 | async def stop_sampling() -> SamplingHeapProfile: 108 | r''' 109 | 110 | 111 | :returns: Recorded sampling heap profile. 112 | ''' 113 | session = get_session_context('heap_profiler.stop_sampling') 114 | return await session.execute(cdp.heap_profiler.stop_sampling()) 115 | 116 | 117 | async def stop_tracking_heap_objects( 118 | report_progress: typing.Optional[bool] = None, 119 | treat_global_objects_as_roots: typing.Optional[bool] = None, 120 | capture_numeric_value: typing.Optional[bool] = None 121 | ) -> None: 122 | r''' 123 | :param report_progress: *(Optional)* If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken when the tracking is stopped. 124 | :param treat_global_objects_as_roots: *(Optional)* 125 | :param capture_numeric_value: *(Optional)* If true, numerical values are included in the snapshot 126 | ''' 127 | session = get_session_context('heap_profiler.stop_tracking_heap_objects') 128 | return await session.execute(cdp.heap_profiler.stop_tracking_heap_objects(report_progress, treat_global_objects_as_roots, capture_numeric_value)) 129 | 130 | 131 | async def take_heap_snapshot( 132 | report_progress: typing.Optional[bool] = None, 133 | treat_global_objects_as_roots: typing.Optional[bool] = None, 134 | capture_numeric_value: typing.Optional[bool] = None 135 | ) -> None: 136 | r''' 137 | :param report_progress: *(Optional)* If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken. 138 | :param treat_global_objects_as_roots: *(Optional)* If true, a raw snapshot without artificial roots will be generated 139 | :param capture_numeric_value: *(Optional)* If true, numerical values are included in the snapshot 140 | ''' 141 | session = get_session_context('heap_profiler.take_heap_snapshot') 142 | return await session.execute(cdp.heap_profiler.take_heap_snapshot(report_progress, treat_global_objects_as_roots, capture_numeric_value)) 143 | -------------------------------------------------------------------------------- /trio_cdp/generated/storage.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.storage 12 | from cdp.storage import ( 13 | CacheStorageContentUpdated, 14 | CacheStorageListUpdated, 15 | IndexedDBContentUpdated, 16 | IndexedDBListUpdated, 17 | StorageType, 18 | TrustTokens, 19 | UsageForType 20 | ) 21 | 22 | 23 | async def clear_cookies( 24 | browser_context_id: typing.Optional[cdp.browser.BrowserContextID] = None 25 | ) -> None: 26 | r''' 27 | Clears cookies. 28 | 29 | :param browser_context_id: *(Optional)* Browser context to use when called on the browser endpoint. 30 | ''' 31 | session = get_session_context('storage.clear_cookies') 32 | return await session.execute(cdp.storage.clear_cookies(browser_context_id)) 33 | 34 | 35 | async def clear_data_for_origin( 36 | origin: str, 37 | storage_types: str 38 | ) -> None: 39 | r''' 40 | Clears storage for origin. 41 | 42 | :param origin: Security origin. 43 | :param storage_types: Comma separated list of StorageType to clear. 44 | ''' 45 | session = get_session_context('storage.clear_data_for_origin') 46 | return await session.execute(cdp.storage.clear_data_for_origin(origin, storage_types)) 47 | 48 | 49 | async def clear_trust_tokens( 50 | issuer_origin: str 51 | ) -> bool: 52 | r''' 53 | Removes all Trust Tokens issued by the provided issuerOrigin. 54 | Leaves other stored data, including the issuer's Redemption Records, intact. 55 | 56 | **EXPERIMENTAL** 57 | 58 | :param issuer_origin: 59 | :returns: True if any tokens were deleted, false otherwise. 60 | ''' 61 | session = get_session_context('storage.clear_trust_tokens') 62 | return await session.execute(cdp.storage.clear_trust_tokens(issuer_origin)) 63 | 64 | 65 | async def get_cookies( 66 | browser_context_id: typing.Optional[cdp.browser.BrowserContextID] = None 67 | ) -> typing.List[cdp.network.Cookie]: 68 | r''' 69 | Returns all browser cookies. 70 | 71 | :param browser_context_id: *(Optional)* Browser context to use when called on the browser endpoint. 72 | :returns: Array of cookie objects. 73 | ''' 74 | session = get_session_context('storage.get_cookies') 75 | return await session.execute(cdp.storage.get_cookies(browser_context_id)) 76 | 77 | 78 | async def get_trust_tokens() -> typing.List[TrustTokens]: 79 | r''' 80 | Returns the number of stored Trust Tokens per issuer for the 81 | current browsing context. 82 | 83 | **EXPERIMENTAL** 84 | 85 | :returns: 86 | ''' 87 | session = get_session_context('storage.get_trust_tokens') 88 | return await session.execute(cdp.storage.get_trust_tokens()) 89 | 90 | 91 | async def get_usage_and_quota( 92 | origin: str 93 | ) -> typing.Tuple[float, float, bool, typing.List[UsageForType]]: 94 | r''' 95 | Returns usage and quota in bytes. 96 | 97 | :param origin: Security origin. 98 | :returns: A tuple with the following items: 99 | 100 | 0. **usage** - Storage usage (bytes). 101 | 1. **quota** - Storage quota (bytes). 102 | 2. **overrideActive** - Whether or not the origin has an active storage quota override 103 | 3. **usageBreakdown** - Storage usage per type (bytes). 104 | ''' 105 | session = get_session_context('storage.get_usage_and_quota') 106 | return await session.execute(cdp.storage.get_usage_and_quota(origin)) 107 | 108 | 109 | async def override_quota_for_origin( 110 | origin: str, 111 | quota_size: typing.Optional[float] = None 112 | ) -> None: 113 | r''' 114 | Override quota for the specified origin 115 | 116 | **EXPERIMENTAL** 117 | 118 | :param origin: Security origin. 119 | :param quota_size: *(Optional)* The quota size (in bytes) to override the original quota with. If this is called multiple times, the overridden quota will be equal to the quotaSize provided in the final call. If this is called without specifying a quotaSize, the quota will be reset to the default value for the specified origin. If this is called multiple times with different origins, the override will be maintained for each origin until it is disabled (called without a quotaSize). 120 | ''' 121 | session = get_session_context('storage.override_quota_for_origin') 122 | return await session.execute(cdp.storage.override_quota_for_origin(origin, quota_size)) 123 | 124 | 125 | async def set_cookies( 126 | cookies: typing.List[cdp.network.CookieParam], 127 | browser_context_id: typing.Optional[cdp.browser.BrowserContextID] = None 128 | ) -> None: 129 | r''' 130 | Sets given cookies. 131 | 132 | :param cookies: Cookies to be set. 133 | :param browser_context_id: *(Optional)* Browser context to use when called on the browser endpoint. 134 | ''' 135 | session = get_session_context('storage.set_cookies') 136 | return await session.execute(cdp.storage.set_cookies(cookies, browser_context_id)) 137 | 138 | 139 | async def track_cache_storage_for_origin( 140 | origin: str 141 | ) -> None: 142 | r''' 143 | Registers origin to be notified when an update occurs to its cache storage list. 144 | 145 | :param origin: Security origin. 146 | ''' 147 | session = get_session_context('storage.track_cache_storage_for_origin') 148 | return await session.execute(cdp.storage.track_cache_storage_for_origin(origin)) 149 | 150 | 151 | async def track_indexed_db_for_origin( 152 | origin: str 153 | ) -> None: 154 | r''' 155 | Registers origin to be notified when an update occurs to its IndexedDB. 156 | 157 | :param origin: Security origin. 158 | ''' 159 | session = get_session_context('storage.track_indexed_db_for_origin') 160 | return await session.execute(cdp.storage.track_indexed_db_for_origin(origin)) 161 | 162 | 163 | async def untrack_cache_storage_for_origin( 164 | origin: str 165 | ) -> None: 166 | r''' 167 | Unregisters origin from receiving notifications for cache storage. 168 | 169 | :param origin: Security origin. 170 | ''' 171 | session = get_session_context('storage.untrack_cache_storage_for_origin') 172 | return await session.execute(cdp.storage.untrack_cache_storage_for_origin(origin)) 173 | 174 | 175 | async def untrack_indexed_db_for_origin( 176 | origin: str 177 | ) -> None: 178 | r''' 179 | Unregisters origin from receiving notifications for IndexedDB. 180 | 181 | :param origin: Security origin. 182 | ''' 183 | session = get_session_context('storage.untrack_indexed_db_for_origin') 184 | return await session.execute(cdp.storage.untrack_indexed_db_for_origin(origin)) 185 | -------------------------------------------------------------------------------- /generator/generate.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | import inspect 3 | import pathlib 4 | from textwrap import indent as tw_indent 5 | import types 6 | import typing 7 | 8 | import cdp 9 | 10 | 11 | try: 12 | ForwardRef = typing.ForwardRef # type: ignore 13 | except AttributeError: 14 | ForwardRef = typing._ForwardRef # type: ignore 15 | 16 | 17 | cdp_modules = None 18 | 19 | 20 | def indent(text: str, count: int): 21 | ''' Indent text with the specified number of spaces. ''' 22 | return tw_indent(text, ' ' * count) 23 | 24 | 25 | def main(): 26 | ''' Main entry point. ''' 27 | root = pathlib.Path(__file__).resolve().parent.parent / 'trio_cdp' / 'generated' 28 | clean(root) 29 | ignored = lambda name: name.startswith('_') or name in ('cdp', 'util') 30 | global cdp_modules 31 | cdp_modules = {n:m for n,m in inspect.getmembers(cdp) if not ignored(n)} 32 | for name, module in cdp_modules.items(): 33 | generate_module(root, name, module) 34 | init = root / '__init__.py' 35 | with init.open('w') as file: 36 | file.write('# DO NOT EDIT THIS FILE!\n#\n') 37 | file.write('# This code is generated off of PyCDP modules. If you need to make\n') 38 | file.write('# changes, edit the generator and regenerate all of the modules.\n\n') 39 | for module in cdp_modules: 40 | file.write(f'from . import {module}\n') 41 | 42 | 43 | def clean(root: pathlib.Path): 44 | ''' Remove files from directory. ''' 45 | for path in root.iterdir(): 46 | if path.is_file(): 47 | path.unlink() 48 | 49 | 50 | def generate_module(root: pathlib.Path, module_name: str, 51 | module: types.ModuleType): 52 | ''' Generate code for a module. ''' 53 | print('* Generating module:', module_name) 54 | module_path = root / f'{module_name}.py' 55 | commands = list() 56 | classes = list() 57 | for name, obj in inspect.getmembers(module): 58 | if name.startswith('_') or name in ('dataclass', 'deprecated', 'event_class'): 59 | continue 60 | if inspect.isfunction(obj): 61 | commands.append(generate_command(module, module_name, obj)) 62 | elif inspect.isclass(obj): 63 | classes.append(name) 64 | 65 | with module_path.open('w') as file: 66 | file.write('# DO NOT EDIT THIS FILE!\n#\n') 67 | file.write('# This code is generated off of PyCDP modules. If you need to make\n') 68 | file.write('# changes, edit the generator and regenerate all of the modules.\n\n') 69 | file.write('from __future__ import annotations\n') 70 | file.write('import typing\n\n') 71 | file.write('from ..context import get_connection_context, get_session_context\n\n') 72 | file.write(f'import cdp.{module_name}\n') 73 | if classes: 74 | file.write(f'from cdp.{module_name} import (\n') 75 | file.write(indent(',\n'.join(classes), 4)) 76 | file.write('\n)\n\n\n') 77 | else: 78 | file.write('\n') 79 | file.write('\n\n'.join(commands)) 80 | 81 | 82 | def generate_command(module: types.ModuleType, module_name: str, 83 | fn: types.FunctionType): 84 | ''' Generate code for one command, i.e. one PyCDP wrapper function. ''' 85 | fn_name = fn.__name__ 86 | print(f' - {fn_name}()') 87 | sig = inspect.signature(fn) 88 | type_hints = typing.get_type_hints(fn, globalns=vars(module), localns=None) 89 | 90 | # Generate the argument list. 91 | args = list() 92 | call_args = list() 93 | for param in sig.parameters.values(): 94 | ann = format_annotation(module, type_hints[param.name]) 95 | if param.default != inspect.Parameter.empty: 96 | default_str = f' = {param.default}' 97 | else: 98 | default_str = '' 99 | args.append(f'{param.name}: {ann}{default_str}') 100 | call_args.append(param.name) 101 | 102 | if len(args) == 0: 103 | arg_str = ', '.join(args) 104 | else: 105 | arg_str = indent('\n' + ',\n'.join(args), 8) + '\n ' 106 | 107 | call_arg_str = ', '.join(call_args) 108 | 109 | # Copy docstring. 110 | if fn.__doc__: 111 | doc = " r'''" + fn.__doc__ + "'''\n" 112 | else: 113 | doc = '' 114 | 115 | # The original function returns a generator. We want to grab the return type of the 116 | # generator and set that as the return type of this wrapper function. 117 | return_type = format_annotation(module, type_hints['return'].__args__[2]) 118 | 119 | # Format the function and return it as a string. 120 | ctx_name, ctx_fn = which_context(module_name, fn_name) 121 | body = f"{ctx_name} = {ctx_fn}('{module_name}.{fn_name}')\n" 122 | body += f"return await {ctx_name}.execute(cdp.{module_name}.{fn_name}({call_arg_str}))" 123 | body = indent(body, 4) 124 | return f'async def {fn_name}({arg_str}) -> {return_type}:\n{doc}{body}\n' 125 | 126 | 127 | def format_annotation(current_module: types.ModuleType, ann: typing.Any): 128 | ''' 129 | Given a type annotation, return a stringified version. 130 | 131 | This is ugly since it seems there's not much official tooling for manipulating 132 | types. Many of the types are all of the same class ``typing._GenericAlias``, and we 133 | have to access private members to figure out what the specific annotation actually 134 | is. 135 | ''' 136 | if ann is type(None): 137 | ann_str = 'None' 138 | elif isinstance(ann, type): 139 | if ann.__module__ not in (current_module.__name__, 'builtins'): 140 | ann_str = f'{ann.__module__}.{ann.__name__}' 141 | else: 142 | ann_str = ann.__name__ 143 | elif ann._name == 'Any': 144 | ann_str = 'typing.Any' 145 | elif ann._name == 'List': 146 | nested_ann = format_annotation(current_module, ann.__args__[0]) 147 | ann_str = f'typing.List[{nested_ann}]' 148 | elif ann._name == 'Tuple': 149 | nested_anns = ', '.join(format_annotation(current_module, a) for a in ann.__args__) 150 | ann_str = f'typing.Tuple[{nested_anns}]' 151 | elif ann._name is None and len(ann.__args__) > 1: 152 | # For some reason union annotations don't have a name? 153 | # If the union has two members and one of them is NoneType, then it's really 154 | # a typing.Optional. 155 | if len(ann.__args__) == 2 and any(a is type(None) for a in ann.__args__): 156 | opt_type = [a for a in ann.__args__ if a is not type(None)][0] 157 | nested_ann = format_annotation(current_module, opt_type) 158 | ann_str = f'typing.Optional[{nested_ann}]' 159 | else: 160 | nested_anns = ', '.join(format_annotation(current_module, a) for a in ann.__args__) 161 | ann_str = f'typing.Union[{nested_anns}]' 162 | else: 163 | raise Exception(f'Cannot format annotation: {repr(ann)}') 164 | return ann_str 165 | 166 | 167 | def which_context(module: str, name: str): 168 | ''' 169 | Returns the information for which context to use when executing a particular 170 | function. 171 | 172 | Returns a tuple of the variable name for the context and the function name that 173 | retrieves the context. 174 | ''' 175 | if module == 'target': 176 | context = 'connection', 'get_connection_context' 177 | else: 178 | context = 'session', 'get_session_context' 179 | return context 180 | 181 | 182 | if __name__ == '__main__': 183 | main() 184 | -------------------------------------------------------------------------------- /trio_cdp/generated/accessibility.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.accessibility 12 | from cdp.accessibility import ( 13 | AXNode, 14 | AXNodeId, 15 | AXProperty, 16 | AXPropertyName, 17 | AXRelatedNode, 18 | AXValue, 19 | AXValueNativeSourceType, 20 | AXValueSource, 21 | AXValueSourceType, 22 | AXValueType, 23 | LoadComplete, 24 | NodesUpdated 25 | ) 26 | 27 | 28 | async def disable() -> None: 29 | r''' 30 | Disables the accessibility domain. 31 | ''' 32 | session = get_session_context('accessibility.disable') 33 | return await session.execute(cdp.accessibility.disable()) 34 | 35 | 36 | async def enable() -> None: 37 | r''' 38 | Enables the accessibility domain which causes ``AXNodeId``'s to remain consistent between method calls. 39 | This turns on accessibility for the page, which can impact performance until accessibility is disabled. 40 | ''' 41 | session = get_session_context('accessibility.enable') 42 | return await session.execute(cdp.accessibility.enable()) 43 | 44 | 45 | async def get_ax_node_and_ancestors( 46 | node_id: typing.Optional[cdp.dom.NodeId] = None, 47 | backend_node_id: typing.Optional[cdp.dom.BackendNodeId] = None, 48 | object_id: typing.Optional[cdp.runtime.RemoteObjectId] = None 49 | ) -> typing.List[AXNode]: 50 | r''' 51 | Fetches a node and all ancestors up to and including the root. 52 | Requires ``enable()`` to have been called previously. 53 | 54 | **EXPERIMENTAL** 55 | 56 | :param node_id: *(Optional)* Identifier of the node to get. 57 | :param backend_node_id: *(Optional)* Identifier of the backend node to get. 58 | :param object_id: *(Optional)* JavaScript object id of the node wrapper to get. 59 | :returns: 60 | ''' 61 | session = get_session_context('accessibility.get_ax_node_and_ancestors') 62 | return await session.execute(cdp.accessibility.get_ax_node_and_ancestors(node_id, backend_node_id, object_id)) 63 | 64 | 65 | async def get_child_ax_nodes( 66 | id_: AXNodeId, 67 | frame_id: typing.Optional[cdp.page.FrameId] = None 68 | ) -> typing.List[AXNode]: 69 | r''' 70 | Fetches a particular accessibility node by AXNodeId. 71 | Requires ``enable()`` to have been called previously. 72 | 73 | **EXPERIMENTAL** 74 | 75 | :param id_: 76 | :param frame_id: *(Optional)* The frame in whose document the node resides. If omitted, the root frame is used. 77 | :returns: 78 | ''' 79 | session = get_session_context('accessibility.get_child_ax_nodes') 80 | return await session.execute(cdp.accessibility.get_child_ax_nodes(id_, frame_id)) 81 | 82 | 83 | async def get_full_ax_tree( 84 | depth: typing.Optional[int] = None, 85 | max_depth: typing.Optional[int] = None, 86 | frame_id: typing.Optional[cdp.page.FrameId] = None 87 | ) -> typing.List[AXNode]: 88 | r''' 89 | Fetches the entire accessibility tree for the root Document 90 | 91 | **EXPERIMENTAL** 92 | 93 | :param depth: *(Optional)* The maximum depth at which descendants of the root node should be retrieved. If omitted, the full tree is returned. 94 | :param max_depth: **(DEPRECATED)** *(Optional)* Deprecated. This parameter has been renamed to ```depth```. If depth is not provided, max_depth will be used. 95 | :param frame_id: *(Optional)* The frame for whose document the AX tree should be retrieved. If omited, the root frame is used. 96 | :returns: 97 | ''' 98 | session = get_session_context('accessibility.get_full_ax_tree') 99 | return await session.execute(cdp.accessibility.get_full_ax_tree(depth, max_depth, frame_id)) 100 | 101 | 102 | async def get_partial_ax_tree( 103 | node_id: typing.Optional[cdp.dom.NodeId] = None, 104 | backend_node_id: typing.Optional[cdp.dom.BackendNodeId] = None, 105 | object_id: typing.Optional[cdp.runtime.RemoteObjectId] = None, 106 | fetch_relatives: typing.Optional[bool] = None 107 | ) -> typing.List[AXNode]: 108 | r''' 109 | Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists. 110 | 111 | **EXPERIMENTAL** 112 | 113 | :param node_id: *(Optional)* Identifier of the node to get the partial accessibility tree for. 114 | :param backend_node_id: *(Optional)* Identifier of the backend node to get the partial accessibility tree for. 115 | :param object_id: *(Optional)* JavaScript object id of the node wrapper to get the partial accessibility tree for. 116 | :param fetch_relatives: *(Optional)* Whether to fetch this nodes ancestors, siblings and children. Defaults to true. 117 | :returns: The ``Accessibility.AXNode`` for this DOM node, if it exists, plus its ancestors, siblings and children, if requested. 118 | ''' 119 | session = get_session_context('accessibility.get_partial_ax_tree') 120 | return await session.execute(cdp.accessibility.get_partial_ax_tree(node_id, backend_node_id, object_id, fetch_relatives)) 121 | 122 | 123 | async def get_root_ax_node( 124 | frame_id: typing.Optional[cdp.page.FrameId] = None 125 | ) -> AXNode: 126 | r''' 127 | Fetches the root node. 128 | Requires ``enable()`` to have been called previously. 129 | 130 | **EXPERIMENTAL** 131 | 132 | :param frame_id: *(Optional)* The frame in whose document the node resides. If omitted, the root frame is used. 133 | :returns: 134 | ''' 135 | session = get_session_context('accessibility.get_root_ax_node') 136 | return await session.execute(cdp.accessibility.get_root_ax_node(frame_id)) 137 | 138 | 139 | async def query_ax_tree( 140 | node_id: typing.Optional[cdp.dom.NodeId] = None, 141 | backend_node_id: typing.Optional[cdp.dom.BackendNodeId] = None, 142 | object_id: typing.Optional[cdp.runtime.RemoteObjectId] = None, 143 | accessible_name: typing.Optional[str] = None, 144 | role: typing.Optional[str] = None 145 | ) -> typing.List[AXNode]: 146 | r''' 147 | Query a DOM node's accessibility subtree for accessible name and role. 148 | This command computes the name and role for all nodes in the subtree, including those that are 149 | ignored for accessibility, and returns those that mactch the specified name and role. If no DOM 150 | node is specified, or the DOM node does not exist, the command returns an error. If neither 151 | ``accessibleName`` or ``role`` is specified, it returns all the accessibility nodes in the subtree. 152 | 153 | **EXPERIMENTAL** 154 | 155 | :param node_id: *(Optional)* Identifier of the node for the root to query. 156 | :param backend_node_id: *(Optional)* Identifier of the backend node for the root to query. 157 | :param object_id: *(Optional)* JavaScript object id of the node wrapper for the root to query. 158 | :param accessible_name: *(Optional)* Find nodes with this computed name. 159 | :param role: *(Optional)* Find nodes with this computed role. 160 | :returns: A list of ``Accessibility.AXNode`` matching the specified attributes, including nodes that are ignored for accessibility. 161 | ''' 162 | session = get_session_context('accessibility.query_ax_tree') 163 | return await session.execute(cdp.accessibility.query_ax_tree(node_id, backend_node_id, object_id, accessible_name, role)) 164 | -------------------------------------------------------------------------------- /trio_cdp/generated/fetch.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.fetch 12 | from cdp.fetch import ( 13 | AuthChallenge, 14 | AuthChallengeResponse, 15 | AuthRequired, 16 | HeaderEntry, 17 | RequestId, 18 | RequestPattern, 19 | RequestPaused, 20 | RequestStage 21 | ) 22 | 23 | 24 | async def continue_request( 25 | request_id: RequestId, 26 | url: typing.Optional[str] = None, 27 | method: typing.Optional[str] = None, 28 | post_data: typing.Optional[str] = None, 29 | headers: typing.Optional[typing.List[HeaderEntry]] = None, 30 | intercept_response: typing.Optional[bool] = None 31 | ) -> None: 32 | r''' 33 | Continues the request, optionally modifying some of its parameters. 34 | 35 | :param request_id: An id the client received in requestPaused event. 36 | :param url: *(Optional)* If set, the request url will be modified in a way that's not observable by page. 37 | :param method: *(Optional)* If set, the request method is overridden. 38 | :param post_data: *(Optional)* If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON) 39 | :param headers: *(Optional)* If set, overrides the request headers. 40 | :param intercept_response: **(EXPERIMENTAL)** *(Optional)* If set, overrides response interception behavior for this request. 41 | ''' 42 | session = get_session_context('fetch.continue_request') 43 | return await session.execute(cdp.fetch.continue_request(request_id, url, method, post_data, headers, intercept_response)) 44 | 45 | 46 | async def continue_response( 47 | request_id: RequestId, 48 | response_code: typing.Optional[int] = None, 49 | response_phrase: typing.Optional[str] = None, 50 | response_headers: typing.Optional[typing.List[HeaderEntry]] = None, 51 | binary_response_headers: typing.Optional[str] = None 52 | ) -> None: 53 | r''' 54 | Continues loading of the paused response, optionally modifying the 55 | response headers. If either responseCode or headers are modified, all of them 56 | must be present. 57 | 58 | **EXPERIMENTAL** 59 | 60 | :param request_id: An id the client received in requestPaused event. 61 | :param response_code: *(Optional)* An HTTP response code. If absent, original response code will be used. 62 | :param response_phrase: *(Optional)* A textual representation of responseCode. If absent, a standard phrase matching responseCode is used. 63 | :param response_headers: *(Optional)* Response headers. If absent, original response headers will be used. 64 | :param binary_response_headers: *(Optional)* Alternative way of specifying response headers as a \0-separated series of name: value pairs. Prefer the above method unless you need to represent some non-UTF8 values that can't be transmitted over the protocol as text. (Encoded as a base64 string when passed over JSON) 65 | ''' 66 | session = get_session_context('fetch.continue_response') 67 | return await session.execute(cdp.fetch.continue_response(request_id, response_code, response_phrase, response_headers, binary_response_headers)) 68 | 69 | 70 | async def continue_with_auth( 71 | request_id: RequestId, 72 | auth_challenge_response: AuthChallengeResponse 73 | ) -> None: 74 | r''' 75 | Continues a request supplying authChallengeResponse following authRequired event. 76 | 77 | :param request_id: An id the client received in authRequired event. 78 | :param auth_challenge_response: Response to with an authChallenge. 79 | ''' 80 | session = get_session_context('fetch.continue_with_auth') 81 | return await session.execute(cdp.fetch.continue_with_auth(request_id, auth_challenge_response)) 82 | 83 | 84 | async def disable() -> None: 85 | r''' 86 | Disables the fetch domain. 87 | ''' 88 | session = get_session_context('fetch.disable') 89 | return await session.execute(cdp.fetch.disable()) 90 | 91 | 92 | async def enable( 93 | patterns: typing.Optional[typing.List[RequestPattern]] = None, 94 | handle_auth_requests: typing.Optional[bool] = None 95 | ) -> None: 96 | r''' 97 | Enables issuing of requestPaused events. A request will be paused until client 98 | calls one of failRequest, fulfillRequest or continueRequest/continueWithAuth. 99 | 100 | :param patterns: *(Optional)* If specified, only requests matching any of these patterns will produce fetchRequested event and will be paused until clients response. If not set, all requests will be affected. 101 | :param handle_auth_requests: *(Optional)* If true, authRequired events will be issued and requests will be paused expecting a call to continueWithAuth. 102 | ''' 103 | session = get_session_context('fetch.enable') 104 | return await session.execute(cdp.fetch.enable(patterns, handle_auth_requests)) 105 | 106 | 107 | async def fail_request( 108 | request_id: RequestId, 109 | error_reason: cdp.network.ErrorReason 110 | ) -> None: 111 | r''' 112 | Causes the request to fail with specified reason. 113 | 114 | :param request_id: An id the client received in requestPaused event. 115 | :param error_reason: Causes the request to fail with the given reason. 116 | ''' 117 | session = get_session_context('fetch.fail_request') 118 | return await session.execute(cdp.fetch.fail_request(request_id, error_reason)) 119 | 120 | 121 | async def fulfill_request( 122 | request_id: RequestId, 123 | response_code: int, 124 | response_headers: typing.Optional[typing.List[HeaderEntry]] = None, 125 | binary_response_headers: typing.Optional[str] = None, 126 | body: typing.Optional[str] = None, 127 | response_phrase: typing.Optional[str] = None 128 | ) -> None: 129 | r''' 130 | Provides response to the request. 131 | 132 | :param request_id: An id the client received in requestPaused event. 133 | :param response_code: An HTTP response code. 134 | :param response_headers: *(Optional)* Response headers. 135 | :param binary_response_headers: *(Optional)* Alternative way of specifying response headers as a \0-separated series of name: value pairs. Prefer the above method unless you need to represent some non-UTF8 values that can't be transmitted over the protocol as text. (Encoded as a base64 string when passed over JSON) 136 | :param body: *(Optional)* A response body. If absent, original response body will be used if the request is intercepted at the response stage and empty body will be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON) 137 | :param response_phrase: *(Optional)* A textual representation of responseCode. If absent, a standard phrase matching responseCode is used. 138 | ''' 139 | session = get_session_context('fetch.fulfill_request') 140 | return await session.execute(cdp.fetch.fulfill_request(request_id, response_code, response_headers, binary_response_headers, body, response_phrase)) 141 | 142 | 143 | async def get_response_body( 144 | request_id: RequestId 145 | ) -> typing.Tuple[str, bool]: 146 | r''' 147 | Causes the body of the response to be received from the server and 148 | returned as a single string. May only be issued for a request that 149 | is paused in the Response stage and is mutually exclusive with 150 | takeResponseBodyForInterceptionAsStream. Calling other methods that 151 | affect the request or disabling fetch domain before body is received 152 | results in an undefined behavior. 153 | 154 | :param request_id: Identifier for the intercepted request to get body for. 155 | :returns: A tuple with the following items: 156 | 157 | 0. **body** - Response body. 158 | 1. **base64Encoded** - True, if content was sent as base64. 159 | ''' 160 | session = get_session_context('fetch.get_response_body') 161 | return await session.execute(cdp.fetch.get_response_body(request_id)) 162 | 163 | 164 | async def take_response_body_as_stream( 165 | request_id: RequestId 166 | ) -> cdp.io.StreamHandle: 167 | r''' 168 | Returns a handle to the stream representing the response body. 169 | The request must be paused in the HeadersReceived stage. 170 | Note that after this command the request can't be continued 171 | as is -- client either needs to cancel it or to provide the 172 | response body. 173 | The stream only supports sequential read, IO.read will fail if the position 174 | is specified. 175 | This method is mutually exclusive with getResponseBody. 176 | Calling other methods that affect the request or disabling fetch 177 | domain before body is received results in an undefined behavior. 178 | 179 | :param request_id: 180 | :returns: 181 | ''' 182 | session = get_session_context('fetch.take_response_body_as_stream') 183 | return await session.execute(cdp.fetch.take_response_body_as_stream(request_id)) 184 | -------------------------------------------------------------------------------- /trio_cdp/generated/browser.py: -------------------------------------------------------------------------------- 1 | # DO NOT EDIT THIS FILE! 2 | # 3 | # This code is generated off of PyCDP modules. If you need to make 4 | # changes, edit the generator and regenerate all of the modules. 5 | 6 | from __future__ import annotations 7 | import typing 8 | 9 | from ..context import get_connection_context, get_session_context 10 | 11 | import cdp.browser 12 | from cdp.browser import ( 13 | Bounds, 14 | BrowserCommandId, 15 | BrowserContextID, 16 | Bucket, 17 | DownloadProgress, 18 | DownloadWillBegin, 19 | Histogram, 20 | PermissionDescriptor, 21 | PermissionSetting, 22 | PermissionType, 23 | WindowID, 24 | WindowState 25 | ) 26 | 27 | 28 | async def cancel_download( 29 | guid: str, 30 | browser_context_id: typing.Optional[BrowserContextID] = None 31 | ) -> None: 32 | r''' 33 | Cancel a download if in progress 34 | 35 | **EXPERIMENTAL** 36 | 37 | :param guid: Global unique identifier of the download. 38 | :param browser_context_id: *(Optional)* BrowserContext to perform the action in. When omitted, default browser context is used. 39 | ''' 40 | session = get_session_context('browser.cancel_download') 41 | return await session.execute(cdp.browser.cancel_download(guid, browser_context_id)) 42 | 43 | 44 | async def close() -> None: 45 | r''' 46 | Close browser gracefully. 47 | ''' 48 | session = get_session_context('browser.close') 49 | return await session.execute(cdp.browser.close()) 50 | 51 | 52 | async def crash() -> None: 53 | r''' 54 | Crashes browser on the main thread. 55 | 56 | **EXPERIMENTAL** 57 | ''' 58 | session = get_session_context('browser.crash') 59 | return await session.execute(cdp.browser.crash()) 60 | 61 | 62 | async def crash_gpu_process() -> None: 63 | r''' 64 | Crashes GPU process. 65 | 66 | **EXPERIMENTAL** 67 | ''' 68 | session = get_session_context('browser.crash_gpu_process') 69 | return await session.execute(cdp.browser.crash_gpu_process()) 70 | 71 | 72 | async def execute_browser_command( 73 | command_id: BrowserCommandId 74 | ) -> None: 75 | r''' 76 | Invoke custom browser commands used by telemetry. 77 | 78 | **EXPERIMENTAL** 79 | 80 | :param command_id: 81 | ''' 82 | session = get_session_context('browser.execute_browser_command') 83 | return await session.execute(cdp.browser.execute_browser_command(command_id)) 84 | 85 | 86 | async def get_browser_command_line() -> typing.List[str]: 87 | r''' 88 | Returns the command line switches for the browser process if, and only if 89 | --enable-automation is on the commandline. 90 | 91 | **EXPERIMENTAL** 92 | 93 | :returns: Commandline parameters 94 | ''' 95 | session = get_session_context('browser.get_browser_command_line') 96 | return await session.execute(cdp.browser.get_browser_command_line()) 97 | 98 | 99 | async def get_histogram( 100 | name: str, 101 | delta: typing.Optional[bool] = None 102 | ) -> Histogram: 103 | r''' 104 | Get a Chrome histogram by name. 105 | 106 | **EXPERIMENTAL** 107 | 108 | :param name: Requested histogram name. 109 | :param delta: *(Optional)* If true, retrieve delta since last call. 110 | :returns: Histogram. 111 | ''' 112 | session = get_session_context('browser.get_histogram') 113 | return await session.execute(cdp.browser.get_histogram(name, delta)) 114 | 115 | 116 | async def get_histograms( 117 | query: typing.Optional[str] = None, 118 | delta: typing.Optional[bool] = None 119 | ) -> typing.List[Histogram]: 120 | r''' 121 | Get Chrome histograms. 122 | 123 | **EXPERIMENTAL** 124 | 125 | :param query: *(Optional)* Requested substring in name. Only histograms which have query as a substring in their name are extracted. An empty or absent query returns all histograms. 126 | :param delta: *(Optional)* If true, retrieve delta since last call. 127 | :returns: Histograms. 128 | ''' 129 | session = get_session_context('browser.get_histograms') 130 | return await session.execute(cdp.browser.get_histograms(query, delta)) 131 | 132 | 133 | async def get_version() -> typing.Tuple[str, str, str, str, str]: 134 | r''' 135 | Returns version information. 136 | 137 | :returns: A tuple with the following items: 138 | 139 | 0. **protocolVersion** - Protocol version. 140 | 1. **product** - Product name. 141 | 2. **revision** - Product revision. 142 | 3. **userAgent** - User-Agent. 143 | 4. **jsVersion** - V8 version. 144 | ''' 145 | session = get_session_context('browser.get_version') 146 | return await session.execute(cdp.browser.get_version()) 147 | 148 | 149 | async def get_window_bounds( 150 | window_id: WindowID 151 | ) -> Bounds: 152 | r''' 153 | Get position and size of the browser window. 154 | 155 | **EXPERIMENTAL** 156 | 157 | :param window_id: Browser window id. 158 | :returns: Bounds information of the window. When window state is 'minimized', the restored window position and size are returned. 159 | ''' 160 | session = get_session_context('browser.get_window_bounds') 161 | return await session.execute(cdp.browser.get_window_bounds(window_id)) 162 | 163 | 164 | async def get_window_for_target( 165 | target_id: typing.Optional[cdp.target.TargetID] = None 166 | ) -> typing.Tuple[WindowID, Bounds]: 167 | r''' 168 | Get the browser window that contains the devtools target. 169 | 170 | **EXPERIMENTAL** 171 | 172 | :param target_id: *(Optional)* Devtools agent host id. If called as a part of the session, associated targetId is used. 173 | :returns: A tuple with the following items: 174 | 175 | 0. **windowId** - Browser window id. 176 | 1. **bounds** - Bounds information of the window. When window state is 'minimized', the restored window position and size are returned. 177 | ''' 178 | session = get_session_context('browser.get_window_for_target') 179 | return await session.execute(cdp.browser.get_window_for_target(target_id)) 180 | 181 | 182 | async def grant_permissions( 183 | permissions: typing.List[PermissionType], 184 | origin: typing.Optional[str] = None, 185 | browser_context_id: typing.Optional[BrowserContextID] = None 186 | ) -> None: 187 | r''' 188 | Grant specific permissions to the given origin and reject all others. 189 | 190 | **EXPERIMENTAL** 191 | 192 | :param permissions: 193 | :param origin: *(Optional)* Origin the permission applies to, all origins if not specified. 194 | :param browser_context_id: *(Optional)* BrowserContext to override permissions. When omitted, default browser context is used. 195 | ''' 196 | session = get_session_context('browser.grant_permissions') 197 | return await session.execute(cdp.browser.grant_permissions(permissions, origin, browser_context_id)) 198 | 199 | 200 | async def reset_permissions( 201 | browser_context_id: typing.Optional[BrowserContextID] = None 202 | ) -> None: 203 | r''' 204 | Reset all permission management for all origins. 205 | 206 | **EXPERIMENTAL** 207 | 208 | :param browser_context_id: *(Optional)* BrowserContext to reset permissions. When omitted, default browser context is used. 209 | ''' 210 | session = get_session_context('browser.reset_permissions') 211 | return await session.execute(cdp.browser.reset_permissions(browser_context_id)) 212 | 213 | 214 | async def set_dock_tile( 215 | badge_label: typing.Optional[str] = None, 216 | image: typing.Optional[str] = None 217 | ) -> None: 218 | r''' 219 | Set dock tile details, platform-specific. 220 | 221 | **EXPERIMENTAL** 222 | 223 | :param badge_label: *(Optional)* 224 | :param image: *(Optional)* Png encoded image. (Encoded as a base64 string when passed over JSON) 225 | ''' 226 | session = get_session_context('browser.set_dock_tile') 227 | return await session.execute(cdp.browser.set_dock_tile(badge_label, image)) 228 | 229 | 230 | async def set_download_behavior( 231 | behavior: str, 232 | browser_context_id: typing.Optional[BrowserContextID] = None, 233 | download_path: typing.Optional[str] = None, 234 | events_enabled: typing.Optional[bool] = None 235 | ) -> None: 236 | r''' 237 | Set the behavior when downloading a file. 238 | 239 | **EXPERIMENTAL** 240 | 241 | :param behavior: Whether to allow all or deny all download requests, or use default Chrome behavior if available (otherwise deny). ``allowAndName`` allows download and names files according to their dowmload guids. 242 | :param browser_context_id: *(Optional)* BrowserContext to set download behavior. When omitted, default browser context is used. 243 | :param download_path: *(Optional)* The default path to save downloaded files to. This is required if behavior is set to 'allow' or 'allowAndName'. 244 | :param events_enabled: *(Optional)* Whether to emit download events (defaults to false). 245 | ''' 246 | session = get_session_context('browser.set_download_behavior') 247 | return await session.execute(cdp.browser.set_download_behavior(behavior, browser_context_id, download_path, events_enabled)) 248 | 249 | 250 | async def set_permission( 251 | permission: PermissionDescriptor, 252 | setting: PermissionSetting, 253 | origin: typing.Optional[str] = None, 254 | browser_context_id: typing.Optional[BrowserContextID] = None 255 | ) -> None: 256 | r''' 257 | Set permission settings for given origin. 258 | 259 | **EXPERIMENTAL** 260 | 261 | :param permission: Descriptor of permission to override. 262 | :param setting: Setting of the permission. 263 | :param origin: *(Optional)* Origin the permission applies to, all origins if not specified. 264 | :param browser_context_id: *(Optional)* Context to override. When omitted, default browser context is used. 265 | ''' 266 | session = get_session_context('browser.set_permission') 267 | return await session.execute(cdp.browser.set_permission(permission, setting, origin, browser_context_id)) 268 | 269 | 270 | async def set_window_bounds( 271 | window_id: WindowID, 272 | bounds: Bounds 273 | ) -> None: 274 | r''' 275 | Set position and/or size of the browser window. 276 | 277 | **EXPERIMENTAL** 278 | 279 | :param window_id: Browser window id. 280 | :param bounds: New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged. 281 | ''' 282 | session = get_session_context('browser.set_window_bounds') 283 | return await session.execute(cdp.browser.set_window_bounds(window_id, bounds)) 284 | --------------------------------------------------------------------------------