├── .gitignore ├── .idea └── vcs.xml ├── .travis.yml ├── Dockerfile ├── LICENSE.txt ├── Makefile ├── README.md ├── chromewhip ├── __init__.py ├── base.py ├── chrome.py ├── helpers.py ├── middleware.py ├── protocol │ ├── __init__.py │ ├── accessibility.py │ ├── animation.py │ ├── applicationcache.py │ ├── audits.py │ ├── backgroundservice.py │ ├── browser.py │ ├── cachestorage.py │ ├── cast.py │ ├── console.py │ ├── css.py │ ├── database.py │ ├── debugger.py │ ├── deviceorientation.py │ ├── dom.py │ ├── domdebugger.py │ ├── domsnapshot.py │ ├── domstorage.py │ ├── emulation.py │ ├── fetch.py │ ├── headlessexperimental.py │ ├── heapprofiler.py │ ├── indexeddb.py │ ├── input.py │ ├── inspector.py │ ├── io.py │ ├── layertree.py │ ├── log.py │ ├── media.py │ ├── memory.py │ ├── network.py │ ├── overlay.py │ ├── page.py │ ├── performance.py │ ├── profiler.py │ ├── runtime.py │ ├── schema.py │ ├── security.py │ ├── serviceworker.py │ ├── storage.py │ ├── systeminfo.py │ ├── target.py │ ├── testing.py │ ├── tethering.py │ ├── tracing.py │ ├── webaudio.py │ └── webauthn.py ├── routes.py └── views.py ├── config └── dev.yaml ├── data ├── browser_protocol.json ├── browser_protocol_patch.json ├── devtools_protocol_msg ├── js_protocol.json ├── js_protocol_patch.json └── protocol.py.j2 ├── dev_requirements.txt ├── docs ├── Makefile ├── api.rst ├── conf.py ├── index.rst └── install.rst ├── requirements.txt ├── run_docker.sh ├── scripts ├── check_generation.py ├── generate_protocol.py ├── get-pip.py ├── get_latest_chrome.sh ├── regenerate_protocol.sh └── run_chromewhip_linux.sh ├── setup.cfg ├── setup.py └── tests ├── conftest.py ├── resources ├── js │ └── profiles │ │ └── httpbin-org-html │ │ ├── 001_change_p.js │ │ └── 002_change_h3.js └── responses │ ├── httpbin.org.html.after_profile.txt │ └── httpbin.org.html.txt ├── test_chrome.py ├── test_chromewhip.py └── test_helpers.py /.gitignore: -------------------------------------------------------------------------------- 1 | ### OSX ### 2 | .DS_Store 3 | .AppleDouble 4 | .LSOverride 5 | 6 | ### SublimeText ### 7 | # cache files for sublime text 8 | *.tmlanguage.cache 9 | *.tmPreferences.cache 10 | *.stTheme.cache 11 | 12 | # workspace files are user-specific 13 | *.sublime-workspace 14 | 15 | # project files should be checked into the repository, unless a significant 16 | # proportion of contributors will probably not be using SublimeText 17 | # *.sublime-project 18 | 19 | # sftp configuration file 20 | sftp-config.json 21 | 22 | # Basics 23 | *.py[cod] 24 | __pycache__ 25 | 26 | # Logs 27 | logs 28 | *.log 29 | pip-log.txt 30 | npm-debug.log* 31 | 32 | # Unit test / coverage reports 33 | .coverage 34 | .tox 35 | nosetests.xml 36 | htmlcov 37 | 38 | # Translations 39 | *.mo 40 | *.pot 41 | 42 | # Pycharm 43 | .idea/* 44 | 45 | # Provided default Pycharm Run/Debug Configurations should be tracked by git 46 | # In case of local modifications made by Pycharm, use update-index command 47 | # for each changed file, like this: 48 | # git update-index --assume-unchanged .idea/cloudrank.iml 49 | !.idea/runConfigurations/ 50 | !.idea/cloudrank.iml 51 | !.idea/vcs.xml 52 | !.idea/webResources.xml 53 | 54 | 55 | # Vim 56 | 57 | *~ 58 | *.swp 59 | *.swo 60 | 61 | # npm 62 | node_modules/ 63 | 64 | # Compass 65 | .sass-cache 66 | 67 | # env file 68 | .env 69 | 70 | # virtual environments 71 | venv 72 | 73 | # User-uploaded media 74 | cloudrank/media/ 75 | 76 | 77 | 78 | staticfiles/ 79 | 80 | .cache/ 81 | 82 | /docs/_build/ 83 | dist 84 | chromewhip.egg-info 85 | README.rst 86 | 87 | scripts/devtools-protocol 88 | build/ 89 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "3.7" 4 | dist: xenial 5 | #addons: 6 | # chrome: stable 7 | #before_install: 8 | # - # start your web application and listen on `localhost` 9 | # - google-chrome-stable --headless --disable-gpu --remote-debugging-port=9222 http://localhost & 10 | install: "pip install -r requirements.txt" 11 | script: py.test 12 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM ubuntu:18.04 2 | 3 | MAINTAINER Charlie Smith 4 | 5 | ENV DEBIAN_FRONTEND=noninteractive \ 6 | DEBCONF_NONINTERACTIVE_SEEN=true \ 7 | DISPLAY=:99 8 | 9 | RUN echo "deb http://archive.ubuntu.com/ubuntu bionic main universe\n" > /etc/apt/sources.list \ 10 | && echo "deb http://archive.ubuntu.com/ubuntu bionic-updates main universe\n" >> /etc/apt/sources.list \ 11 | && echo "deb http://security.ubuntu.com/ubuntu bionic-security main universe\n" >> /etc/apt/sources.list 12 | 13 | RUN apt-get update -qqy 14 | RUN apt-get install -y software-properties-common tzdata 15 | 16 | # RUN add-apt-repository ppa:deadsnakes/ppa 17 | # RUN apt-get update -qqy 18 | 19 | ENV TZ "UTC" 20 | RUN echo "${TZ}" > /etc/timezone \ 21 | && dpkg-reconfigure --frontend noninteractive tzdata 22 | 23 | # RUN apt-get -y install python3.7 xvfb \ 24 | # TODO: remove once gui render.png working 25 | RUN apt-get update -qqy --fix-missing 26 | RUN apt-get -y install python3.7 python3.7-distutils xvfb curl 27 | # && rm /etc/apt/sources.list.d/debian.list \ 28 | # && rm -rf /var/lib/apt/lists/* /var/cache/apt/* 29 | 30 | RUN set -xe \ 31 | && curl -fsSL https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ 32 | && echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" > /etc/apt/sources.list.d/google-chrome.list \ 33 | && apt-get update \ 34 | && apt-get install -y google-chrome-stable 35 | # && rm -rf /var/lib/apt/lists/* 36 | 37 | RUN apt-get install -y fonts-ipafont-gothic xfonts-100dpi xfonts-75dpi xfonts-cyrillic xfonts-scalable x11vnc fluxbox 38 | RUN mkdir -p ~/.vnc \ 39 | && x11vnc -storepasswd secret ~/.vnc/passwd 40 | 41 | COPY scripts/get-pip.py /tmp/ 42 | # RUN python3.7 /tmp/get-pip.py && rm /tmp/get-pip.py 43 | RUN curl https://bootstrap.pypa.io/get-pip.py | python3.7 44 | # RUN apt-get -y install python3.7-pip 45 | 46 | RUN mkdir /usr/jsprofiles 47 | WORKDIR /usr/src/app 48 | 49 | COPY requirements.txt ./ 50 | 51 | RUN pip3.7 install --no-cache-dir -r requirements.txt 52 | 53 | COPY . . 54 | 55 | COPY scripts/run_chromewhip_linux.sh . 56 | ENTRYPOINT [ "bash", "run_chromewhip_linux.sh" ] 57 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright 2017 Robert Charles Smith 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: release 2 | release: 3 | git checkout master 4 | git pull 5 | py.test 6 | bumpversion patch 7 | python setup.py sdist bdist_wheel upload 8 | git push origin master --tags 9 | 10 | .PHONY: regenerate 11 | regenerate: 12 | cd scripts && ./regenerate_protocol.sh 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chromewhip - Google Chrome™ as a web service 2 | 3 | [![Build Status](https://travis-ci.org/chuckus/chromewhip.svg?branch=master)](https://travis-ci.org/chuckus/chromewhip) 4 | [![Docker Hub Status](https://img.shields.io/docker/build/chuckus/chromewhip.svg)](https://img.shields.io/docker/build/chuckus/chromewhip.svg) 5 | [![PyPi version](https://img.shields.io/pypi/v/chromewhip.svg)](https://img.shields.io/pypi/v/chromewhip.svg) 6 | 7 | 8 | ### Chrome browser as an HTTP service with an splash compatible HTTP API 9 | 10 | Chromewhip is an easily deployable service that runs headless Chrome process 11 | wrapped with an HTTP API. Inspired by the [`splash`](https://github.com/scrapinghub/splash) 12 | project, we aim to provide a drop-in replacement for the `splash` service by adhering to their documented API. 13 | 14 | It is currently in early **alpha** and still being heavily developed. Please use the issue tracker 15 | to track the progress towards **beta**. For now, the required milestone can be summarised as 16 | **implementing the entire Splash API**. 17 | 18 | ## How to use as a service 19 | 20 | One can simply deploy as a Docker container and use the API that is served on port `8080`. 21 | 22 | ``` 23 | docker run --init -it --rm --shm-size=1024m -p=127.0.0.1:8080:8080 --cap-add=SYS_ADMIN \ 24 | chuckus/chromewhip 25 | ``` 26 | 27 | Refer to the HTTP API reference at the bottom of the README for what features are available. 28 | 29 | ## How to use the low-level driver 30 | 31 | As part of the Chromewhip service, a Python 3.6 asyncio compatible driver for Chrome devtools protocol was 32 | developed and can be leveraged without having to run the HTTP server. The advantages of 33 | our devtools driver are: 34 | 35 | * Typed Python bindings for devtools protocol through templated generation - get autocomplete with your code editor. 36 | * Can bind events to concurrent commands, which is required for providing a robust HTTP service. 37 | 38 | ### Prerequisites 39 | 40 | Before executing the code below, please have the following: 41 | 42 | * Google Chrome Canary running with flag `--remote-debugging-port=9222` 43 | 44 | ### Example driver code 45 | 46 | ```python 47 | import asyncio 48 | import logging 49 | 50 | from chromewhip import Chrome 51 | from chromewhip.protocol import browser, page, dom 52 | 53 | # see logging from chromewhip 54 | logging.basicConfig(level=logging.DEBUG) 55 | 56 | HOST = '127.0.0.1' 57 | PORT = 9222 58 | 59 | loop = asyncio.get_event_loop() 60 | c = Chrome(host=HOST, port=PORT) 61 | 62 | loop.run_until_complete(c.connect()) 63 | 64 | 65 | # use the startup tab or create a new one 66 | tab = c.tabs[0] 67 | tab = loop.run_until_complete(c.create_tab()) 68 | 69 | loop.run_until_complete(tab.enable_page_events()) 70 | 71 | def sync_cmd(*args, **kwargs): 72 | return loop.run_until_complete(tab.send_command(*args, **kwargs)) 73 | 74 | # send_command will return once the frameStoppedLoading event is received THAT matches 75 | # the frameId that it is in the returned command payload. 76 | result = sync_cmd(page.Page.navigate(url='http://nzherald.co.nz'), 77 | await_on_event_type=page.FrameStoppedLoadingEvent) 78 | 79 | # send_command always returns a dict with keys `ack` and `event` 80 | # `ack` contains the payload on response of a command 81 | # `event` contains the payload of the awaited event if `await_on_event_type` is provided 82 | ack = result['ack']['result'] 83 | event = result['event'] 84 | assert ack['frameId'] == event.frameId 85 | 86 | sync_cmd(page.Page.setDeviceMetricsOverride(width=800, 87 | height=600, 88 | deviceScaleFactor=0.0, 89 | mobile=False)) 90 | 91 | 92 | result = sync_cmd(dom.DOM.getDocument()) 93 | 94 | dom_obj = result['ack']['result']['root'] 95 | 96 | # Python types are determined by the `types` fields in the JSON reference for the 97 | # devtools protocol, and `send_command` will convert if possible. 98 | assert isinstance(dom_obj, dom.Node) 99 | 100 | print(dom_obj.nodeId) 101 | print(dom_obj.nodeName) 102 | 103 | # close the tab 104 | loop.run_until_complete(c.close_tab(tab)) 105 | 106 | # or close the browser via Devtools API 107 | tab = c.tabs[0] 108 | sync_cmd(browser.Browser.close()) 109 | ``` 110 | 111 | 112 | 113 | ## Implemented HTTP API 114 | 115 | ### /render.html 116 | 117 | Query params: 118 | 119 | * url : string : required 120 | * The url to render (required) 121 | 122 | * js : string : optional 123 | Javascript profile name. 124 | 125 | * js_source : string : optional 126 | * JavaScript code to be executed in page context 127 | 128 | * viewport : string : optional 129 | * View width and height (in pixels) of the browser viewport to render the web 130 | page. Format is "x", e.g. 800x600. Default value is 1024x768. 131 | 132 | 'viewport' parameter is more important for PNG and JPEG rendering; it is supported for 133 | all rendering endpoints because javascript code execution can depend on 134 | viewport size. 135 | 136 | ### /render.png 137 | 138 | Query params (including render.html): 139 | 140 | * render_all : int : optional 141 | * Possible values are `1` and `0`. When `render_all=1`, extend the 142 | viewport to include the whole webpage (possibly very tall) before rendering. 143 | 144 | ### Why not just use Selenium? 145 | * chromewhip uses the devtools protocol instead of the json wire protocol, where the devtools protocol has 146 | greater flexibility, especially when it comes to subscribing to granular events from the browser. 147 | 148 | ## Bug reports and requests 149 | Please simply file one using the Github tracker 150 | 151 | ## Contributing 152 | Please :) 153 | 154 | ### How to regenerate the Python protocol files 155 | 156 | In `scripts`, you can run `regenerate_protocol.sh`, which downloads HEAD of offical devtools specs, regenerates, 157 | runs some sanity tests and creates a commit with the message of official devtools specs HEAD. 158 | 159 | From time to time, it will fail, due to desynchronization of the `chromewhip` patch with the json specs, or 160 | mistakes in the protocol. 161 | 162 | Under `data`, there are `*_patch` files, which follow the [RFC 6902 JSON Patch notation](https://tools.ietf.org/html/rfc6902). 163 | You will see that there are some checks to see whether particular items in arrays exist before patching. If you get 164 | a `jsonpatch.JsonPatchTestFailed` exception, it's likely to desynchronization, so check the official spec and adjust 165 | the patch json file. 166 | 167 | ## Implementation 168 | 169 | Developed to run on Python 3.6, it leverages both `aiohttp` and `asyncio` for the implementation of the 170 | asynchronous HTTP server that wraps `chrome`. 171 | 172 | 173 | -------------------------------------------------------------------------------- /chromewhip/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio.subprocess 2 | import logging 3 | import logging.config 4 | import platform 5 | import signal 6 | import os 7 | from collections import namedtuple 8 | import time 9 | 10 | from aiohttp import web 11 | import yaml 12 | 13 | from chromewhip.chrome import Chrome 14 | from chromewhip.middleware import error_middleware 15 | from chromewhip.routes import setup_routes 16 | 17 | 18 | log = logging.getLogger(__name__) 19 | 20 | HOST = '127.0.0.1' 21 | PORT = 9222 22 | NUM_TABS = 4 23 | DISPLAY = ':99' 24 | 25 | async def on_shutdown(app): 26 | c = app['chrome-driver'] 27 | if c.is_connected: 28 | for tab in c.tabs: 29 | await tab.disconnect() 30 | 31 | chrome = app['chrome-process'] 32 | chrome.send_signal(signal.SIGINT) 33 | try: 34 | returncode = await asyncio.wait_for(chrome.wait(), timeout=15) 35 | if not returncode: 36 | log.error('Timed out trying to shutdown Chrome gracefully!') 37 | elif returncode < 0: 38 | log.error('Error code "%s" received while shutting down Chrome!' % abs(returncode)) 39 | else: 40 | log.debug("Successfully shut down Chrome!") 41 | except asyncio.TimeoutError: 42 | log.error('Timed out trying to shutdown Chrome gracefully!') 43 | 44 | Settings = namedtuple('Settings', [ 45 | 'chrome_fp', 46 | 'chrome_flags', 47 | 'should_run_xfvb' 48 | ]) 49 | 50 | def get_settings(): 51 | chrome_flags = [ 52 | '--window-size=1920,1080', 53 | '--enable-logging', 54 | '--hide-scrollbars', 55 | '--no-first-run', 56 | '--remote-debugging-address=%s' % HOST, 57 | '--remote-debugging-port=%s' % PORT, 58 | '--user-data-dir=/tmp', 59 | 'about:blank' # TODO: multiple tabs 60 | ] 61 | os_type = platform.system() 62 | if os_type == 'Linux': 63 | chrome_flags.insert(3, '--no-sandbox') 64 | chrome_fp = '/opt/google/chrome/chrome' 65 | should_run_xfvb = True 66 | elif os_type == 'Darwin': 67 | chrome_fp = '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary' 68 | should_run_xfvb = False 69 | else: 70 | raise Exception('"%s" system is not supported!' % os_type) 71 | return Settings( 72 | chrome_fp, 73 | chrome_flags, 74 | should_run_xfvb 75 | ) 76 | 77 | 78 | def setup_chrome(settings: Settings, env: dict = None, loop: asyncio.AbstractEventLoop = None): 79 | # TODO: manage process lifecycle in coro 80 | args = [settings.chrome_fp] + settings.chrome_flags 81 | chrome = asyncio.subprocess.create_subprocess_exec(*args, env=env, loop=loop) 82 | return chrome 83 | 84 | 85 | def setup_xvfb(settings: Settings, env: dict = None, loop: asyncio.AbstractEventLoop = None): 86 | # TODO: manage process lifecycle in coro 87 | if not settings.should_run_xfvb: 88 | return 89 | flags = [ 90 | DISPLAY, 91 | '-ac', 92 | '-screen', 93 | '0', 94 | '1920x1080x16', 95 | '+extension', 96 | 'RANDR', 97 | '-nolisten', 98 | 'tcp', 99 | ] 100 | args = ['/usr/bin/Xvfb'] + flags 101 | xvfb = asyncio.subprocess.create_subprocess_exec(*args, env=env, loop=loop) 102 | return xvfb 103 | 104 | 105 | def setup_app(loop=None, js_profiles_path=None): 106 | app = web.Application(loop=loop, middlewares=[error_middleware]) 107 | 108 | js_profiles = {} 109 | 110 | if js_profiles_path: 111 | root, _, files, _ = next(os.fwalk(js_profiles_path)) 112 | js_files = filter(lambda f: os.path.splitext(f)[1] == '.js', files) 113 | _, profile_name = os.path.split(root) 114 | log.debug('adding profile "{}"'.format(profile_name)) 115 | js_profiles[profile_name] = "" 116 | for f in js_files: 117 | code = open(os.path.join(root, f)).read() 118 | js_profiles[profile_name] += '{}\n'.format(code) 119 | 120 | app.on_shutdown.append(on_shutdown) 121 | 122 | c = Chrome(host=HOST, port=PORT) 123 | 124 | app['chrome-driver'] = c 125 | app['js-profiles'] = js_profiles 126 | 127 | setup_routes(app) 128 | 129 | return app 130 | 131 | 132 | if __name__ == '__main__': 133 | import argparse 134 | import sys 135 | config_fp = os.path.abspath(os.path.join(os.path.dirname(__file__), '../config/dev.yaml')) 136 | config_f = open(config_fp) 137 | config = yaml.load(config_f) 138 | logging.config.dictConfig(config['logging']) 139 | parser = argparse.ArgumentParser() 140 | parser.add_argument('--js-profiles-path', 141 | help="path to a folder with javascript profiles") 142 | args = parser.parse_args(sys.argv[1:]) 143 | kwargs = {} 144 | if args.js_profiles_path: 145 | kwargs['js_profiles_path'] = args.js_profiles_path 146 | 147 | loop = asyncio.get_event_loop() 148 | 149 | env = { 150 | 'DISPLAY': DISPLAY 151 | } 152 | 153 | settings = get_settings() 154 | app = setup_app(**kwargs, loop=loop) 155 | 156 | if settings.should_run_xfvb: 157 | xvfb = setup_xvfb(settings, env=env, loop=loop) 158 | xvfb_future = loop.run_until_complete(xvfb) 159 | log.debug('Started xvfb!') 160 | app['xvfb-process'] = xvfb_future 161 | 162 | chrome = setup_chrome(settings, env=env, loop=loop) 163 | chrome_future = loop.run_until_complete(chrome) 164 | time.sleep(3) # TODO: use event for continuing as opposed to sleep 165 | 166 | log.debug('Started Chrome!') 167 | app['chrome-process'] = chrome_future 168 | 169 | # TODO: need indication from chrome process to start http server 170 | loop.run_until_complete(asyncio.sleep(3)) 171 | loop.run_until_complete(app['chrome-driver'].connect()) 172 | web.run_app(app) 173 | -------------------------------------------------------------------------------- /chromewhip/base.py: -------------------------------------------------------------------------------- 1 | # https://stackoverflow.com/questions/30155138/how-can-i-write-asyncio-coroutines-that-optionally-act-as-regular-functions 2 | import asyncio 3 | 4 | 5 | class SyncAdder(type): 6 | """ A metaclass which adds synchronous version of coroutines. 7 | 8 | This metaclass finds all coroutine functions defined on a class 9 | and adds a synchronous version with a '_s' suffix appended to the 10 | original function name. 11 | 12 | """ 13 | def __new__(cls, clsname, bases, dct, **kwargs): 14 | new_dct = {} 15 | for name,val in dct.items(): 16 | # Make a sync version of all coroutine functions 17 | if asyncio.iscoroutinefunction(val): 18 | meth = cls.sync_maker(name) 19 | syncname = '{}_s'.format(name) 20 | meth.__name__ = syncname 21 | meth.__qualname__ = '{}.{}'.format(clsname, syncname) 22 | new_dct[syncname] = meth 23 | dct.update(new_dct) 24 | return super().__new__(cls, clsname, bases, dct) 25 | 26 | @staticmethod 27 | def sync_maker(func): 28 | def sync_func(self, *args, **kwargs): 29 | meth = getattr(self, func) 30 | return asyncio.get_event_loop().run_until_complete(meth(*args, **kwargs)) 31 | return sync_func 32 | -------------------------------------------------------------------------------- /chromewhip/helpers.py: -------------------------------------------------------------------------------- 1 | import copy 2 | import json 3 | import logging 4 | import re 5 | import sys 6 | 7 | 8 | class PayloadMixin: 9 | @classmethod 10 | def build_send_payload(cls, method: str, params: dict): 11 | return { 12 | "method": ".".join([cls.__name__, method]), 13 | "params": {k: v for k, v in params.items() if v is not None} 14 | } 15 | 16 | @classmethod 17 | def convert_payload(cls, types: dict): 18 | def convert(result: dict): 19 | """ 20 | 21 | :param result: 22 | :return: 23 | """ 24 | types_ = copy.copy(types) 25 | for name, val in result.items(): 26 | try: 27 | expected_ = types_.pop(name) 28 | expected_type_ = expected_['class'] 29 | except KeyError: 30 | raise KeyError('name %s not in expected payload of %s' % (name, types)) 31 | if issubclass(expected_type_, ChromeTypeBase): 32 | result[name] = expected_type_(**val) 33 | elif re.match(r'.*Id$', name) and isinstance(val, str): 34 | result[name] = expected_type_(val) 35 | elif not isinstance(val, expected_type_): 36 | raise ValueError('%s is not expected type %s, instead is %s' % (val, expected_type_, val)) 37 | for rn, rv in types_.items(): 38 | if not rv.get('optional', False): 39 | raise ValueError('expected payload param "%s" is missing!' % rn) 40 | return result 41 | return convert 42 | 43 | 44 | log = logging.getLogger(__name__) 45 | 46 | 47 | class BaseEvent: 48 | js_name = 'chromewhipBaseEvent' 49 | hashable = [] 50 | is_hashable = False 51 | 52 | def hash_(self): 53 | hashable_params = {} 54 | for k, v in self.__dict__.items(): 55 | if k in self.hashable: 56 | hashable_params[k] = v 57 | else: 58 | try: 59 | hashable_params['%sId' % k] = v.id 60 | except KeyError: 61 | pass 62 | except AttributeError: 63 | # TODO: make better, fails for event that has 'timestamp` as a param 64 | pass 65 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in hashable_params.items()]) 66 | h = '{}:{}'.format(self.js_name, serialized_id_params) 67 | log.debug('generated hash = %s' % h) 68 | return h 69 | 70 | 71 | # TODO: how do 72 | def json_to_event(payload) -> BaseEvent: 73 | try: 74 | prot_name, js_event = payload['method'].split('.') 75 | except KeyError: 76 | log.error('invalid event JSON, must have a "method" key') 77 | return None 78 | except ValueError: 79 | log.error('invalid method name "%s", must contain a module and event joined with a "."' % payload['method']) 80 | return None 81 | module_name = 'chromewhip.protocol.%s' % prot_name.lower() 82 | try: 83 | prot_module = sys.modules[module_name] 84 | except KeyError: 85 | msg = '"%s" is not available in sys.modules!' % module_name 86 | log.error(msg) 87 | raise KeyError(msg) 88 | py_event_name = '{}{}Event'.format(js_event[0].upper(), js_event[1:]) 89 | event_cls = getattr(prot_module, py_event_name) 90 | try: 91 | result = event_cls(**payload['params']) 92 | except TypeError as e: 93 | raise TypeError('%s unable to deserialise: %s' % (event_cls.__name__, e)) 94 | return result 95 | 96 | 97 | class ChromeTypeBase: 98 | 99 | def to_dict(self): 100 | return self.__dict__ 101 | 102 | 103 | class ChromewhipJSONEncoder(json.JSONEncoder): 104 | def default(self, obj): 105 | if isinstance(obj, BaseEvent): 106 | return {'method': obj.js_name, 'params': obj.__dict__} 107 | 108 | if isinstance(obj, ChromeTypeBase): 109 | return obj.__dict__ 110 | 111 | return json.JSONEncoder.default(self, obj) 112 | -------------------------------------------------------------------------------- /chromewhip/middleware.py: -------------------------------------------------------------------------------- 1 | import json 2 | import traceback 3 | import pprint 4 | 5 | from aiohttp import web 6 | 7 | from chromewhip.chrome import ChromewhipException 8 | 9 | 10 | def json_error(message): 11 | # return web.Response( 12 | # body=json.dumps({'error': message}).encode('utf-8'), 13 | # content_type='application/json') 14 | # return web.Response(text=pprint.pformat({'error': message})) 15 | return web.Response(text=json.dumps({'error': message}, indent=4)) 16 | 17 | async def error_middleware(app, handler): 18 | async def middleware_handler(request): 19 | try: 20 | response = await handler(request) 21 | if response.status != 200: 22 | return json_error(response.message) 23 | return response 24 | except web.HTTPException as ex: 25 | return json_error(ex.reason) 26 | except ChromewhipException as ex: 27 | return json_error(ex.args[0]) 28 | except Exception as ex: 29 | verbose_tb = traceback.format_exc() 30 | return json_error(verbose_tb) 31 | return middleware_handler 32 | -------------------------------------------------------------------------------- /chromewhip/protocol/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chazkii/chromewhip/f0014b1afe077588f77649bf31d1c41071d835a7/chromewhip/protocol/__init__.py -------------------------------------------------------------------------------- /chromewhip/protocol/accessibility.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import dom as DOM 16 | 17 | # AXNodeId: Unique accessibility node identifier. 18 | AXNodeId = str 19 | 20 | # AXValueType: Enum of possible property types. 21 | AXValueType = str 22 | 23 | # AXValueSourceType: Enum of possible property sources. 24 | AXValueSourceType = str 25 | 26 | # AXValueNativeSourceType: Enum of possible native property sources (as a subtype of a particular AXValueSourceType). 27 | AXValueNativeSourceType = str 28 | 29 | # AXValueSource: A single source for a computed AX property. 30 | class AXValueSource(ChromeTypeBase): 31 | def __init__(self, 32 | type: Union['AXValueSourceType'], 33 | value: Optional['AXValue'] = None, 34 | attribute: Optional['str'] = None, 35 | attributeValue: Optional['AXValue'] = None, 36 | superseded: Optional['bool'] = None, 37 | nativeSource: Optional['AXValueNativeSourceType'] = None, 38 | nativeSourceValue: Optional['AXValue'] = None, 39 | invalid: Optional['bool'] = None, 40 | invalidReason: Optional['str'] = None, 41 | ): 42 | 43 | self.type = type 44 | self.value = value 45 | self.attribute = attribute 46 | self.attributeValue = attributeValue 47 | self.superseded = superseded 48 | self.nativeSource = nativeSource 49 | self.nativeSourceValue = nativeSourceValue 50 | self.invalid = invalid 51 | self.invalidReason = invalidReason 52 | 53 | 54 | # AXRelatedNode: 55 | class AXRelatedNode(ChromeTypeBase): 56 | def __init__(self, 57 | backendDOMNodeId: Union['DOM.BackendNodeId'], 58 | idref: Optional['str'] = None, 59 | text: Optional['str'] = None, 60 | ): 61 | 62 | self.backendDOMNodeId = backendDOMNodeId 63 | self.idref = idref 64 | self.text = text 65 | 66 | 67 | # AXProperty: 68 | class AXProperty(ChromeTypeBase): 69 | def __init__(self, 70 | name: Union['AXPropertyName'], 71 | value: Union['AXValue'], 72 | ): 73 | 74 | self.name = name 75 | self.value = value 76 | 77 | 78 | # AXValue: A single computed AX property. 79 | class AXValue(ChromeTypeBase): 80 | def __init__(self, 81 | type: Union['AXValueType'], 82 | value: Optional['Any'] = None, 83 | relatedNodes: Optional['[AXRelatedNode]'] = None, 84 | sources: Optional['[AXValueSource]'] = None, 85 | ): 86 | 87 | self.type = type 88 | self.value = value 89 | self.relatedNodes = relatedNodes 90 | self.sources = sources 91 | 92 | 93 | # AXPropertyName: Values of AXProperty name:- from 'busy' to 'roledescription': states which apply to every AX node- from 'live' to 'root': attributes which apply to nodes in live regions- from 'autocomplete' to 'valuetext': attributes which apply to widgets- from 'checked' to 'selected': states which apply to widgets- from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling. 94 | AXPropertyName = str 95 | 96 | # AXNode: A node in the accessibility tree. 97 | class AXNode(ChromeTypeBase): 98 | def __init__(self, 99 | nodeId: Union['AXNodeId'], 100 | ignored: Union['bool'], 101 | ignoredReasons: Optional['[AXProperty]'] = None, 102 | role: Optional['AXValue'] = None, 103 | name: Optional['AXValue'] = None, 104 | description: Optional['AXValue'] = None, 105 | value: Optional['AXValue'] = None, 106 | properties: Optional['[AXProperty]'] = None, 107 | childIds: Optional['[AXNodeId]'] = None, 108 | backendDOMNodeId: Optional['DOM.BackendNodeId'] = None, 109 | ): 110 | 111 | self.nodeId = nodeId 112 | self.ignored = ignored 113 | self.ignoredReasons = ignoredReasons 114 | self.role = role 115 | self.name = name 116 | self.description = description 117 | self.value = value 118 | self.properties = properties 119 | self.childIds = childIds 120 | self.backendDOMNodeId = backendDOMNodeId 121 | 122 | 123 | class Accessibility(PayloadMixin): 124 | """ 125 | """ 126 | @classmethod 127 | def disable(cls): 128 | """Disables the accessibility domain. 129 | """ 130 | return ( 131 | cls.build_send_payload("disable", { 132 | }), 133 | None 134 | ) 135 | 136 | @classmethod 137 | def enable(cls): 138 | """Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls. 139 | This turns on accessibility for the page, which can impact performance until accessibility is disabled. 140 | """ 141 | return ( 142 | cls.build_send_payload("enable", { 143 | }), 144 | None 145 | ) 146 | 147 | @classmethod 148 | def getPartialAXTree(cls, 149 | nodeId: Optional['DOM.NodeId'] = None, 150 | backendNodeId: Optional['DOM.BackendNodeId'] = None, 151 | objectId: Optional['Runtime.RemoteObjectId'] = None, 152 | fetchRelatives: Optional['bool'] = None, 153 | ): 154 | """Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists. 155 | :param nodeId: Identifier of the node to get the partial accessibility tree for. 156 | :type nodeId: DOM.NodeId 157 | :param backendNodeId: Identifier of the backend node to get the partial accessibility tree for. 158 | :type backendNodeId: DOM.BackendNodeId 159 | :param objectId: JavaScript object id of the node wrapper to get the partial accessibility tree for. 160 | :type objectId: Runtime.RemoteObjectId 161 | :param fetchRelatives: Whether to fetch this nodes ancestors, siblings and children. Defaults to true. 162 | :type fetchRelatives: bool 163 | """ 164 | return ( 165 | cls.build_send_payload("getPartialAXTree", { 166 | "nodeId": nodeId, 167 | "backendNodeId": backendNodeId, 168 | "objectId": objectId, 169 | "fetchRelatives": fetchRelatives, 170 | }), 171 | cls.convert_payload({ 172 | "nodes": { 173 | "class": [AXNode], 174 | "optional": False 175 | }, 176 | }) 177 | ) 178 | 179 | @classmethod 180 | def getFullAXTree(cls): 181 | """Fetches the entire accessibility tree 182 | """ 183 | return ( 184 | cls.build_send_payload("getFullAXTree", { 185 | }), 186 | cls.convert_payload({ 187 | "nodes": { 188 | "class": [AXNode], 189 | "optional": False 190 | }, 191 | }) 192 | ) 193 | 194 | -------------------------------------------------------------------------------- /chromewhip/protocol/animation.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import runtime as Runtime 16 | from chromewhip.protocol import dom as DOM 17 | 18 | # Animation: Animation instance. 19 | class Animation(ChromeTypeBase): 20 | def __init__(self, 21 | id: Union['str'], 22 | name: Union['str'], 23 | pausedState: Union['bool'], 24 | playState: Union['str'], 25 | playbackRate: Union['float'], 26 | startTime: Union['float'], 27 | currentTime: Union['float'], 28 | type: Union['str'], 29 | source: Optional['AnimationEffect'] = None, 30 | cssId: Optional['str'] = None, 31 | ): 32 | 33 | self.id = id 34 | self.name = name 35 | self.pausedState = pausedState 36 | self.playState = playState 37 | self.playbackRate = playbackRate 38 | self.startTime = startTime 39 | self.currentTime = currentTime 40 | self.type = type 41 | self.source = source 42 | self.cssId = cssId 43 | 44 | 45 | # AnimationEffect: AnimationEffect instance 46 | class AnimationEffect(ChromeTypeBase): 47 | def __init__(self, 48 | delay: Union['float'], 49 | endDelay: Union['float'], 50 | iterationStart: Union['float'], 51 | iterations: Union['float'], 52 | duration: Union['float'], 53 | direction: Union['str'], 54 | fill: Union['str'], 55 | easing: Union['str'], 56 | backendNodeId: Optional['DOM.BackendNodeId'] = None, 57 | keyframesRule: Optional['KeyframesRule'] = None, 58 | ): 59 | 60 | self.delay = delay 61 | self.endDelay = endDelay 62 | self.iterationStart = iterationStart 63 | self.iterations = iterations 64 | self.duration = duration 65 | self.direction = direction 66 | self.fill = fill 67 | self.backendNodeId = backendNodeId 68 | self.keyframesRule = keyframesRule 69 | self.easing = easing 70 | 71 | 72 | # KeyframesRule: Keyframes Rule 73 | class KeyframesRule(ChromeTypeBase): 74 | def __init__(self, 75 | keyframes: Union['[KeyframeStyle]'], 76 | name: Optional['str'] = None, 77 | ): 78 | 79 | self.name = name 80 | self.keyframes = keyframes 81 | 82 | 83 | # KeyframeStyle: Keyframe Style 84 | class KeyframeStyle(ChromeTypeBase): 85 | def __init__(self, 86 | offset: Union['str'], 87 | easing: Union['str'], 88 | ): 89 | 90 | self.offset = offset 91 | self.easing = easing 92 | 93 | 94 | class Animation(PayloadMixin): 95 | """ 96 | """ 97 | @classmethod 98 | def disable(cls): 99 | """Disables animation domain notifications. 100 | """ 101 | return ( 102 | cls.build_send_payload("disable", { 103 | }), 104 | None 105 | ) 106 | 107 | @classmethod 108 | def enable(cls): 109 | """Enables animation domain notifications. 110 | """ 111 | return ( 112 | cls.build_send_payload("enable", { 113 | }), 114 | None 115 | ) 116 | 117 | @classmethod 118 | def getCurrentTime(cls, 119 | id: Union['str'], 120 | ): 121 | """Returns the current time of the an animation. 122 | :param id: Id of animation. 123 | :type id: str 124 | """ 125 | return ( 126 | cls.build_send_payload("getCurrentTime", { 127 | "id": id, 128 | }), 129 | cls.convert_payload({ 130 | "currentTime": { 131 | "class": float, 132 | "optional": False 133 | }, 134 | }) 135 | ) 136 | 137 | @classmethod 138 | def getPlaybackRate(cls): 139 | """Gets the playback rate of the document timeline. 140 | """ 141 | return ( 142 | cls.build_send_payload("getPlaybackRate", { 143 | }), 144 | cls.convert_payload({ 145 | "playbackRate": { 146 | "class": float, 147 | "optional": False 148 | }, 149 | }) 150 | ) 151 | 152 | @classmethod 153 | def releaseAnimations(cls, 154 | animations: Union['[]'], 155 | ): 156 | """Releases a set of animations to no longer be manipulated. 157 | :param animations: List of animation ids to seek. 158 | :type animations: [] 159 | """ 160 | return ( 161 | cls.build_send_payload("releaseAnimations", { 162 | "animations": animations, 163 | }), 164 | None 165 | ) 166 | 167 | @classmethod 168 | def resolveAnimation(cls, 169 | animationId: Union['str'], 170 | ): 171 | """Gets the remote object of the Animation. 172 | :param animationId: Animation id. 173 | :type animationId: str 174 | """ 175 | return ( 176 | cls.build_send_payload("resolveAnimation", { 177 | "animationId": animationId, 178 | }), 179 | cls.convert_payload({ 180 | "remoteObject": { 181 | "class": Runtime.RemoteObject, 182 | "optional": False 183 | }, 184 | }) 185 | ) 186 | 187 | @classmethod 188 | def seekAnimations(cls, 189 | animations: Union['[]'], 190 | currentTime: Union['float'], 191 | ): 192 | """Seek a set of animations to a particular time within each animation. 193 | :param animations: List of animation ids to seek. 194 | :type animations: [] 195 | :param currentTime: Set the current time of each animation. 196 | :type currentTime: float 197 | """ 198 | return ( 199 | cls.build_send_payload("seekAnimations", { 200 | "animations": animations, 201 | "currentTime": currentTime, 202 | }), 203 | None 204 | ) 205 | 206 | @classmethod 207 | def setPaused(cls, 208 | animations: Union['[]'], 209 | paused: Union['bool'], 210 | ): 211 | """Sets the paused state of a set of animations. 212 | :param animations: Animations to set the pause state of. 213 | :type animations: [] 214 | :param paused: Paused state to set to. 215 | :type paused: bool 216 | """ 217 | return ( 218 | cls.build_send_payload("setPaused", { 219 | "animations": animations, 220 | "paused": paused, 221 | }), 222 | None 223 | ) 224 | 225 | @classmethod 226 | def setPlaybackRate(cls, 227 | playbackRate: Union['float'], 228 | ): 229 | """Sets the playback rate of the document timeline. 230 | :param playbackRate: Playback rate for animations on page 231 | :type playbackRate: float 232 | """ 233 | return ( 234 | cls.build_send_payload("setPlaybackRate", { 235 | "playbackRate": playbackRate, 236 | }), 237 | None 238 | ) 239 | 240 | @classmethod 241 | def setTiming(cls, 242 | animationId: Union['str'], 243 | duration: Union['float'], 244 | delay: Union['float'], 245 | ): 246 | """Sets the timing of an animation node. 247 | :param animationId: Animation id. 248 | :type animationId: str 249 | :param duration: Duration of the animation. 250 | :type duration: float 251 | :param delay: Delay of the animation. 252 | :type delay: float 253 | """ 254 | return ( 255 | cls.build_send_payload("setTiming", { 256 | "animationId": animationId, 257 | "duration": duration, 258 | "delay": delay, 259 | }), 260 | None 261 | ) 262 | 263 | 264 | 265 | class AnimationCanceledEvent(BaseEvent): 266 | 267 | js_name = 'Animation.animationCanceled' 268 | hashable = ['id'] 269 | is_hashable = True 270 | 271 | def __init__(self, 272 | id: Union['str', dict], 273 | ): 274 | if isinstance(id, dict): 275 | id = str(**id) 276 | self.id = id 277 | 278 | @classmethod 279 | def build_hash(cls, id): 280 | kwargs = locals() 281 | kwargs.pop('cls') 282 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 283 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 284 | log.debug('generated hash = %s' % h) 285 | return h 286 | 287 | 288 | class AnimationCreatedEvent(BaseEvent): 289 | 290 | js_name = 'Animation.animationCreated' 291 | hashable = ['id'] 292 | is_hashable = True 293 | 294 | def __init__(self, 295 | id: Union['str', dict], 296 | ): 297 | if isinstance(id, dict): 298 | id = str(**id) 299 | self.id = id 300 | 301 | @classmethod 302 | def build_hash(cls, id): 303 | kwargs = locals() 304 | kwargs.pop('cls') 305 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 306 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 307 | log.debug('generated hash = %s' % h) 308 | return h 309 | 310 | 311 | class AnimationStartedEvent(BaseEvent): 312 | 313 | js_name = 'Animation.animationStarted' 314 | hashable = ['animationId'] 315 | is_hashable = True 316 | 317 | def __init__(self, 318 | animation: Union['Animation', dict], 319 | ): 320 | if isinstance(animation, dict): 321 | animation = Animation(**animation) 322 | self.animation = animation 323 | 324 | @classmethod 325 | def build_hash(cls, animationId): 326 | kwargs = locals() 327 | kwargs.pop('cls') 328 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 329 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 330 | log.debug('generated hash = %s' % h) 331 | return h 332 | -------------------------------------------------------------------------------- /chromewhip/protocol/applicationcache.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # ApplicationCacheResource: Detailed application cache resource information. 17 | class ApplicationCacheResource(ChromeTypeBase): 18 | def __init__(self, 19 | url: Union['str'], 20 | size: Union['int'], 21 | type: Union['str'], 22 | ): 23 | 24 | self.url = url 25 | self.size = size 26 | self.type = type 27 | 28 | 29 | # ApplicationCache: Detailed application cache information. 30 | class ApplicationCache(ChromeTypeBase): 31 | def __init__(self, 32 | manifestURL: Union['str'], 33 | size: Union['float'], 34 | creationTime: Union['float'], 35 | updateTime: Union['float'], 36 | resources: Union['[ApplicationCacheResource]'], 37 | ): 38 | 39 | self.manifestURL = manifestURL 40 | self.size = size 41 | self.creationTime = creationTime 42 | self.updateTime = updateTime 43 | self.resources = resources 44 | 45 | 46 | # FrameWithManifest: Frame identifier - manifest URL pair. 47 | class FrameWithManifest(ChromeTypeBase): 48 | def __init__(self, 49 | frameId: Union['Page.FrameId'], 50 | manifestURL: Union['str'], 51 | status: Union['int'], 52 | ): 53 | 54 | self.frameId = frameId 55 | self.manifestURL = manifestURL 56 | self.status = status 57 | 58 | 59 | class ApplicationCache(PayloadMixin): 60 | """ 61 | """ 62 | @classmethod 63 | def enable(cls): 64 | """Enables application cache domain notifications. 65 | """ 66 | return ( 67 | cls.build_send_payload("enable", { 68 | }), 69 | None 70 | ) 71 | 72 | @classmethod 73 | def getApplicationCacheForFrame(cls, 74 | frameId: Union['Page.FrameId'], 75 | ): 76 | """Returns relevant application cache data for the document in given frame. 77 | :param frameId: Identifier of the frame containing document whose application cache is retrieved. 78 | :type frameId: Page.FrameId 79 | """ 80 | return ( 81 | cls.build_send_payload("getApplicationCacheForFrame", { 82 | "frameId": frameId, 83 | }), 84 | cls.convert_payload({ 85 | "applicationCache": { 86 | "class": ApplicationCache, 87 | "optional": False 88 | }, 89 | }) 90 | ) 91 | 92 | @classmethod 93 | def getFramesWithManifests(cls): 94 | """Returns array of frame identifiers with manifest urls for each frame containing a document 95 | associated with some application cache. 96 | """ 97 | return ( 98 | cls.build_send_payload("getFramesWithManifests", { 99 | }), 100 | cls.convert_payload({ 101 | "frameIds": { 102 | "class": [FrameWithManifest], 103 | "optional": False 104 | }, 105 | }) 106 | ) 107 | 108 | @classmethod 109 | def getManifestForFrame(cls, 110 | frameId: Union['Page.FrameId'], 111 | ): 112 | """Returns manifest URL for document in the given frame. 113 | :param frameId: Identifier of the frame containing document whose manifest is retrieved. 114 | :type frameId: Page.FrameId 115 | """ 116 | return ( 117 | cls.build_send_payload("getManifestForFrame", { 118 | "frameId": frameId, 119 | }), 120 | cls.convert_payload({ 121 | "manifestURL": { 122 | "class": str, 123 | "optional": False 124 | }, 125 | }) 126 | ) 127 | 128 | 129 | 130 | class ApplicationCacheStatusUpdatedEvent(BaseEvent): 131 | 132 | js_name = 'Applicationcache.applicationCacheStatusUpdated' 133 | hashable = ['frameId'] 134 | is_hashable = True 135 | 136 | def __init__(self, 137 | frameId: Union['Page.FrameId', dict], 138 | manifestURL: Union['str', dict], 139 | status: Union['int', dict], 140 | ): 141 | if isinstance(frameId, dict): 142 | frameId = Page.FrameId(**frameId) 143 | self.frameId = frameId 144 | if isinstance(manifestURL, dict): 145 | manifestURL = str(**manifestURL) 146 | self.manifestURL = manifestURL 147 | if isinstance(status, dict): 148 | status = int(**status) 149 | self.status = status 150 | 151 | @classmethod 152 | def build_hash(cls, frameId): 153 | kwargs = locals() 154 | kwargs.pop('cls') 155 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 156 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 157 | log.debug('generated hash = %s' % h) 158 | return h 159 | 160 | 161 | class NetworkStateUpdatedEvent(BaseEvent): 162 | 163 | js_name = 'Applicationcache.networkStateUpdated' 164 | hashable = [] 165 | is_hashable = False 166 | 167 | def __init__(self, 168 | isNowOnline: Union['bool', dict], 169 | ): 170 | if isinstance(isNowOnline, dict): 171 | isNowOnline = bool(**isNowOnline) 172 | self.isNowOnline = isNowOnline 173 | 174 | @classmethod 175 | def build_hash(cls): 176 | raise ValueError('Unable to build hash for non-hashable type') 177 | -------------------------------------------------------------------------------- /chromewhip/protocol/audits.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import network as Network 16 | 17 | class Audits(PayloadMixin): 18 | """ Audits domain allows investigation of page violations and possible improvements. 19 | """ 20 | @classmethod 21 | def getEncodedResponse(cls, 22 | requestId: Union['Network.RequestId'], 23 | encoding: Union['str'], 24 | quality: Optional['float'] = None, 25 | sizeOnly: Optional['bool'] = None, 26 | ): 27 | """Returns the response body and size if it were re-encoded with the specified settings. Only 28 | applies to images. 29 | :param requestId: Identifier of the network request to get content for. 30 | :type requestId: Network.RequestId 31 | :param encoding: The encoding to use. 32 | :type encoding: str 33 | :param quality: The quality of the encoding (0-1). (defaults to 1) 34 | :type quality: float 35 | :param sizeOnly: Whether to only return the size information (defaults to false). 36 | :type sizeOnly: bool 37 | """ 38 | return ( 39 | cls.build_send_payload("getEncodedResponse", { 40 | "requestId": requestId, 41 | "encoding": encoding, 42 | "quality": quality, 43 | "sizeOnly": sizeOnly, 44 | }), 45 | cls.convert_payload({ 46 | "body": { 47 | "class": str, 48 | "optional": True 49 | }, 50 | "originalSize": { 51 | "class": int, 52 | "optional": False 53 | }, 54 | "encodedSize": { 55 | "class": int, 56 | "optional": False 57 | }, 58 | }) 59 | ) 60 | 61 | -------------------------------------------------------------------------------- /chromewhip/protocol/backgroundservice.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # ServiceName: The Background Service that will be associated with the commands/events.Every Background Service operates independently, but they share the sameAPI. 17 | ServiceName = str 18 | 19 | # EventMetadata: A key-value pair for additional event information to pass along. 20 | class EventMetadata(ChromeTypeBase): 21 | def __init__(self, 22 | key: Union['str'], 23 | value: Union['str'], 24 | ): 25 | 26 | self.key = key 27 | self.value = value 28 | 29 | 30 | # BackgroundServiceEvent: 31 | class BackgroundServiceEvent(ChromeTypeBase): 32 | def __init__(self, 33 | timestamp: Union['Network.TimeSinceEpoch'], 34 | origin: Union['str'], 35 | serviceWorkerRegistrationId: Union['ServiceWorker.RegistrationID'], 36 | service: Union['ServiceName'], 37 | eventName: Union['str'], 38 | instanceId: Union['str'], 39 | eventMetadata: Union['[EventMetadata]'], 40 | ): 41 | 42 | self.timestamp = timestamp 43 | self.origin = origin 44 | self.serviceWorkerRegistrationId = serviceWorkerRegistrationId 45 | self.service = service 46 | self.eventName = eventName 47 | self.instanceId = instanceId 48 | self.eventMetadata = eventMetadata 49 | 50 | 51 | class BackgroundService(PayloadMixin): 52 | """ Defines events for background web platform features. 53 | """ 54 | @classmethod 55 | def startObserving(cls, 56 | service: Union['ServiceName'], 57 | ): 58 | """Enables event updates for the service. 59 | :param service: 60 | :type service: ServiceName 61 | """ 62 | return ( 63 | cls.build_send_payload("startObserving", { 64 | "service": service, 65 | }), 66 | None 67 | ) 68 | 69 | @classmethod 70 | def stopObserving(cls, 71 | service: Union['ServiceName'], 72 | ): 73 | """Disables event updates for the service. 74 | :param service: 75 | :type service: ServiceName 76 | """ 77 | return ( 78 | cls.build_send_payload("stopObserving", { 79 | "service": service, 80 | }), 81 | None 82 | ) 83 | 84 | @classmethod 85 | def setRecording(cls, 86 | shouldRecord: Union['bool'], 87 | service: Union['ServiceName'], 88 | ): 89 | """Set the recording state for the service. 90 | :param shouldRecord: 91 | :type shouldRecord: bool 92 | :param service: 93 | :type service: ServiceName 94 | """ 95 | return ( 96 | cls.build_send_payload("setRecording", { 97 | "shouldRecord": shouldRecord, 98 | "service": service, 99 | }), 100 | None 101 | ) 102 | 103 | @classmethod 104 | def clearEvents(cls, 105 | service: Union['ServiceName'], 106 | ): 107 | """Clears all stored data for the service. 108 | :param service: 109 | :type service: ServiceName 110 | """ 111 | return ( 112 | cls.build_send_payload("clearEvents", { 113 | "service": service, 114 | }), 115 | None 116 | ) 117 | 118 | 119 | 120 | class RecordingStateChangedEvent(BaseEvent): 121 | 122 | js_name = 'Backgroundservice.recordingStateChanged' 123 | hashable = [] 124 | is_hashable = False 125 | 126 | def __init__(self, 127 | isRecording: Union['bool', dict], 128 | service: Union['ServiceName', dict], 129 | ): 130 | if isinstance(isRecording, dict): 131 | isRecording = bool(**isRecording) 132 | self.isRecording = isRecording 133 | if isinstance(service, dict): 134 | service = ServiceName(**service) 135 | self.service = service 136 | 137 | @classmethod 138 | def build_hash(cls): 139 | raise ValueError('Unable to build hash for non-hashable type') 140 | 141 | 142 | class BackgroundServiceEventReceivedEvent(BaseEvent): 143 | 144 | js_name = 'Backgroundservice.backgroundServiceEventReceived' 145 | hashable = [] 146 | is_hashable = False 147 | 148 | def __init__(self, 149 | backgroundServiceEvent: Union['BackgroundServiceEvent', dict], 150 | ): 151 | if isinstance(backgroundServiceEvent, dict): 152 | backgroundServiceEvent = BackgroundServiceEvent(**backgroundServiceEvent) 153 | self.backgroundServiceEvent = backgroundServiceEvent 154 | 155 | @classmethod 156 | def build_hash(cls): 157 | raise ValueError('Unable to build hash for non-hashable type') 158 | -------------------------------------------------------------------------------- /chromewhip/protocol/cachestorage.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # CacheId: Unique identifier of the Cache object. 17 | CacheId = str 18 | 19 | # CachedResponseType: type of HTTP response cached 20 | CachedResponseType = str 21 | 22 | # DataEntry: Data entry. 23 | class DataEntry(ChromeTypeBase): 24 | def __init__(self, 25 | requestURL: Union['str'], 26 | requestMethod: Union['str'], 27 | requestHeaders: Union['[Header]'], 28 | responseTime: Union['float'], 29 | responseStatus: Union['int'], 30 | responseStatusText: Union['str'], 31 | responseType: Union['CachedResponseType'], 32 | responseHeaders: Union['[Header]'], 33 | ): 34 | 35 | self.requestURL = requestURL 36 | self.requestMethod = requestMethod 37 | self.requestHeaders = requestHeaders 38 | self.responseTime = responseTime 39 | self.responseStatus = responseStatus 40 | self.responseStatusText = responseStatusText 41 | self.responseType = responseType 42 | self.responseHeaders = responseHeaders 43 | 44 | 45 | # Cache: Cache identifier. 46 | class Cache(ChromeTypeBase): 47 | def __init__(self, 48 | cacheId: Union['CacheId'], 49 | securityOrigin: Union['str'], 50 | cacheName: Union['str'], 51 | ): 52 | 53 | self.cacheId = cacheId 54 | self.securityOrigin = securityOrigin 55 | self.cacheName = cacheName 56 | 57 | 58 | # Header: 59 | class Header(ChromeTypeBase): 60 | def __init__(self, 61 | name: Union['str'], 62 | value: Union['str'], 63 | ): 64 | 65 | self.name = name 66 | self.value = value 67 | 68 | 69 | # CachedResponse: Cached response 70 | class CachedResponse(ChromeTypeBase): 71 | def __init__(self, 72 | body: Union['str'], 73 | ): 74 | 75 | self.body = body 76 | 77 | 78 | class CacheStorage(PayloadMixin): 79 | """ 80 | """ 81 | @classmethod 82 | def deleteCache(cls, 83 | cacheId: Union['CacheId'], 84 | ): 85 | """Deletes a cache. 86 | :param cacheId: Id of cache for deletion. 87 | :type cacheId: CacheId 88 | """ 89 | return ( 90 | cls.build_send_payload("deleteCache", { 91 | "cacheId": cacheId, 92 | }), 93 | None 94 | ) 95 | 96 | @classmethod 97 | def deleteEntry(cls, 98 | cacheId: Union['CacheId'], 99 | request: Union['str'], 100 | ): 101 | """Deletes a cache entry. 102 | :param cacheId: Id of cache where the entry will be deleted. 103 | :type cacheId: CacheId 104 | :param request: URL spec of the request. 105 | :type request: str 106 | """ 107 | return ( 108 | cls.build_send_payload("deleteEntry", { 109 | "cacheId": cacheId, 110 | "request": request, 111 | }), 112 | None 113 | ) 114 | 115 | @classmethod 116 | def requestCacheNames(cls, 117 | securityOrigin: Union['str'], 118 | ): 119 | """Requests cache names. 120 | :param securityOrigin: Security origin. 121 | :type securityOrigin: str 122 | """ 123 | return ( 124 | cls.build_send_payload("requestCacheNames", { 125 | "securityOrigin": securityOrigin, 126 | }), 127 | cls.convert_payload({ 128 | "caches": { 129 | "class": [Cache], 130 | "optional": False 131 | }, 132 | }) 133 | ) 134 | 135 | @classmethod 136 | def requestCachedResponse(cls, 137 | cacheId: Union['CacheId'], 138 | requestURL: Union['str'], 139 | requestHeaders: Union['[Header]'], 140 | ): 141 | """Fetches cache entry. 142 | :param cacheId: Id of cache that contains the entry. 143 | :type cacheId: CacheId 144 | :param requestURL: URL spec of the request. 145 | :type requestURL: str 146 | :param requestHeaders: headers of the request. 147 | :type requestHeaders: [Header] 148 | """ 149 | return ( 150 | cls.build_send_payload("requestCachedResponse", { 151 | "cacheId": cacheId, 152 | "requestURL": requestURL, 153 | "requestHeaders": requestHeaders, 154 | }), 155 | cls.convert_payload({ 156 | "response": { 157 | "class": CachedResponse, 158 | "optional": False 159 | }, 160 | }) 161 | ) 162 | 163 | @classmethod 164 | def requestEntries(cls, 165 | cacheId: Union['CacheId'], 166 | skipCount: Union['int'], 167 | pageSize: Union['int'], 168 | pathFilter: Optional['str'] = None, 169 | ): 170 | """Requests data from cache. 171 | :param cacheId: ID of cache to get entries from. 172 | :type cacheId: CacheId 173 | :param skipCount: Number of records to skip. 174 | :type skipCount: int 175 | :param pageSize: Number of records to fetch. 176 | :type pageSize: int 177 | :param pathFilter: If present, only return the entries containing this substring in the path 178 | :type pathFilter: str 179 | """ 180 | return ( 181 | cls.build_send_payload("requestEntries", { 182 | "cacheId": cacheId, 183 | "skipCount": skipCount, 184 | "pageSize": pageSize, 185 | "pathFilter": pathFilter, 186 | }), 187 | cls.convert_payload({ 188 | "cacheDataEntries": { 189 | "class": [DataEntry], 190 | "optional": False 191 | }, 192 | "returnCount": { 193 | "class": float, 194 | "optional": False 195 | }, 196 | }) 197 | ) 198 | 199 | -------------------------------------------------------------------------------- /chromewhip/protocol/cast.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # Sink: 17 | class Sink(ChromeTypeBase): 18 | def __init__(self, 19 | name: Union['str'], 20 | id: Union['str'], 21 | session: Optional['str'] = None, 22 | ): 23 | 24 | self.name = name 25 | self.id = id 26 | self.session = session 27 | 28 | 29 | class Cast(PayloadMixin): 30 | """ A domain for interacting with Cast, Presentation API, and Remote Playback API 31 | functionalities. 32 | """ 33 | @classmethod 34 | def enable(cls, 35 | presentationUrl: Optional['str'] = None, 36 | ): 37 | """Starts observing for sinks that can be used for tab mirroring, and if set, 38 | sinks compatible with |presentationUrl| as well. When sinks are found, a 39 | |sinksUpdated| event is fired. 40 | Also starts observing for issue messages. When an issue is added or removed, 41 | an |issueUpdated| event is fired. 42 | :param presentationUrl: 43 | :type presentationUrl: str 44 | """ 45 | return ( 46 | cls.build_send_payload("enable", { 47 | "presentationUrl": presentationUrl, 48 | }), 49 | None 50 | ) 51 | 52 | @classmethod 53 | def disable(cls): 54 | """Stops observing for sinks and issues. 55 | """ 56 | return ( 57 | cls.build_send_payload("disable", { 58 | }), 59 | None 60 | ) 61 | 62 | @classmethod 63 | def setSinkToUse(cls, 64 | sinkName: Union['str'], 65 | ): 66 | """Sets a sink to be used when the web page requests the browser to choose a 67 | sink via Presentation API, Remote Playback API, or Cast SDK. 68 | :param sinkName: 69 | :type sinkName: str 70 | """ 71 | return ( 72 | cls.build_send_payload("setSinkToUse", { 73 | "sinkName": sinkName, 74 | }), 75 | None 76 | ) 77 | 78 | @classmethod 79 | def startTabMirroring(cls, 80 | sinkName: Union['str'], 81 | ): 82 | """Starts mirroring the tab to the sink. 83 | :param sinkName: 84 | :type sinkName: str 85 | """ 86 | return ( 87 | cls.build_send_payload("startTabMirroring", { 88 | "sinkName": sinkName, 89 | }), 90 | None 91 | ) 92 | 93 | @classmethod 94 | def stopCasting(cls, 95 | sinkName: Union['str'], 96 | ): 97 | """Stops the active Cast session on the sink. 98 | :param sinkName: 99 | :type sinkName: str 100 | """ 101 | return ( 102 | cls.build_send_payload("stopCasting", { 103 | "sinkName": sinkName, 104 | }), 105 | None 106 | ) 107 | 108 | 109 | 110 | class SinksUpdatedEvent(BaseEvent): 111 | 112 | js_name = 'Cast.sinksUpdated' 113 | hashable = [] 114 | is_hashable = False 115 | 116 | def __init__(self, 117 | sinks: Union['[Sink]', dict], 118 | ): 119 | if isinstance(sinks, dict): 120 | sinks = [Sink](**sinks) 121 | self.sinks = sinks 122 | 123 | @classmethod 124 | def build_hash(cls): 125 | raise ValueError('Unable to build hash for non-hashable type') 126 | 127 | 128 | class IssueUpdatedEvent(BaseEvent): 129 | 130 | js_name = 'Cast.issueUpdated' 131 | hashable = [] 132 | is_hashable = False 133 | 134 | def __init__(self, 135 | issueMessage: Union['str', dict], 136 | ): 137 | if isinstance(issueMessage, dict): 138 | issueMessage = str(**issueMessage) 139 | self.issueMessage = issueMessage 140 | 141 | @classmethod 142 | def build_hash(cls): 143 | raise ValueError('Unable to build hash for non-hashable type') 144 | -------------------------------------------------------------------------------- /chromewhip/protocol/console.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import runtime as Runtime 16 | 17 | # ConsoleMessage: Console message. 18 | class ConsoleMessage(ChromeTypeBase): 19 | def __init__(self, 20 | source: Union['str'], 21 | level: Union['str'], 22 | text: Union['str'], 23 | url: Optional['str'] = None, 24 | line: Optional['int'] = None, 25 | column: Optional['int'] = None, 26 | ): 27 | 28 | self.source = source 29 | self.level = level 30 | self.text = text 31 | self.url = url 32 | self.line = line 33 | self.column = column 34 | 35 | 36 | class Console(PayloadMixin): 37 | """ This domain is deprecated - use Runtime or Log instead. 38 | """ 39 | @classmethod 40 | def clearMessages(cls): 41 | """Does nothing. 42 | """ 43 | return ( 44 | cls.build_send_payload("clearMessages", { 45 | }), 46 | None 47 | ) 48 | 49 | @classmethod 50 | def disable(cls): 51 | """Disables console domain, prevents further console messages from being reported to the client. 52 | """ 53 | return ( 54 | cls.build_send_payload("disable", { 55 | }), 56 | None 57 | ) 58 | 59 | @classmethod 60 | def enable(cls): 61 | """Enables console domain, sends the messages collected so far to the client by means of the 62 | `messageAdded` notification. 63 | """ 64 | return ( 65 | cls.build_send_payload("enable", { 66 | }), 67 | None 68 | ) 69 | 70 | 71 | 72 | class MessageAddedEvent(BaseEvent): 73 | 74 | js_name = 'Console.messageAdded' 75 | hashable = [] 76 | is_hashable = False 77 | 78 | def __init__(self, 79 | message: Union['ConsoleMessage', dict], 80 | ): 81 | if isinstance(message, dict): 82 | message = ConsoleMessage(**message) 83 | self.message = message 84 | 85 | @classmethod 86 | def build_hash(cls): 87 | raise ValueError('Unable to build hash for non-hashable type') 88 | -------------------------------------------------------------------------------- /chromewhip/protocol/database.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # DatabaseId: Unique identifier of Database object. 17 | DatabaseId = str 18 | 19 | # Database: Database object. 20 | class Database(ChromeTypeBase): 21 | def __init__(self, 22 | id: Union['DatabaseId'], 23 | domain: Union['str'], 24 | name: Union['str'], 25 | version: Union['str'], 26 | ): 27 | 28 | self.id = id 29 | self.domain = domain 30 | self.name = name 31 | self.version = version 32 | 33 | 34 | # Error: Database error. 35 | class Error(ChromeTypeBase): 36 | def __init__(self, 37 | message: Union['str'], 38 | code: Union['int'], 39 | ): 40 | 41 | self.message = message 42 | self.code = code 43 | 44 | 45 | class Database(PayloadMixin): 46 | """ 47 | """ 48 | @classmethod 49 | def disable(cls): 50 | """Disables database tracking, prevents database events from being sent to the client. 51 | """ 52 | return ( 53 | cls.build_send_payload("disable", { 54 | }), 55 | None 56 | ) 57 | 58 | @classmethod 59 | def enable(cls): 60 | """Enables database tracking, database events will now be delivered to the client. 61 | """ 62 | return ( 63 | cls.build_send_payload("enable", { 64 | }), 65 | None 66 | ) 67 | 68 | @classmethod 69 | def executeSQL(cls, 70 | databaseId: Union['DatabaseId'], 71 | query: Union['str'], 72 | ): 73 | """ 74 | :param databaseId: 75 | :type databaseId: DatabaseId 76 | :param query: 77 | :type query: str 78 | """ 79 | return ( 80 | cls.build_send_payload("executeSQL", { 81 | "databaseId": databaseId, 82 | "query": query, 83 | }), 84 | cls.convert_payload({ 85 | "columnNames": { 86 | "class": [], 87 | "optional": True 88 | }, 89 | "values": { 90 | "class": [], 91 | "optional": True 92 | }, 93 | "sqlError": { 94 | "class": Error, 95 | "optional": True 96 | }, 97 | }) 98 | ) 99 | 100 | @classmethod 101 | def getDatabaseTableNames(cls, 102 | databaseId: Union['DatabaseId'], 103 | ): 104 | """ 105 | :param databaseId: 106 | :type databaseId: DatabaseId 107 | """ 108 | return ( 109 | cls.build_send_payload("getDatabaseTableNames", { 110 | "databaseId": databaseId, 111 | }), 112 | cls.convert_payload({ 113 | "tableNames": { 114 | "class": [], 115 | "optional": False 116 | }, 117 | }) 118 | ) 119 | 120 | 121 | 122 | class AddDatabaseEvent(BaseEvent): 123 | 124 | js_name = 'Database.addDatabase' 125 | hashable = ['databaseId'] 126 | is_hashable = True 127 | 128 | def __init__(self, 129 | database: Union['Database', dict], 130 | ): 131 | if isinstance(database, dict): 132 | database = Database(**database) 133 | self.database = database 134 | 135 | @classmethod 136 | def build_hash(cls, databaseId): 137 | kwargs = locals() 138 | kwargs.pop('cls') 139 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 140 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 141 | log.debug('generated hash = %s' % h) 142 | return h 143 | -------------------------------------------------------------------------------- /chromewhip/protocol/deviceorientation.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | class DeviceOrientation(PayloadMixin): 17 | """ 18 | """ 19 | @classmethod 20 | def clearDeviceOrientationOverride(cls): 21 | """Clears the overridden Device Orientation. 22 | """ 23 | return ( 24 | cls.build_send_payload("clearDeviceOrientationOverride", { 25 | }), 26 | None 27 | ) 28 | 29 | @classmethod 30 | def setDeviceOrientationOverride(cls, 31 | alpha: Union['float'], 32 | beta: Union['float'], 33 | gamma: Union['float'], 34 | ): 35 | """Overrides the Device Orientation. 36 | :param alpha: Mock alpha 37 | :type alpha: float 38 | :param beta: Mock beta 39 | :type beta: float 40 | :param gamma: Mock gamma 41 | :type gamma: float 42 | """ 43 | return ( 44 | cls.build_send_payload("setDeviceOrientationOverride", { 45 | "alpha": alpha, 46 | "beta": beta, 47 | "gamma": gamma, 48 | }), 49 | None 50 | ) 51 | 52 | -------------------------------------------------------------------------------- /chromewhip/protocol/domdebugger.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import dom as DOM 16 | from chromewhip.protocol import debugger as Debugger 17 | from chromewhip.protocol import runtime as Runtime 18 | 19 | # DOMBreakpointType: DOM breakpoint type. 20 | DOMBreakpointType = str 21 | 22 | # EventListener: Object event listener. 23 | class EventListener(ChromeTypeBase): 24 | def __init__(self, 25 | type: Union['str'], 26 | useCapture: Union['bool'], 27 | passive: Union['bool'], 28 | once: Union['bool'], 29 | scriptId: Union['Runtime.ScriptId'], 30 | lineNumber: Union['int'], 31 | columnNumber: Union['int'], 32 | handler: Optional['Runtime.RemoteObject'] = None, 33 | originalHandler: Optional['Runtime.RemoteObject'] = None, 34 | backendNodeId: Optional['DOM.BackendNodeId'] = None, 35 | ): 36 | 37 | self.type = type 38 | self.useCapture = useCapture 39 | self.passive = passive 40 | self.once = once 41 | self.scriptId = scriptId 42 | self.lineNumber = lineNumber 43 | self.columnNumber = columnNumber 44 | self.handler = handler 45 | self.originalHandler = originalHandler 46 | self.backendNodeId = backendNodeId 47 | 48 | 49 | class DOMDebugger(PayloadMixin): 50 | """ DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript 51 | execution will stop on these operations as if there was a regular breakpoint set. 52 | """ 53 | @classmethod 54 | def getEventListeners(cls, 55 | objectId: Union['Runtime.RemoteObjectId'], 56 | depth: Optional['int'] = None, 57 | pierce: Optional['bool'] = None, 58 | ): 59 | """Returns event listeners of the given object. 60 | :param objectId: Identifier of the object to return listeners for. 61 | :type objectId: Runtime.RemoteObjectId 62 | :param depth: The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the 63 | entire subtree or provide an integer larger than 0. 64 | :type depth: int 65 | :param pierce: Whether or not iframes and shadow roots should be traversed when returning the subtree 66 | (default is false). Reports listeners for all contexts if pierce is enabled. 67 | :type pierce: bool 68 | """ 69 | return ( 70 | cls.build_send_payload("getEventListeners", { 71 | "objectId": objectId, 72 | "depth": depth, 73 | "pierce": pierce, 74 | }), 75 | cls.convert_payload({ 76 | "listeners": { 77 | "class": [EventListener], 78 | "optional": False 79 | }, 80 | }) 81 | ) 82 | 83 | @classmethod 84 | def removeDOMBreakpoint(cls, 85 | nodeId: Union['DOM.NodeId'], 86 | type: Union['DOMBreakpointType'], 87 | ): 88 | """Removes DOM breakpoint that was set using `setDOMBreakpoint`. 89 | :param nodeId: Identifier of the node to remove breakpoint from. 90 | :type nodeId: DOM.NodeId 91 | :param type: Type of the breakpoint to remove. 92 | :type type: DOMBreakpointType 93 | """ 94 | return ( 95 | cls.build_send_payload("removeDOMBreakpoint", { 96 | "nodeId": nodeId, 97 | "type": type, 98 | }), 99 | None 100 | ) 101 | 102 | @classmethod 103 | def removeEventListenerBreakpoint(cls, 104 | eventName: Union['str'], 105 | targetName: Optional['str'] = None, 106 | ): 107 | """Removes breakpoint on particular DOM event. 108 | :param eventName: Event name. 109 | :type eventName: str 110 | :param targetName: EventTarget interface name. 111 | :type targetName: str 112 | """ 113 | return ( 114 | cls.build_send_payload("removeEventListenerBreakpoint", { 115 | "eventName": eventName, 116 | "targetName": targetName, 117 | }), 118 | None 119 | ) 120 | 121 | @classmethod 122 | def removeInstrumentationBreakpoint(cls, 123 | eventName: Union['str'], 124 | ): 125 | """Removes breakpoint on particular native event. 126 | :param eventName: Instrumentation name to stop on. 127 | :type eventName: str 128 | """ 129 | return ( 130 | cls.build_send_payload("removeInstrumentationBreakpoint", { 131 | "eventName": eventName, 132 | }), 133 | None 134 | ) 135 | 136 | @classmethod 137 | def removeXHRBreakpoint(cls, 138 | url: Union['str'], 139 | ): 140 | """Removes breakpoint from XMLHttpRequest. 141 | :param url: Resource URL substring. 142 | :type url: str 143 | """ 144 | return ( 145 | cls.build_send_payload("removeXHRBreakpoint", { 146 | "url": url, 147 | }), 148 | None 149 | ) 150 | 151 | @classmethod 152 | def setDOMBreakpoint(cls, 153 | nodeId: Union['DOM.NodeId'], 154 | type: Union['DOMBreakpointType'], 155 | ): 156 | """Sets breakpoint on particular operation with DOM. 157 | :param nodeId: Identifier of the node to set breakpoint on. 158 | :type nodeId: DOM.NodeId 159 | :param type: Type of the operation to stop upon. 160 | :type type: DOMBreakpointType 161 | """ 162 | return ( 163 | cls.build_send_payload("setDOMBreakpoint", { 164 | "nodeId": nodeId, 165 | "type": type, 166 | }), 167 | None 168 | ) 169 | 170 | @classmethod 171 | def setEventListenerBreakpoint(cls, 172 | eventName: Union['str'], 173 | targetName: Optional['str'] = None, 174 | ): 175 | """Sets breakpoint on particular DOM event. 176 | :param eventName: DOM Event name to stop on (any DOM event will do). 177 | :type eventName: str 178 | :param targetName: EventTarget interface name to stop on. If equal to `"*"` or not provided, will stop on any 179 | EventTarget. 180 | :type targetName: str 181 | """ 182 | return ( 183 | cls.build_send_payload("setEventListenerBreakpoint", { 184 | "eventName": eventName, 185 | "targetName": targetName, 186 | }), 187 | None 188 | ) 189 | 190 | @classmethod 191 | def setInstrumentationBreakpoint(cls, 192 | eventName: Union['str'], 193 | ): 194 | """Sets breakpoint on particular native event. 195 | :param eventName: Instrumentation name to stop on. 196 | :type eventName: str 197 | """ 198 | return ( 199 | cls.build_send_payload("setInstrumentationBreakpoint", { 200 | "eventName": eventName, 201 | }), 202 | None 203 | ) 204 | 205 | @classmethod 206 | def setXHRBreakpoint(cls, 207 | url: Union['str'], 208 | ): 209 | """Sets breakpoint on XMLHttpRequest. 210 | :param url: Resource URL substring. All XHRs having this substring in the URL will get stopped upon. 211 | :type url: str 212 | """ 213 | return ( 214 | cls.build_send_payload("setXHRBreakpoint", { 215 | "url": url, 216 | }), 217 | None 218 | ) 219 | 220 | -------------------------------------------------------------------------------- /chromewhip/protocol/domstorage.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # StorageId: DOM Storage identifier. 17 | class StorageId(ChromeTypeBase): 18 | def __init__(self, 19 | securityOrigin: Union['str'], 20 | isLocalStorage: Union['bool'], 21 | ): 22 | 23 | self.securityOrigin = securityOrigin 24 | self.isLocalStorage = isLocalStorage 25 | 26 | 27 | # Item: DOM Storage item. 28 | Item = [str] 29 | 30 | class DOMStorage(PayloadMixin): 31 | """ Query and modify DOM storage. 32 | """ 33 | @classmethod 34 | def clear(cls, 35 | storageId: Union['StorageId'], 36 | ): 37 | """ 38 | :param storageId: 39 | :type storageId: StorageId 40 | """ 41 | return ( 42 | cls.build_send_payload("clear", { 43 | "storageId": storageId, 44 | }), 45 | None 46 | ) 47 | 48 | @classmethod 49 | def disable(cls): 50 | """Disables storage tracking, prevents storage events from being sent to the client. 51 | """ 52 | return ( 53 | cls.build_send_payload("disable", { 54 | }), 55 | None 56 | ) 57 | 58 | @classmethod 59 | def enable(cls): 60 | """Enables storage tracking, storage events will now be delivered to the client. 61 | """ 62 | return ( 63 | cls.build_send_payload("enable", { 64 | }), 65 | None 66 | ) 67 | 68 | @classmethod 69 | def getDOMStorageItems(cls, 70 | storageId: Union['StorageId'], 71 | ): 72 | """ 73 | :param storageId: 74 | :type storageId: StorageId 75 | """ 76 | return ( 77 | cls.build_send_payload("getDOMStorageItems", { 78 | "storageId": storageId, 79 | }), 80 | cls.convert_payload({ 81 | "entries": { 82 | "class": [Item], 83 | "optional": False 84 | }, 85 | }) 86 | ) 87 | 88 | @classmethod 89 | def removeDOMStorageItem(cls, 90 | storageId: Union['StorageId'], 91 | key: Union['str'], 92 | ): 93 | """ 94 | :param storageId: 95 | :type storageId: StorageId 96 | :param key: 97 | :type key: str 98 | """ 99 | return ( 100 | cls.build_send_payload("removeDOMStorageItem", { 101 | "storageId": storageId, 102 | "key": key, 103 | }), 104 | None 105 | ) 106 | 107 | @classmethod 108 | def setDOMStorageItem(cls, 109 | storageId: Union['StorageId'], 110 | key: Union['str'], 111 | value: Union['str'], 112 | ): 113 | """ 114 | :param storageId: 115 | :type storageId: StorageId 116 | :param key: 117 | :type key: str 118 | :param value: 119 | :type value: str 120 | """ 121 | return ( 122 | cls.build_send_payload("setDOMStorageItem", { 123 | "storageId": storageId, 124 | "key": key, 125 | "value": value, 126 | }), 127 | None 128 | ) 129 | 130 | 131 | 132 | class DomStorageItemAddedEvent(BaseEvent): 133 | 134 | js_name = 'Domstorage.domStorageItemAdded' 135 | hashable = ['storageId'] 136 | is_hashable = True 137 | 138 | def __init__(self, 139 | storageId: Union['StorageId', dict], 140 | key: Union['str', dict], 141 | newValue: Union['str', dict], 142 | ): 143 | if isinstance(storageId, dict): 144 | storageId = StorageId(**storageId) 145 | self.storageId = storageId 146 | if isinstance(key, dict): 147 | key = str(**key) 148 | self.key = key 149 | if isinstance(newValue, dict): 150 | newValue = str(**newValue) 151 | self.newValue = newValue 152 | 153 | @classmethod 154 | def build_hash(cls, storageId): 155 | kwargs = locals() 156 | kwargs.pop('cls') 157 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 158 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 159 | log.debug('generated hash = %s' % h) 160 | return h 161 | 162 | 163 | class DomStorageItemRemovedEvent(BaseEvent): 164 | 165 | js_name = 'Domstorage.domStorageItemRemoved' 166 | hashable = ['storageId'] 167 | is_hashable = True 168 | 169 | def __init__(self, 170 | storageId: Union['StorageId', dict], 171 | key: Union['str', dict], 172 | ): 173 | if isinstance(storageId, dict): 174 | storageId = StorageId(**storageId) 175 | self.storageId = storageId 176 | if isinstance(key, dict): 177 | key = str(**key) 178 | self.key = key 179 | 180 | @classmethod 181 | def build_hash(cls, storageId): 182 | kwargs = locals() 183 | kwargs.pop('cls') 184 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 185 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 186 | log.debug('generated hash = %s' % h) 187 | return h 188 | 189 | 190 | class DomStorageItemUpdatedEvent(BaseEvent): 191 | 192 | js_name = 'Domstorage.domStorageItemUpdated' 193 | hashable = ['storageId'] 194 | is_hashable = True 195 | 196 | def __init__(self, 197 | storageId: Union['StorageId', dict], 198 | key: Union['str', dict], 199 | oldValue: Union['str', dict], 200 | newValue: Union['str', dict], 201 | ): 202 | if isinstance(storageId, dict): 203 | storageId = StorageId(**storageId) 204 | self.storageId = storageId 205 | if isinstance(key, dict): 206 | key = str(**key) 207 | self.key = key 208 | if isinstance(oldValue, dict): 209 | oldValue = str(**oldValue) 210 | self.oldValue = oldValue 211 | if isinstance(newValue, dict): 212 | newValue = str(**newValue) 213 | self.newValue = newValue 214 | 215 | @classmethod 216 | def build_hash(cls, storageId): 217 | kwargs = locals() 218 | kwargs.pop('cls') 219 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 220 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 221 | log.debug('generated hash = %s' % h) 222 | return h 223 | 224 | 225 | class DomStorageItemsClearedEvent(BaseEvent): 226 | 227 | js_name = 'Domstorage.domStorageItemsCleared' 228 | hashable = ['storageId'] 229 | is_hashable = True 230 | 231 | def __init__(self, 232 | storageId: Union['StorageId', dict], 233 | ): 234 | if isinstance(storageId, dict): 235 | storageId = StorageId(**storageId) 236 | self.storageId = storageId 237 | 238 | @classmethod 239 | def build_hash(cls, storageId): 240 | kwargs = locals() 241 | kwargs.pop('cls') 242 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 243 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 244 | log.debug('generated hash = %s' % h) 245 | return h 246 | -------------------------------------------------------------------------------- /chromewhip/protocol/headlessexperimental.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import page as Page 16 | from chromewhip.protocol import runtime as Runtime 17 | 18 | # ScreenshotParams: Encoding options for a screenshot. 19 | class ScreenshotParams(ChromeTypeBase): 20 | def __init__(self, 21 | format: Optional['str'] = None, 22 | quality: Optional['int'] = None, 23 | ): 24 | 25 | self.format = format 26 | self.quality = quality 27 | 28 | 29 | class HeadlessExperimental(PayloadMixin): 30 | """ This domain provides experimental commands only supported in headless mode. 31 | """ 32 | @classmethod 33 | def beginFrame(cls, 34 | frameTimeTicks: Optional['float'] = None, 35 | interval: Optional['float'] = None, 36 | noDisplayUpdates: Optional['bool'] = None, 37 | screenshot: Optional['ScreenshotParams'] = None, 38 | ): 39 | """Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a 40 | screenshot from the resulting frame. Requires that the target was created with enabled 41 | BeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also 42 | https://goo.gl/3zHXhB for more background. 43 | :param frameTimeTicks: Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set, 44 | the current time will be used. 45 | :type frameTimeTicks: float 46 | :param interval: The interval between BeginFrames that is reported to the compositor, in milliseconds. 47 | Defaults to a 60 frames/second interval, i.e. about 16.666 milliseconds. 48 | :type interval: float 49 | :param noDisplayUpdates: Whether updates should not be committed and drawn onto the display. False by default. If 50 | true, only side effects of the BeginFrame will be run, such as layout and animations, but 51 | any visual updates may not be visible on the display or in screenshots. 52 | :type noDisplayUpdates: bool 53 | :param screenshot: If set, a screenshot of the frame will be captured and returned in the response. Otherwise, 54 | no screenshot will be captured. Note that capturing a screenshot can fail, for example, 55 | during renderer initialization. In such a case, no screenshot data will be returned. 56 | :type screenshot: ScreenshotParams 57 | """ 58 | return ( 59 | cls.build_send_payload("beginFrame", { 60 | "frameTimeTicks": frameTimeTicks, 61 | "interval": interval, 62 | "noDisplayUpdates": noDisplayUpdates, 63 | "screenshot": screenshot, 64 | }), 65 | cls.convert_payload({ 66 | "hasDamage": { 67 | "class": bool, 68 | "optional": False 69 | }, 70 | "screenshotData": { 71 | "class": str, 72 | "optional": True 73 | }, 74 | }) 75 | ) 76 | 77 | @classmethod 78 | def disable(cls): 79 | """Disables headless events for the target. 80 | """ 81 | return ( 82 | cls.build_send_payload("disable", { 83 | }), 84 | None 85 | ) 86 | 87 | @classmethod 88 | def enable(cls): 89 | """Enables headless events for the target. 90 | """ 91 | return ( 92 | cls.build_send_payload("enable", { 93 | }), 94 | None 95 | ) 96 | 97 | 98 | 99 | class NeedsBeginFramesChangedEvent(BaseEvent): 100 | 101 | js_name = 'Headlessexperimental.needsBeginFramesChanged' 102 | hashable = [] 103 | is_hashable = False 104 | 105 | def __init__(self, 106 | needsBeginFrames: Union['bool', dict], 107 | ): 108 | if isinstance(needsBeginFrames, dict): 109 | needsBeginFrames = bool(**needsBeginFrames) 110 | self.needsBeginFrames = needsBeginFrames 111 | 112 | @classmethod 113 | def build_hash(cls): 114 | raise ValueError('Unable to build hash for non-hashable type') 115 | -------------------------------------------------------------------------------- /chromewhip/protocol/inspector.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | class Inspector(PayloadMixin): 17 | """ 18 | """ 19 | @classmethod 20 | def disable(cls): 21 | """Disables inspector domain notifications. 22 | """ 23 | return ( 24 | cls.build_send_payload("disable", { 25 | }), 26 | None 27 | ) 28 | 29 | @classmethod 30 | def enable(cls): 31 | """Enables inspector domain notifications. 32 | """ 33 | return ( 34 | cls.build_send_payload("enable", { 35 | }), 36 | None 37 | ) 38 | 39 | 40 | 41 | class DetachedEvent(BaseEvent): 42 | 43 | js_name = 'Inspector.detached' 44 | hashable = [] 45 | is_hashable = False 46 | 47 | def __init__(self, 48 | reason: Union['str', dict], 49 | ): 50 | if isinstance(reason, dict): 51 | reason = str(**reason) 52 | self.reason = reason 53 | 54 | @classmethod 55 | def build_hash(cls): 56 | raise ValueError('Unable to build hash for non-hashable type') 57 | 58 | 59 | class TargetCrashedEvent(BaseEvent): 60 | 61 | js_name = 'Inspector.targetCrashed' 62 | hashable = [] 63 | is_hashable = False 64 | 65 | def __init__(self): 66 | pass 67 | 68 | @classmethod 69 | def build_hash(cls): 70 | raise ValueError('Unable to build hash for non-hashable type') 71 | 72 | 73 | class TargetReloadedAfterCrashEvent(BaseEvent): 74 | 75 | js_name = 'Inspector.targetReloadedAfterCrash' 76 | hashable = [] 77 | is_hashable = False 78 | 79 | def __init__(self): 80 | pass 81 | 82 | @classmethod 83 | def build_hash(cls): 84 | raise ValueError('Unable to build hash for non-hashable type') 85 | -------------------------------------------------------------------------------- /chromewhip/protocol/io.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # StreamHandle: This is either obtained from another method or specifed as `blob:<uuid>` where`<uuid>` is an UUID of a Blob. 17 | StreamHandle = str 18 | 19 | class IO(PayloadMixin): 20 | """ Input/Output operations for streams produced by DevTools. 21 | """ 22 | @classmethod 23 | def close(cls, 24 | handle: Union['StreamHandle'], 25 | ): 26 | """Close the stream, discard any temporary backing storage. 27 | :param handle: Handle of the stream to close. 28 | :type handle: StreamHandle 29 | """ 30 | return ( 31 | cls.build_send_payload("close", { 32 | "handle": handle, 33 | }), 34 | None 35 | ) 36 | 37 | @classmethod 38 | def read(cls, 39 | handle: Union['StreamHandle'], 40 | offset: Optional['int'] = None, 41 | size: Optional['int'] = None, 42 | ): 43 | """Read a chunk of the stream 44 | :param handle: Handle of the stream to read. 45 | :type handle: StreamHandle 46 | :param offset: Seek to the specified offset before reading (if not specificed, proceed with offset 47 | following the last read). Some types of streams may only support sequential reads. 48 | :type offset: int 49 | :param size: Maximum number of bytes to read (left upon the agent discretion if not specified). 50 | :type size: int 51 | """ 52 | return ( 53 | cls.build_send_payload("read", { 54 | "handle": handle, 55 | "offset": offset, 56 | "size": size, 57 | }), 58 | cls.convert_payload({ 59 | "base64Encoded": { 60 | "class": bool, 61 | "optional": True 62 | }, 63 | "data": { 64 | "class": str, 65 | "optional": False 66 | }, 67 | "eof": { 68 | "class": bool, 69 | "optional": False 70 | }, 71 | }) 72 | ) 73 | 74 | @classmethod 75 | def resolveBlob(cls, 76 | objectId: Union['Runtime.RemoteObjectId'], 77 | ): 78 | """Return UUID of Blob object specified by a remote object id. 79 | :param objectId: Object id of a Blob object wrapper. 80 | :type objectId: Runtime.RemoteObjectId 81 | """ 82 | return ( 83 | cls.build_send_payload("resolveBlob", { 84 | "objectId": objectId, 85 | }), 86 | cls.convert_payload({ 87 | "uuid": { 88 | "class": str, 89 | "optional": False 90 | }, 91 | }) 92 | ) 93 | 94 | -------------------------------------------------------------------------------- /chromewhip/protocol/log.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import runtime as Runtime 16 | from chromewhip.protocol import network as Network 17 | 18 | # LogEntry: Log entry. 19 | class LogEntry(ChromeTypeBase): 20 | def __init__(self, 21 | source: Union['str'], 22 | level: Union['str'], 23 | text: Union['str'], 24 | timestamp: Union['Runtime.Timestamp'], 25 | url: Optional['str'] = None, 26 | lineNumber: Optional['int'] = None, 27 | stackTrace: Optional['Runtime.StackTrace'] = None, 28 | networkRequestId: Optional['Network.RequestId'] = None, 29 | workerId: Optional['str'] = None, 30 | args: Optional['[Runtime.RemoteObject]'] = None, 31 | ): 32 | 33 | self.source = source 34 | self.level = level 35 | self.text = text 36 | self.timestamp = timestamp 37 | self.url = url 38 | self.lineNumber = lineNumber 39 | self.stackTrace = stackTrace 40 | self.networkRequestId = networkRequestId 41 | self.workerId = workerId 42 | self.args = args 43 | 44 | 45 | # ViolationSetting: Violation configuration setting. 46 | class ViolationSetting(ChromeTypeBase): 47 | def __init__(self, 48 | name: Union['str'], 49 | threshold: Union['float'], 50 | ): 51 | 52 | self.name = name 53 | self.threshold = threshold 54 | 55 | 56 | class Log(PayloadMixin): 57 | """ Provides access to log entries. 58 | """ 59 | @classmethod 60 | def clear(cls): 61 | """Clears the log. 62 | """ 63 | return ( 64 | cls.build_send_payload("clear", { 65 | }), 66 | None 67 | ) 68 | 69 | @classmethod 70 | def disable(cls): 71 | """Disables log domain, prevents further log entries from being reported to the client. 72 | """ 73 | return ( 74 | cls.build_send_payload("disable", { 75 | }), 76 | None 77 | ) 78 | 79 | @classmethod 80 | def enable(cls): 81 | """Enables log domain, sends the entries collected so far to the client by means of the 82 | `entryAdded` notification. 83 | """ 84 | return ( 85 | cls.build_send_payload("enable", { 86 | }), 87 | None 88 | ) 89 | 90 | @classmethod 91 | def startViolationsReport(cls, 92 | config: Union['[ViolationSetting]'], 93 | ): 94 | """start violation reporting. 95 | :param config: Configuration for violations. 96 | :type config: [ViolationSetting] 97 | """ 98 | return ( 99 | cls.build_send_payload("startViolationsReport", { 100 | "config": config, 101 | }), 102 | None 103 | ) 104 | 105 | @classmethod 106 | def stopViolationsReport(cls): 107 | """Stop violation reporting. 108 | """ 109 | return ( 110 | cls.build_send_payload("stopViolationsReport", { 111 | }), 112 | None 113 | ) 114 | 115 | 116 | 117 | class EntryAddedEvent(BaseEvent): 118 | 119 | js_name = 'Log.entryAdded' 120 | hashable = [] 121 | is_hashable = False 122 | 123 | def __init__(self, 124 | entry: Union['LogEntry', dict], 125 | ): 126 | if isinstance(entry, dict): 127 | entry = LogEntry(**entry) 128 | self.entry = entry 129 | 130 | @classmethod 131 | def build_hash(cls): 132 | raise ValueError('Unable to build hash for non-hashable type') 133 | -------------------------------------------------------------------------------- /chromewhip/protocol/media.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # PlayerId: Players will get an ID that is unique within the agent context. 17 | PlayerId = str 18 | 19 | # Timestamp: 20 | Timestamp = float 21 | 22 | # PlayerProperty: Player Property type 23 | class PlayerProperty(ChromeTypeBase): 24 | def __init__(self, 25 | name: Union['str'], 26 | value: Optional['str'] = None, 27 | ): 28 | 29 | self.name = name 30 | self.value = value 31 | 32 | 33 | # PlayerEventType: Break out events into different types 34 | PlayerEventType = str 35 | 36 | # PlayerEvent: 37 | class PlayerEvent(ChromeTypeBase): 38 | def __init__(self, 39 | type: Union['PlayerEventType'], 40 | timestamp: Union['Timestamp'], 41 | name: Union['str'], 42 | value: Union['str'], 43 | ): 44 | 45 | self.type = type 46 | self.timestamp = timestamp 47 | self.name = name 48 | self.value = value 49 | 50 | 51 | class Media(PayloadMixin): 52 | """ This domain allows detailed inspection of media elements 53 | """ 54 | @classmethod 55 | def enable(cls): 56 | """Enables the Media domain 57 | """ 58 | return ( 59 | cls.build_send_payload("enable", { 60 | }), 61 | None 62 | ) 63 | 64 | @classmethod 65 | def disable(cls): 66 | """Disables the Media domain. 67 | """ 68 | return ( 69 | cls.build_send_payload("disable", { 70 | }), 71 | None 72 | ) 73 | 74 | 75 | 76 | class PlayerPropertiesChangedEvent(BaseEvent): 77 | 78 | js_name = 'Media.playerPropertiesChanged' 79 | hashable = ['playerId'] 80 | is_hashable = True 81 | 82 | def __init__(self, 83 | playerId: Union['PlayerId', dict], 84 | properties: Union['[PlayerProperty]', dict], 85 | ): 86 | if isinstance(playerId, dict): 87 | playerId = PlayerId(**playerId) 88 | self.playerId = playerId 89 | if isinstance(properties, dict): 90 | properties = [PlayerProperty](**properties) 91 | self.properties = properties 92 | 93 | @classmethod 94 | def build_hash(cls, playerId): 95 | kwargs = locals() 96 | kwargs.pop('cls') 97 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 98 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 99 | log.debug('generated hash = %s' % h) 100 | return h 101 | 102 | 103 | class PlayerEventsAddedEvent(BaseEvent): 104 | 105 | js_name = 'Media.playerEventsAdded' 106 | hashable = ['playerId'] 107 | is_hashable = True 108 | 109 | def __init__(self, 110 | playerId: Union['PlayerId', dict], 111 | events: Union['[PlayerEvent]', dict], 112 | ): 113 | if isinstance(playerId, dict): 114 | playerId = PlayerId(**playerId) 115 | self.playerId = playerId 116 | if isinstance(events, dict): 117 | events = [PlayerEvent](**events) 118 | self.events = events 119 | 120 | @classmethod 121 | def build_hash(cls, playerId): 122 | kwargs = locals() 123 | kwargs.pop('cls') 124 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 125 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 126 | log.debug('generated hash = %s' % h) 127 | return h 128 | 129 | 130 | class PlayersCreatedEvent(BaseEvent): 131 | 132 | js_name = 'Media.playersCreated' 133 | hashable = [] 134 | is_hashable = False 135 | 136 | def __init__(self, 137 | players: Union['[PlayerId]', dict], 138 | ): 139 | if isinstance(players, dict): 140 | players = [PlayerId](**players) 141 | self.players = players 142 | 143 | @classmethod 144 | def build_hash(cls): 145 | raise ValueError('Unable to build hash for non-hashable type') 146 | -------------------------------------------------------------------------------- /chromewhip/protocol/memory.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # PressureLevel: Memory pressure level. 17 | PressureLevel = str 18 | 19 | # SamplingProfileNode: Heap profile sample. 20 | class SamplingProfileNode(ChromeTypeBase): 21 | def __init__(self, 22 | size: Union['float'], 23 | total: Union['float'], 24 | stack: Union['[]'], 25 | ): 26 | 27 | self.size = size 28 | self.total = total 29 | self.stack = stack 30 | 31 | 32 | # SamplingProfile: Array of heap profile samples. 33 | class SamplingProfile(ChromeTypeBase): 34 | def __init__(self, 35 | samples: Union['[SamplingProfileNode]'], 36 | modules: Union['[Module]'], 37 | ): 38 | 39 | self.samples = samples 40 | self.modules = modules 41 | 42 | 43 | # Module: Executable module information 44 | class Module(ChromeTypeBase): 45 | def __init__(self, 46 | name: Union['str'], 47 | uuid: Union['str'], 48 | baseAddress: Union['str'], 49 | size: Union['float'], 50 | ): 51 | 52 | self.name = name 53 | self.uuid = uuid 54 | self.baseAddress = baseAddress 55 | self.size = size 56 | 57 | 58 | class Memory(PayloadMixin): 59 | """ 60 | """ 61 | @classmethod 62 | def getDOMCounters(cls): 63 | """ 64 | """ 65 | return ( 66 | cls.build_send_payload("getDOMCounters", { 67 | }), 68 | cls.convert_payload({ 69 | "documents": { 70 | "class": int, 71 | "optional": False 72 | }, 73 | "nodes": { 74 | "class": int, 75 | "optional": False 76 | }, 77 | "jsEventListeners": { 78 | "class": int, 79 | "optional": False 80 | }, 81 | }) 82 | ) 83 | 84 | @classmethod 85 | def prepareForLeakDetection(cls): 86 | """ 87 | """ 88 | return ( 89 | cls.build_send_payload("prepareForLeakDetection", { 90 | }), 91 | None 92 | ) 93 | 94 | @classmethod 95 | def forciblyPurgeJavaScriptMemory(cls): 96 | """Simulate OomIntervention by purging V8 memory. 97 | """ 98 | return ( 99 | cls.build_send_payload("forciblyPurgeJavaScriptMemory", { 100 | }), 101 | None 102 | ) 103 | 104 | @classmethod 105 | def setPressureNotificationsSuppressed(cls, 106 | suppressed: Union['bool'], 107 | ): 108 | """Enable/disable suppressing memory pressure notifications in all processes. 109 | :param suppressed: If true, memory pressure notifications will be suppressed. 110 | :type suppressed: bool 111 | """ 112 | return ( 113 | cls.build_send_payload("setPressureNotificationsSuppressed", { 114 | "suppressed": suppressed, 115 | }), 116 | None 117 | ) 118 | 119 | @classmethod 120 | def simulatePressureNotification(cls, 121 | level: Union['PressureLevel'], 122 | ): 123 | """Simulate a memory pressure notification in all processes. 124 | :param level: Memory pressure level of the notification. 125 | :type level: PressureLevel 126 | """ 127 | return ( 128 | cls.build_send_payload("simulatePressureNotification", { 129 | "level": level, 130 | }), 131 | None 132 | ) 133 | 134 | @classmethod 135 | def startSampling(cls, 136 | samplingInterval: Optional['int'] = None, 137 | suppressRandomness: Optional['bool'] = None, 138 | ): 139 | """Start collecting native memory profile. 140 | :param samplingInterval: Average number of bytes between samples. 141 | :type samplingInterval: int 142 | :param suppressRandomness: Do not randomize intervals between samples. 143 | :type suppressRandomness: bool 144 | """ 145 | return ( 146 | cls.build_send_payload("startSampling", { 147 | "samplingInterval": samplingInterval, 148 | "suppressRandomness": suppressRandomness, 149 | }), 150 | None 151 | ) 152 | 153 | @classmethod 154 | def stopSampling(cls): 155 | """Stop collecting native memory profile. 156 | """ 157 | return ( 158 | cls.build_send_payload("stopSampling", { 159 | }), 160 | None 161 | ) 162 | 163 | @classmethod 164 | def getAllTimeSamplingProfile(cls): 165 | """Retrieve native memory allocations profile 166 | collected since renderer process startup. 167 | """ 168 | return ( 169 | cls.build_send_payload("getAllTimeSamplingProfile", { 170 | }), 171 | cls.convert_payload({ 172 | "profile": { 173 | "class": SamplingProfile, 174 | "optional": False 175 | }, 176 | }) 177 | ) 178 | 179 | @classmethod 180 | def getBrowserSamplingProfile(cls): 181 | """Retrieve native memory allocations profile 182 | collected since browser process startup. 183 | """ 184 | return ( 185 | cls.build_send_payload("getBrowserSamplingProfile", { 186 | }), 187 | cls.convert_payload({ 188 | "profile": { 189 | "class": SamplingProfile, 190 | "optional": False 191 | }, 192 | }) 193 | ) 194 | 195 | @classmethod 196 | def getSamplingProfile(cls): 197 | """Retrieve native memory allocations profile collected since last 198 | `startSampling` call. 199 | """ 200 | return ( 201 | cls.build_send_payload("getSamplingProfile", { 202 | }), 203 | cls.convert_payload({ 204 | "profile": { 205 | "class": SamplingProfile, 206 | "optional": False 207 | }, 208 | }) 209 | ) 210 | 211 | -------------------------------------------------------------------------------- /chromewhip/protocol/performance.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # Metric: Run-time execution metric. 17 | class Metric(ChromeTypeBase): 18 | def __init__(self, 19 | name: Union['str'], 20 | value: Union['float'], 21 | ): 22 | 23 | self.name = name 24 | self.value = value 25 | 26 | 27 | class Performance(PayloadMixin): 28 | """ 29 | """ 30 | @classmethod 31 | def disable(cls): 32 | """Disable collecting and reporting metrics. 33 | """ 34 | return ( 35 | cls.build_send_payload("disable", { 36 | }), 37 | None 38 | ) 39 | 40 | @classmethod 41 | def enable(cls): 42 | """Enable collecting and reporting metrics. 43 | """ 44 | return ( 45 | cls.build_send_payload("enable", { 46 | }), 47 | None 48 | ) 49 | 50 | @classmethod 51 | def setTimeDomain(cls, 52 | timeDomain: Union['str'], 53 | ): 54 | """Sets time domain to use for collecting and reporting duration metrics. 55 | Note that this must be called before enabling metrics collection. Calling 56 | this method while metrics collection is enabled returns an error. 57 | :param timeDomain: Time domain 58 | :type timeDomain: str 59 | """ 60 | return ( 61 | cls.build_send_payload("setTimeDomain", { 62 | "timeDomain": timeDomain, 63 | }), 64 | None 65 | ) 66 | 67 | @classmethod 68 | def getMetrics(cls): 69 | """Retrieve current values of run-time metrics. 70 | """ 71 | return ( 72 | cls.build_send_payload("getMetrics", { 73 | }), 74 | cls.convert_payload({ 75 | "metrics": { 76 | "class": [Metric], 77 | "optional": False 78 | }, 79 | }) 80 | ) 81 | 82 | 83 | 84 | class MetricsEvent(BaseEvent): 85 | 86 | js_name = 'Performance.metrics' 87 | hashable = [] 88 | is_hashable = False 89 | 90 | def __init__(self, 91 | metrics: Union['[Metric]', dict], 92 | title: Union['str', dict], 93 | ): 94 | if isinstance(metrics, dict): 95 | metrics = [Metric](**metrics) 96 | self.metrics = metrics 97 | if isinstance(title, dict): 98 | title = str(**title) 99 | self.title = title 100 | 101 | @classmethod 102 | def build_hash(cls): 103 | raise ValueError('Unable to build hash for non-hashable type') 104 | -------------------------------------------------------------------------------- /chromewhip/protocol/schema.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # Domain: Description of the protocol domain. 17 | class Domain(ChromeTypeBase): 18 | def __init__(self, 19 | name: Union['str'], 20 | version: Union['str'], 21 | ): 22 | 23 | self.name = name 24 | self.version = version 25 | 26 | 27 | class Schema(PayloadMixin): 28 | """ This domain is deprecated. 29 | """ 30 | @classmethod 31 | def getDomains(cls): 32 | """Returns supported domains. 33 | """ 34 | return ( 35 | cls.build_send_payload("getDomains", { 36 | }), 37 | cls.convert_payload({ 38 | "domains": { 39 | "class": [Domain], 40 | "optional": False 41 | }, 42 | }) 43 | ) 44 | 45 | -------------------------------------------------------------------------------- /chromewhip/protocol/security.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # CertificateId: An internal certificate ID value. 17 | CertificateId = int 18 | 19 | # MixedContentType: A description of mixed content (HTTP resources on HTTPS pages), as defined byhttps://www.w3.org/TR/mixed-content/#categories 20 | MixedContentType = str 21 | 22 | # SecurityState: The security level of a page or resource. 23 | SecurityState = str 24 | 25 | # SecurityStateExplanation: An explanation of an factor contributing to the security state. 26 | class SecurityStateExplanation(ChromeTypeBase): 27 | def __init__(self, 28 | securityState: Union['SecurityState'], 29 | title: Union['str'], 30 | summary: Union['str'], 31 | description: Union['str'], 32 | mixedContentType: Union['MixedContentType'], 33 | certificate: Union['[]'], 34 | recommendations: Optional['[]'] = None, 35 | ): 36 | 37 | self.securityState = securityState 38 | self.title = title 39 | self.summary = summary 40 | self.description = description 41 | self.mixedContentType = mixedContentType 42 | self.certificate = certificate 43 | self.recommendations = recommendations 44 | 45 | 46 | # InsecureContentStatus: Information about insecure content on the page. 47 | class InsecureContentStatus(ChromeTypeBase): 48 | def __init__(self, 49 | ranMixedContent: Union['bool'], 50 | displayedMixedContent: Union['bool'], 51 | containedMixedForm: Union['bool'], 52 | ranContentWithCertErrors: Union['bool'], 53 | displayedContentWithCertErrors: Union['bool'], 54 | ranInsecureContentStyle: Union['SecurityState'], 55 | displayedInsecureContentStyle: Union['SecurityState'], 56 | ): 57 | 58 | self.ranMixedContent = ranMixedContent 59 | self.displayedMixedContent = displayedMixedContent 60 | self.containedMixedForm = containedMixedForm 61 | self.ranContentWithCertErrors = ranContentWithCertErrors 62 | self.displayedContentWithCertErrors = displayedContentWithCertErrors 63 | self.ranInsecureContentStyle = ranInsecureContentStyle 64 | self.displayedInsecureContentStyle = displayedInsecureContentStyle 65 | 66 | 67 | # CertificateErrorAction: The action to take when a certificate error occurs. continue will continue processing therequest and cancel will cancel the request. 68 | CertificateErrorAction = str 69 | 70 | class Security(PayloadMixin): 71 | """ Security 72 | """ 73 | @classmethod 74 | def disable(cls): 75 | """Disables tracking security state changes. 76 | """ 77 | return ( 78 | cls.build_send_payload("disable", { 79 | }), 80 | None 81 | ) 82 | 83 | @classmethod 84 | def enable(cls): 85 | """Enables tracking security state changes. 86 | """ 87 | return ( 88 | cls.build_send_payload("enable", { 89 | }), 90 | None 91 | ) 92 | 93 | @classmethod 94 | def setIgnoreCertificateErrors(cls, 95 | ignore: Union['bool'], 96 | ): 97 | """Enable/disable whether all certificate errors should be ignored. 98 | :param ignore: If true, all certificate errors will be ignored. 99 | :type ignore: bool 100 | """ 101 | return ( 102 | cls.build_send_payload("setIgnoreCertificateErrors", { 103 | "ignore": ignore, 104 | }), 105 | None 106 | ) 107 | 108 | @classmethod 109 | def handleCertificateError(cls, 110 | eventId: Union['int'], 111 | action: Union['CertificateErrorAction'], 112 | ): 113 | """Handles a certificate error that fired a certificateError event. 114 | :param eventId: The ID of the event. 115 | :type eventId: int 116 | :param action: The action to take on the certificate error. 117 | :type action: CertificateErrorAction 118 | """ 119 | return ( 120 | cls.build_send_payload("handleCertificateError", { 121 | "eventId": eventId, 122 | "action": action, 123 | }), 124 | None 125 | ) 126 | 127 | @classmethod 128 | def setOverrideCertificateErrors(cls, 129 | override: Union['bool'], 130 | ): 131 | """Enable/disable overriding certificate errors. If enabled, all certificate error events need to 132 | be handled by the DevTools client and should be answered with `handleCertificateError` commands. 133 | :param override: If true, certificate errors will be overridden. 134 | :type override: bool 135 | """ 136 | return ( 137 | cls.build_send_payload("setOverrideCertificateErrors", { 138 | "override": override, 139 | }), 140 | None 141 | ) 142 | 143 | 144 | 145 | class CertificateErrorEvent(BaseEvent): 146 | 147 | js_name = 'Security.certificateError' 148 | hashable = ['eventId'] 149 | is_hashable = True 150 | 151 | def __init__(self, 152 | eventId: Union['int', dict], 153 | errorType: Union['str', dict], 154 | requestURL: Union['str', dict], 155 | ): 156 | if isinstance(eventId, dict): 157 | eventId = int(**eventId) 158 | self.eventId = eventId 159 | if isinstance(errorType, dict): 160 | errorType = str(**errorType) 161 | self.errorType = errorType 162 | if isinstance(requestURL, dict): 163 | requestURL = str(**requestURL) 164 | self.requestURL = requestURL 165 | 166 | @classmethod 167 | def build_hash(cls, eventId): 168 | kwargs = locals() 169 | kwargs.pop('cls') 170 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 171 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 172 | log.debug('generated hash = %s' % h) 173 | return h 174 | 175 | 176 | class SecurityStateChangedEvent(BaseEvent): 177 | 178 | js_name = 'Security.securityStateChanged' 179 | hashable = [] 180 | is_hashable = False 181 | 182 | def __init__(self, 183 | securityState: Union['SecurityState', dict], 184 | schemeIsCryptographic: Union['bool', dict], 185 | explanations: Union['[SecurityStateExplanation]', dict], 186 | insecureContentStatus: Union['InsecureContentStatus', dict], 187 | summary: Union['str', dict, None] = None, 188 | ): 189 | if isinstance(securityState, dict): 190 | securityState = SecurityState(**securityState) 191 | self.securityState = securityState 192 | if isinstance(schemeIsCryptographic, dict): 193 | schemeIsCryptographic = bool(**schemeIsCryptographic) 194 | self.schemeIsCryptographic = schemeIsCryptographic 195 | if isinstance(explanations, dict): 196 | explanations = [SecurityStateExplanation](**explanations) 197 | self.explanations = explanations 198 | if isinstance(insecureContentStatus, dict): 199 | insecureContentStatus = InsecureContentStatus(**insecureContentStatus) 200 | self.insecureContentStatus = insecureContentStatus 201 | if isinstance(summary, dict): 202 | summary = str(**summary) 203 | self.summary = summary 204 | 205 | @classmethod 206 | def build_hash(cls): 207 | raise ValueError('Unable to build hash for non-hashable type') 208 | -------------------------------------------------------------------------------- /chromewhip/protocol/storage.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # StorageType: Enum of possible storage types. 17 | StorageType = str 18 | 19 | # UsageForType: Usage for a storage type. 20 | class UsageForType(ChromeTypeBase): 21 | def __init__(self, 22 | storageType: Union['StorageType'], 23 | usage: Union['float'], 24 | ): 25 | 26 | self.storageType = storageType 27 | self.usage = usage 28 | 29 | 30 | class Storage(PayloadMixin): 31 | """ 32 | """ 33 | @classmethod 34 | def clearDataForOrigin(cls, 35 | origin: Union['str'], 36 | storageTypes: Union['str'], 37 | ): 38 | """Clears storage for origin. 39 | :param origin: Security origin. 40 | :type origin: str 41 | :param storageTypes: Comma separated list of StorageType to clear. 42 | :type storageTypes: str 43 | """ 44 | return ( 45 | cls.build_send_payload("clearDataForOrigin", { 46 | "origin": origin, 47 | "storageTypes": storageTypes, 48 | }), 49 | None 50 | ) 51 | 52 | @classmethod 53 | def getUsageAndQuota(cls, 54 | origin: Union['str'], 55 | ): 56 | """Returns usage and quota in bytes. 57 | :param origin: Security origin. 58 | :type origin: str 59 | """ 60 | return ( 61 | cls.build_send_payload("getUsageAndQuota", { 62 | "origin": origin, 63 | }), 64 | cls.convert_payload({ 65 | "usage": { 66 | "class": float, 67 | "optional": False 68 | }, 69 | "quota": { 70 | "class": float, 71 | "optional": False 72 | }, 73 | "usageBreakdown": { 74 | "class": [UsageForType], 75 | "optional": False 76 | }, 77 | }) 78 | ) 79 | 80 | @classmethod 81 | def trackCacheStorageForOrigin(cls, 82 | origin: Union['str'], 83 | ): 84 | """Registers origin to be notified when an update occurs to its cache storage list. 85 | :param origin: Security origin. 86 | :type origin: str 87 | """ 88 | return ( 89 | cls.build_send_payload("trackCacheStorageForOrigin", { 90 | "origin": origin, 91 | }), 92 | None 93 | ) 94 | 95 | @classmethod 96 | def trackIndexedDBForOrigin(cls, 97 | origin: Union['str'], 98 | ): 99 | """Registers origin to be notified when an update occurs to its IndexedDB. 100 | :param origin: Security origin. 101 | :type origin: str 102 | """ 103 | return ( 104 | cls.build_send_payload("trackIndexedDBForOrigin", { 105 | "origin": origin, 106 | }), 107 | None 108 | ) 109 | 110 | @classmethod 111 | def untrackCacheStorageForOrigin(cls, 112 | origin: Union['str'], 113 | ): 114 | """Unregisters origin from receiving notifications for cache storage. 115 | :param origin: Security origin. 116 | :type origin: str 117 | """ 118 | return ( 119 | cls.build_send_payload("untrackCacheStorageForOrigin", { 120 | "origin": origin, 121 | }), 122 | None 123 | ) 124 | 125 | @classmethod 126 | def untrackIndexedDBForOrigin(cls, 127 | origin: Union['str'], 128 | ): 129 | """Unregisters origin from receiving notifications for IndexedDB. 130 | :param origin: Security origin. 131 | :type origin: str 132 | """ 133 | return ( 134 | cls.build_send_payload("untrackIndexedDBForOrigin", { 135 | "origin": origin, 136 | }), 137 | None 138 | ) 139 | 140 | 141 | 142 | class CacheStorageContentUpdatedEvent(BaseEvent): 143 | 144 | js_name = 'Storage.cacheStorageContentUpdated' 145 | hashable = [] 146 | is_hashable = False 147 | 148 | def __init__(self, 149 | origin: Union['str', dict], 150 | cacheName: Union['str', dict], 151 | ): 152 | if isinstance(origin, dict): 153 | origin = str(**origin) 154 | self.origin = origin 155 | if isinstance(cacheName, dict): 156 | cacheName = str(**cacheName) 157 | self.cacheName = cacheName 158 | 159 | @classmethod 160 | def build_hash(cls): 161 | raise ValueError('Unable to build hash for non-hashable type') 162 | 163 | 164 | class CacheStorageListUpdatedEvent(BaseEvent): 165 | 166 | js_name = 'Storage.cacheStorageListUpdated' 167 | hashable = [] 168 | is_hashable = False 169 | 170 | def __init__(self, 171 | origin: Union['str', dict], 172 | ): 173 | if isinstance(origin, dict): 174 | origin = str(**origin) 175 | self.origin = origin 176 | 177 | @classmethod 178 | def build_hash(cls): 179 | raise ValueError('Unable to build hash for non-hashable type') 180 | 181 | 182 | class IndexedDBContentUpdatedEvent(BaseEvent): 183 | 184 | js_name = 'Storage.indexedDBContentUpdated' 185 | hashable = [] 186 | is_hashable = False 187 | 188 | def __init__(self, 189 | origin: Union['str', dict], 190 | databaseName: Union['str', dict], 191 | objectStoreName: Union['str', dict], 192 | ): 193 | if isinstance(origin, dict): 194 | origin = str(**origin) 195 | self.origin = origin 196 | if isinstance(databaseName, dict): 197 | databaseName = str(**databaseName) 198 | self.databaseName = databaseName 199 | if isinstance(objectStoreName, dict): 200 | objectStoreName = str(**objectStoreName) 201 | self.objectStoreName = objectStoreName 202 | 203 | @classmethod 204 | def build_hash(cls): 205 | raise ValueError('Unable to build hash for non-hashable type') 206 | 207 | 208 | class IndexedDBListUpdatedEvent(BaseEvent): 209 | 210 | js_name = 'Storage.indexedDBListUpdated' 211 | hashable = [] 212 | is_hashable = False 213 | 214 | def __init__(self, 215 | origin: Union['str', dict], 216 | ): 217 | if isinstance(origin, dict): 218 | origin = str(**origin) 219 | self.origin = origin 220 | 221 | @classmethod 222 | def build_hash(cls): 223 | raise ValueError('Unable to build hash for non-hashable type') 224 | -------------------------------------------------------------------------------- /chromewhip/protocol/systeminfo.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # GPUDevice: Describes a single graphics processor (GPU). 17 | class GPUDevice(ChromeTypeBase): 18 | def __init__(self, 19 | vendorId: Union['float'], 20 | deviceId: Union['float'], 21 | vendorString: Union['str'], 22 | deviceString: Union['str'], 23 | driverVendor: Union['str'], 24 | driverVersion: Union['str'], 25 | ): 26 | 27 | self.vendorId = vendorId 28 | self.deviceId = deviceId 29 | self.vendorString = vendorString 30 | self.deviceString = deviceString 31 | self.driverVendor = driverVendor 32 | self.driverVersion = driverVersion 33 | 34 | 35 | # Size: Describes the width and height dimensions of an entity. 36 | class Size(ChromeTypeBase): 37 | def __init__(self, 38 | width: Union['int'], 39 | height: Union['int'], 40 | ): 41 | 42 | self.width = width 43 | self.height = height 44 | 45 | 46 | # VideoDecodeAcceleratorCapability: Describes a supported video decoding profile with its associated minimum andmaximum resolutions. 47 | class VideoDecodeAcceleratorCapability(ChromeTypeBase): 48 | def __init__(self, 49 | profile: Union['str'], 50 | maxResolution: Union['Size'], 51 | minResolution: Union['Size'], 52 | ): 53 | 54 | self.profile = profile 55 | self.maxResolution = maxResolution 56 | self.minResolution = minResolution 57 | 58 | 59 | # VideoEncodeAcceleratorCapability: Describes a supported video encoding profile with its associated maximumresolution and maximum framerate. 60 | class VideoEncodeAcceleratorCapability(ChromeTypeBase): 61 | def __init__(self, 62 | profile: Union['str'], 63 | maxResolution: Union['Size'], 64 | maxFramerateNumerator: Union['int'], 65 | maxFramerateDenominator: Union['int'], 66 | ): 67 | 68 | self.profile = profile 69 | self.maxResolution = maxResolution 70 | self.maxFramerateNumerator = maxFramerateNumerator 71 | self.maxFramerateDenominator = maxFramerateDenominator 72 | 73 | 74 | # SubsamplingFormat: YUV subsampling type of the pixels of a given image. 75 | SubsamplingFormat = str 76 | 77 | # ImageType: Image format of a given image. 78 | ImageType = str 79 | 80 | # ImageDecodeAcceleratorCapability: Describes a supported image decoding profile with its associated minimum andmaximum resolutions and subsampling. 81 | class ImageDecodeAcceleratorCapability(ChromeTypeBase): 82 | def __init__(self, 83 | imageType: Union['ImageType'], 84 | maxDimensions: Union['Size'], 85 | minDimensions: Union['Size'], 86 | subsamplings: Union['[SubsamplingFormat]'], 87 | ): 88 | 89 | self.imageType = imageType 90 | self.maxDimensions = maxDimensions 91 | self.minDimensions = minDimensions 92 | self.subsamplings = subsamplings 93 | 94 | 95 | # GPUInfo: Provides information about the GPU(s) on the system. 96 | class GPUInfo(ChromeTypeBase): 97 | def __init__(self, 98 | devices: Union['[GPUDevice]'], 99 | driverBugWorkarounds: Union['[]'], 100 | videoDecoding: Union['[VideoDecodeAcceleratorCapability]'], 101 | videoEncoding: Union['[VideoEncodeAcceleratorCapability]'], 102 | imageDecoding: Union['[ImageDecodeAcceleratorCapability]'], 103 | auxAttributes: Optional['dict'] = None, 104 | featureStatus: Optional['dict'] = None, 105 | ): 106 | 107 | self.devices = devices 108 | self.auxAttributes = auxAttributes 109 | self.featureStatus = featureStatus 110 | self.driverBugWorkarounds = driverBugWorkarounds 111 | self.videoDecoding = videoDecoding 112 | self.videoEncoding = videoEncoding 113 | self.imageDecoding = imageDecoding 114 | 115 | 116 | # ProcessInfo: Represents process info. 117 | class ProcessInfo(ChromeTypeBase): 118 | def __init__(self, 119 | type: Union['str'], 120 | id: Union['int'], 121 | cpuTime: Union['float'], 122 | ): 123 | 124 | self.type = type 125 | self.id = id 126 | self.cpuTime = cpuTime 127 | 128 | 129 | class SystemInfo(PayloadMixin): 130 | """ The SystemInfo domain defines methods and events for querying low-level system information. 131 | """ 132 | @classmethod 133 | def getInfo(cls): 134 | """Returns information about the system. 135 | """ 136 | return ( 137 | cls.build_send_payload("getInfo", { 138 | }), 139 | cls.convert_payload({ 140 | "gpu": { 141 | "class": GPUInfo, 142 | "optional": False 143 | }, 144 | "modelName": { 145 | "class": str, 146 | "optional": False 147 | }, 148 | "modelVersion": { 149 | "class": str, 150 | "optional": False 151 | }, 152 | "commandLine": { 153 | "class": str, 154 | "optional": False 155 | }, 156 | }) 157 | ) 158 | 159 | @classmethod 160 | def getProcessInfo(cls): 161 | """Returns information about all running processes. 162 | """ 163 | return ( 164 | cls.build_send_payload("getProcessInfo", { 165 | }), 166 | cls.convert_payload({ 167 | "processInfo": { 168 | "class": [ProcessInfo], 169 | "optional": False 170 | }, 171 | }) 172 | ) 173 | 174 | -------------------------------------------------------------------------------- /chromewhip/protocol/testing.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import page as Page 16 | 17 | class Testing(PayloadMixin): 18 | """ Testing domain is a dumping ground for the capabilities requires for browser or app testing that do not fit other 19 | domains. 20 | """ 21 | @classmethod 22 | def generateTestReport(cls, 23 | message: Union['str'], 24 | group: Optional['str'] = None, 25 | ): 26 | """Generates a report for testing. 27 | :param message: Message to be displayed in the report. 28 | :type message: str 29 | :param group: Specifies the endpoint group to deliver the report to. 30 | :type group: str 31 | """ 32 | return ( 33 | cls.build_send_payload("generateTestReport", { 34 | "message": message, 35 | "group": group, 36 | }), 37 | None 38 | ) 39 | 40 | -------------------------------------------------------------------------------- /chromewhip/protocol/tethering.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import target as Target 16 | 17 | class Tethering(PayloadMixin): 18 | """ The Tethering domain defines methods and events for browser port binding. 19 | """ 20 | @classmethod 21 | def bind(cls, 22 | port: Union['int'], 23 | ): 24 | """Request browser port binding. 25 | :param port: Port number to bind. 26 | :type port: int 27 | """ 28 | return ( 29 | cls.build_send_payload("bind", { 30 | "port": port, 31 | }), 32 | None 33 | ) 34 | 35 | @classmethod 36 | def unbind(cls, 37 | port: Union['int'], 38 | ): 39 | """Request browser port unbinding. 40 | :param port: Port number to unbind. 41 | :type port: int 42 | """ 43 | return ( 44 | cls.build_send_payload("unbind", { 45 | "port": port, 46 | }), 47 | None 48 | ) 49 | 50 | 51 | 52 | class AcceptedEvent(BaseEvent): 53 | 54 | js_name = 'Tethering.accepted' 55 | hashable = ['connectionId'] 56 | is_hashable = True 57 | 58 | def __init__(self, 59 | port: Union['int', dict], 60 | connectionId: Union['str', dict], 61 | ): 62 | if isinstance(port, dict): 63 | port = int(**port) 64 | self.port = port 65 | if isinstance(connectionId, dict): 66 | connectionId = str(**connectionId) 67 | self.connectionId = connectionId 68 | 69 | @classmethod 70 | def build_hash(cls, connectionId): 71 | kwargs = locals() 72 | kwargs.pop('cls') 73 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 74 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 75 | log.debug('generated hash = %s' % h) 76 | return h 77 | -------------------------------------------------------------------------------- /chromewhip/protocol/tracing.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | from chromewhip.protocol import io as IO 16 | 17 | # MemoryDumpConfig: Configuration for memory dump. Used only when "memory-infra" category is enabled. 18 | MemoryDumpConfig = dict 19 | 20 | # TraceConfig: 21 | class TraceConfig(ChromeTypeBase): 22 | def __init__(self, 23 | recordMode: Optional['str'] = None, 24 | enableSampling: Optional['bool'] = None, 25 | enableSystrace: Optional['bool'] = None, 26 | enableArgumentFilter: Optional['bool'] = None, 27 | includedCategories: Optional['[]'] = None, 28 | excludedCategories: Optional['[]'] = None, 29 | syntheticDelays: Optional['[]'] = None, 30 | memoryDumpConfig: Optional['MemoryDumpConfig'] = None, 31 | ): 32 | 33 | self.recordMode = recordMode 34 | self.enableSampling = enableSampling 35 | self.enableSystrace = enableSystrace 36 | self.enableArgumentFilter = enableArgumentFilter 37 | self.includedCategories = includedCategories 38 | self.excludedCategories = excludedCategories 39 | self.syntheticDelays = syntheticDelays 40 | self.memoryDumpConfig = memoryDumpConfig 41 | 42 | 43 | # StreamFormat: Data format of a trace. Can be either the legacy JSON format or theprotocol buffer format. Note that the JSON format will be deprecated soon. 44 | StreamFormat = str 45 | 46 | # StreamCompression: Compression type to use for traces returned via streams. 47 | StreamCompression = str 48 | 49 | class Tracing(PayloadMixin): 50 | """ 51 | """ 52 | @classmethod 53 | def end(cls): 54 | """Stop trace events collection. 55 | """ 56 | return ( 57 | cls.build_send_payload("end", { 58 | }), 59 | None 60 | ) 61 | 62 | @classmethod 63 | def getCategories(cls): 64 | """Gets supported tracing categories. 65 | """ 66 | return ( 67 | cls.build_send_payload("getCategories", { 68 | }), 69 | cls.convert_payload({ 70 | "categories": { 71 | "class": [], 72 | "optional": False 73 | }, 74 | }) 75 | ) 76 | 77 | @classmethod 78 | def recordClockSyncMarker(cls, 79 | syncId: Union['str'], 80 | ): 81 | """Record a clock sync marker in the trace. 82 | :param syncId: The ID of this clock sync marker 83 | :type syncId: str 84 | """ 85 | return ( 86 | cls.build_send_payload("recordClockSyncMarker", { 87 | "syncId": syncId, 88 | }), 89 | None 90 | ) 91 | 92 | @classmethod 93 | def requestMemoryDump(cls): 94 | """Request a global memory dump. 95 | """ 96 | return ( 97 | cls.build_send_payload("requestMemoryDump", { 98 | }), 99 | cls.convert_payload({ 100 | "dumpGuid": { 101 | "class": str, 102 | "optional": False 103 | }, 104 | "success": { 105 | "class": bool, 106 | "optional": False 107 | }, 108 | }) 109 | ) 110 | 111 | @classmethod 112 | def start(cls, 113 | categories: Optional['str'] = None, 114 | options: Optional['str'] = None, 115 | bufferUsageReportingInterval: Optional['float'] = None, 116 | transferMode: Optional['str'] = None, 117 | streamFormat: Optional['StreamFormat'] = None, 118 | streamCompression: Optional['StreamCompression'] = None, 119 | traceConfig: Optional['TraceConfig'] = None, 120 | ): 121 | """Start trace events collection. 122 | :param categories: Category/tag filter 123 | :type categories: str 124 | :param options: Tracing options 125 | :type options: str 126 | :param bufferUsageReportingInterval: If set, the agent will issue bufferUsage events at this interval, specified in milliseconds 127 | :type bufferUsageReportingInterval: float 128 | :param transferMode: Whether to report trace events as series of dataCollected events or to save trace to a 129 | stream (defaults to `ReportEvents`). 130 | :type transferMode: str 131 | :param streamFormat: Trace data format to use. This only applies when using `ReturnAsStream` 132 | transfer mode (defaults to `json`). 133 | :type streamFormat: StreamFormat 134 | :param streamCompression: Compression format to use. This only applies when using `ReturnAsStream` 135 | transfer mode (defaults to `none`) 136 | :type streamCompression: StreamCompression 137 | :param traceConfig: 138 | :type traceConfig: TraceConfig 139 | """ 140 | return ( 141 | cls.build_send_payload("start", { 142 | "categories": categories, 143 | "options": options, 144 | "bufferUsageReportingInterval": bufferUsageReportingInterval, 145 | "transferMode": transferMode, 146 | "streamFormat": streamFormat, 147 | "streamCompression": streamCompression, 148 | "traceConfig": traceConfig, 149 | }), 150 | None 151 | ) 152 | 153 | 154 | 155 | class BufferUsageEvent(BaseEvent): 156 | 157 | js_name = 'Tracing.bufferUsage' 158 | hashable = [] 159 | is_hashable = False 160 | 161 | def __init__(self, 162 | percentFull: Union['float', dict, None] = None, 163 | eventCount: Union['float', dict, None] = None, 164 | value: Union['float', dict, None] = None, 165 | ): 166 | if isinstance(percentFull, dict): 167 | percentFull = float(**percentFull) 168 | self.percentFull = percentFull 169 | if isinstance(eventCount, dict): 170 | eventCount = float(**eventCount) 171 | self.eventCount = eventCount 172 | if isinstance(value, dict): 173 | value = float(**value) 174 | self.value = value 175 | 176 | @classmethod 177 | def build_hash(cls): 178 | raise ValueError('Unable to build hash for non-hashable type') 179 | 180 | 181 | class DataCollectedEvent(BaseEvent): 182 | 183 | js_name = 'Tracing.dataCollected' 184 | hashable = [] 185 | is_hashable = False 186 | 187 | def __init__(self, 188 | value: Union['[]', dict], 189 | ): 190 | if isinstance(value, dict): 191 | value = [](**value) 192 | self.value = value 193 | 194 | @classmethod 195 | def build_hash(cls): 196 | raise ValueError('Unable to build hash for non-hashable type') 197 | 198 | 199 | class TracingCompleteEvent(BaseEvent): 200 | 201 | js_name = 'Tracing.tracingComplete' 202 | hashable = [] 203 | is_hashable = False 204 | 205 | def __init__(self, 206 | dataLossOccurred: Union['bool', dict], 207 | stream: Union['IO.StreamHandle', dict, None] = None, 208 | traceFormat: Union['StreamFormat', dict, None] = None, 209 | streamCompression: Union['StreamCompression', dict, None] = None, 210 | ): 211 | if isinstance(dataLossOccurred, dict): 212 | dataLossOccurred = bool(**dataLossOccurred) 213 | self.dataLossOccurred = dataLossOccurred 214 | if isinstance(stream, dict): 215 | stream = IO.StreamHandle(**stream) 216 | self.stream = stream 217 | if isinstance(traceFormat, dict): 218 | traceFormat = StreamFormat(**traceFormat) 219 | self.traceFormat = traceFormat 220 | if isinstance(streamCompression, dict): 221 | streamCompression = StreamCompression(**streamCompression) 222 | self.streamCompression = streamCompression 223 | 224 | @classmethod 225 | def build_hash(cls): 226 | raise ValueError('Unable to build hash for non-hashable type') 227 | -------------------------------------------------------------------------------- /chromewhip/protocol/webauthn.py: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | # AuthenticatorId: 17 | AuthenticatorId = str 18 | 19 | # AuthenticatorProtocol: 20 | AuthenticatorProtocol = str 21 | 22 | # AuthenticatorTransport: 23 | AuthenticatorTransport = str 24 | 25 | # VirtualAuthenticatorOptions: 26 | class VirtualAuthenticatorOptions(ChromeTypeBase): 27 | def __init__(self, 28 | protocol: Union['AuthenticatorProtocol'], 29 | transport: Union['AuthenticatorTransport'], 30 | hasResidentKey: Union['bool'], 31 | hasUserVerification: Union['bool'], 32 | automaticPresenceSimulation: Optional['bool'] = None, 33 | ): 34 | 35 | self.protocol = protocol 36 | self.transport = transport 37 | self.hasResidentKey = hasResidentKey 38 | self.hasUserVerification = hasUserVerification 39 | self.automaticPresenceSimulation = automaticPresenceSimulation 40 | 41 | 42 | # Credential: 43 | class Credential(ChromeTypeBase): 44 | def __init__(self, 45 | credentialId: Union['str'], 46 | isResidentCredential: Union['bool'], 47 | privateKey: Union['str'], 48 | signCount: Union['int'], 49 | rpId: Optional['str'] = None, 50 | userHandle: Optional['str'] = None, 51 | ): 52 | 53 | self.credentialId = credentialId 54 | self.isResidentCredential = isResidentCredential 55 | self.rpId = rpId 56 | self.privateKey = privateKey 57 | self.userHandle = userHandle 58 | self.signCount = signCount 59 | 60 | 61 | class WebAuthn(PayloadMixin): 62 | """ This domain allows configuring virtual authenticators to test the WebAuthn 63 | API. 64 | """ 65 | @classmethod 66 | def enable(cls): 67 | """Enable the WebAuthn domain and start intercepting credential storage and 68 | retrieval with a virtual authenticator. 69 | """ 70 | return ( 71 | cls.build_send_payload("enable", { 72 | }), 73 | None 74 | ) 75 | 76 | @classmethod 77 | def disable(cls): 78 | """Disable the WebAuthn domain. 79 | """ 80 | return ( 81 | cls.build_send_payload("disable", { 82 | }), 83 | None 84 | ) 85 | 86 | @classmethod 87 | def addVirtualAuthenticator(cls, 88 | options: Union['VirtualAuthenticatorOptions'], 89 | ): 90 | """Creates and adds a virtual authenticator. 91 | :param options: 92 | :type options: VirtualAuthenticatorOptions 93 | """ 94 | return ( 95 | cls.build_send_payload("addVirtualAuthenticator", { 96 | "options": options, 97 | }), 98 | cls.convert_payload({ 99 | "authenticatorId": { 100 | "class": AuthenticatorId, 101 | "optional": False 102 | }, 103 | }) 104 | ) 105 | 106 | @classmethod 107 | def removeVirtualAuthenticator(cls, 108 | authenticatorId: Union['AuthenticatorId'], 109 | ): 110 | """Removes the given authenticator. 111 | :param authenticatorId: 112 | :type authenticatorId: AuthenticatorId 113 | """ 114 | return ( 115 | cls.build_send_payload("removeVirtualAuthenticator", { 116 | "authenticatorId": authenticatorId, 117 | }), 118 | None 119 | ) 120 | 121 | @classmethod 122 | def addCredential(cls, 123 | authenticatorId: Union['AuthenticatorId'], 124 | credential: Union['Credential'], 125 | ): 126 | """Adds the credential to the specified authenticator. 127 | :param authenticatorId: 128 | :type authenticatorId: AuthenticatorId 129 | :param credential: 130 | :type credential: Credential 131 | """ 132 | return ( 133 | cls.build_send_payload("addCredential", { 134 | "authenticatorId": authenticatorId, 135 | "credential": credential, 136 | }), 137 | None 138 | ) 139 | 140 | @classmethod 141 | def getCredential(cls, 142 | authenticatorId: Union['AuthenticatorId'], 143 | credentialId: Union['str'], 144 | ): 145 | """Returns a single credential stored in the given virtual authenticator that 146 | matches the credential ID. 147 | :param authenticatorId: 148 | :type authenticatorId: AuthenticatorId 149 | :param credentialId: 150 | :type credentialId: str 151 | """ 152 | return ( 153 | cls.build_send_payload("getCredential", { 154 | "authenticatorId": authenticatorId, 155 | "credentialId": credentialId, 156 | }), 157 | cls.convert_payload({ 158 | "credential": { 159 | "class": Credential, 160 | "optional": False 161 | }, 162 | }) 163 | ) 164 | 165 | @classmethod 166 | def getCredentials(cls, 167 | authenticatorId: Union['AuthenticatorId'], 168 | ): 169 | """Returns all the credentials stored in the given virtual authenticator. 170 | :param authenticatorId: 171 | :type authenticatorId: AuthenticatorId 172 | """ 173 | return ( 174 | cls.build_send_payload("getCredentials", { 175 | "authenticatorId": authenticatorId, 176 | }), 177 | cls.convert_payload({ 178 | "credentials": { 179 | "class": [Credential], 180 | "optional": False 181 | }, 182 | }) 183 | ) 184 | 185 | @classmethod 186 | def removeCredential(cls, 187 | authenticatorId: Union['AuthenticatorId'], 188 | credentialId: Union['str'], 189 | ): 190 | """Removes a credential from the authenticator. 191 | :param authenticatorId: 192 | :type authenticatorId: AuthenticatorId 193 | :param credentialId: 194 | :type credentialId: str 195 | """ 196 | return ( 197 | cls.build_send_payload("removeCredential", { 198 | "authenticatorId": authenticatorId, 199 | "credentialId": credentialId, 200 | }), 201 | None 202 | ) 203 | 204 | @classmethod 205 | def clearCredentials(cls, 206 | authenticatorId: Union['AuthenticatorId'], 207 | ): 208 | """Clears all the credentials from the specified device. 209 | :param authenticatorId: 210 | :type authenticatorId: AuthenticatorId 211 | """ 212 | return ( 213 | cls.build_send_payload("clearCredentials", { 214 | "authenticatorId": authenticatorId, 215 | }), 216 | None 217 | ) 218 | 219 | @classmethod 220 | def setUserVerified(cls, 221 | authenticatorId: Union['AuthenticatorId'], 222 | isUserVerified: Union['bool'], 223 | ): 224 | """Sets whether User Verification succeeds or fails for an authenticator. 225 | The default is true. 226 | :param authenticatorId: 227 | :type authenticatorId: AuthenticatorId 228 | :param isUserVerified: 229 | :type isUserVerified: bool 230 | """ 231 | return ( 232 | cls.build_send_payload("setUserVerified", { 233 | "authenticatorId": authenticatorId, 234 | "isUserVerified": isUserVerified, 235 | }), 236 | None 237 | ) 238 | 239 | -------------------------------------------------------------------------------- /chromewhip/routes.py: -------------------------------------------------------------------------------- 1 | from chromewhip.views import render_html, render_png 2 | 3 | 4 | def setup_routes(app): 5 | app.router.add_get('/render.html', render_html) 6 | app.router.add_get('/render.png', render_png) 7 | -------------------------------------------------------------------------------- /chromewhip/views.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import functools 3 | import logging 4 | 5 | from bs4 import BeautifulSoup 6 | from aiohttp import web 7 | 8 | from chromewhip.protocol import page, emulation, browser, dom, runtime 9 | 10 | BS = functools.partial(BeautifulSoup, features="lxml") 11 | 12 | log = logging.getLogger('chromewhip.views') 13 | 14 | async def _go(request: web.Request): 15 | 16 | js_profiles = request.app['js-profiles'] 17 | c = request.app['chrome-driver'] 18 | 19 | url = request.query.get('url') 20 | if not url: 21 | return web.HTTPBadRequest(reason='no url query param provided') # TODO: match splash reply 22 | 23 | wait_s = float(request.query.get('wait', 0)) 24 | 25 | raw_viewport = request.query.get('viewport', '1024x768') 26 | parts = raw_viewport.split('x') 27 | width = int(parts[0]) 28 | height = int(parts[1]) 29 | 30 | js_profile_name = request.query.get('js', None) 31 | if js_profile_name: 32 | profile = js_profiles.get(js_profile_name) 33 | if not profile: 34 | return web.HTTPBadRequest(reason='profile name is incorrect') # TODO: match splash 35 | 36 | # TODO: potentially validate and verify js source for errors and security concerrns 37 | js_source = request.query.get('js_source', None) 38 | 39 | await c.connect() 40 | tab = c.tabs[0] 41 | cmd = page.Page.setDeviceMetricsOverride(width=width, 42 | height=height, 43 | deviceScaleFactor=0.0, 44 | mobile=False) 45 | await tab.send_command(cmd) 46 | await tab.enable_page_events() 47 | await tab.go(url) 48 | await asyncio.sleep(wait_s) 49 | if js_profile_name: 50 | await tab.evaluate(js_profiles[js_profile_name]) 51 | 52 | if js_source: 53 | await tab.evaluate(js_source) 54 | 55 | return tab 56 | 57 | 58 | async def render_html(request: web.Request): 59 | # https://splash.readthedocs.io/en/stable/api.html#render-html 60 | tab = await _go(request) 61 | return web.Response(text=BS((await tab.html()).decode()).prettify()) 62 | 63 | 64 | async def render_png(request: web.Request): 65 | # https://splash.readthedocs.io/en/stable/api.html#render-png 66 | tab = await _go(request) 67 | 68 | should_render_all = True if request.query.get('render_all', False) == '1' else False 69 | 70 | if not should_render_all: 71 | data = await tab.screenshot() 72 | return web.Response(body=data, content_type='image/png') 73 | 74 | if should_render_all: 75 | raw_viewport = request.query.get('viewport', '1024x768') 76 | parts = raw_viewport.split('x') 77 | width = int(parts[0]) 78 | height = int(parts[1]) 79 | cmd = page.Page.setDeviceMetricsOverride(width=int(width), 80 | height=int(height), 81 | deviceScaleFactor=0.0, 82 | mobile=False) 83 | await tab.send_command(cmd) 84 | 85 | # model numbers affected by device metrics, so needs to come after 86 | res = await tab.send_command(dom.DOM.getDocument()) 87 | doc_node_id = res['ack']['result']['root'].nodeId 88 | res = await tab.send_command(dom.DOM.querySelector(selector='body', nodeId=doc_node_id)) 89 | body_node_id = res['ack']['result']['nodeId'] 90 | res = await tab.send_command(dom.DOM.getBoxModel(nodeId=body_node_id)) 91 | full_height = res['ack']['result']['model'].height 92 | log.debug('full_height = %s' % full_height) 93 | 94 | offset = 0 95 | import base64 96 | from PIL import Image 97 | from io import BytesIO 98 | full_image = Image.new('RGB', (int(width), int(full_height))) 99 | delta = int(height) 100 | while offset < full_height + 1: # TODO: cut+paste to exact dimensions 101 | await tab.send_command(runtime.Runtime.evaluate('window.scrollTo(0, %s)' % offset)) 102 | result = await tab.send_command(page.Page.captureScreenshot(format='png', fromSurface=False)) 103 | base64_data = result['ack']['result']['data'] 104 | snapshot = Image.open(BytesIO(base64.b64decode(base64_data))) 105 | full_image.paste(snapshot, (0, offset)) 106 | offset += delta 107 | output = BytesIO() 108 | full_image.save(output, format='png') 109 | return web.Response(body=output.getvalue(), content_type='image/png') 110 | 111 | -------------------------------------------------------------------------------- /config/dev.yaml: -------------------------------------------------------------------------------- 1 | logging: 2 | version: 1 3 | disable_existing_loggers: True 4 | formatters: 5 | simple: 6 | format: '%(levelname)s %(message)s' 7 | standard: 8 | format: '%(asctime)s %(name)-64s %(levelname)-8s %(message)s' 9 | 10 | handlers: 11 | console: 12 | level: 'DEBUG' 13 | class: 'logging.StreamHandler' 14 | formatter: 'standard' 15 | loggers: 16 | aiohttp.access: 17 | handlers: [] 18 | propagate: True 19 | level: 'INFO' 20 | websockets.protocol: 21 | handlers: ['console'] 22 | propagate: True 23 | level: 'INFO' 24 | chromewhip.chrome: 25 | handlers: ['console'] 26 | propagate: True 27 | level: 'DEBUG' 28 | chromewhip.protocol: 29 | handlers: [] 30 | propagate: True 31 | level: 'DEBUG' 32 | chromewhip.helpers: 33 | handlers: [] 34 | propagate: True 35 | level: 'DEBUG' 36 | -------------------------------------------------------------------------------- /data/browser_protocol_patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | { "op": "test", "path": "/domains/26/domain", "value": "Page" }, 3 | { "op": "add", "path": "/domains/26/dependencies/-", "value": "Runtime" }, 4 | { "op": "add", "path": "/domains/26/dependencies/-", "value": "Emulation" }, 5 | { "op": "test", "path": "/domains/24/domain", "value": "Network" }, 6 | { "op": "add", "path": "/domains/24/dependencies/-", "value": "Page" }, 7 | { "op": "test", "path": "/domains/33/domain", "value": "Tethering" }, 8 | { "op": "test", "path": "/domains/5/domain", "value": "Browser" }, 9 | { "op": "add", "path": "/domains/33/dependencies", "value": ["Target"] } 10 | ] 11 | -------------------------------------------------------------------------------- /data/devtools_protocol_msg: -------------------------------------------------------------------------------- 1 | d1cec58 Roll protocol to r698331 2 | -------------------------------------------------------------------------------- /data/js_protocol_patch.json: -------------------------------------------------------------------------------- 1 | [ 2 | ] -------------------------------------------------------------------------------- /data/protocol.py.j2: -------------------------------------------------------------------------------- 1 | # noinspection PyPep8 2 | # noinspection PyArgumentList 3 | 4 | """ 5 | AUTO-GENERATED BY `scripts/generate_protocol.py` using `data/browser_protocol.json` 6 | and `data/js_protocol.json` as inputs! Please do not modify this file. 7 | """ 8 | 9 | import logging 10 | from typing import Any, Optional, Union 11 | 12 | from chromewhip.helpers import PayloadMixin, BaseEvent, ChromeTypeBase 13 | 14 | log = logging.getLogger(__name__) 15 | 16 | {%- set py_types = dict(boolean='bool', integer='int', number='float', string='str', any='Any', object='dict') -%} 17 | 18 | {# TODO: find a way not to leak id here #} 19 | {%- macro get_py_type(prop) -%} 20 | {%- if prop.type is defined -%} 21 | {%- if prop.type == 'array' -%} 22 | {%- set t = prop['items']['$ref'] -%} 23 | {%- if t in py_types -%} 24 | [{{ py_types[prop['items']['$ref']] }}] 25 | {%- else -%} 26 | [{{ prop['items']['$ref'] }}] 27 | {%- endif -%} 28 | {%- else -%} 29 | {{ py_types[prop.type] }} 30 | {%- endif -%} 31 | {%- elif prop['$ref'] is defined -%} 32 | {{ prop['$ref'] }} 33 | {%- else -%} 34 | # ERROR: cannot find type for {{ prop }} 35 | {%- endif -%} 36 | {%- endmacro %} 37 | 38 | {% for dependency in domain.dependencies %} 39 | from chromewhip.protocol import {{ dependency | lower }} as {{ dependency }} 40 | {% endfor %} 41 | 42 | {% for type_obj in domain.types %} 43 | # {{ type_obj.id }}: {{ type_obj.description | replace('\n', '') }} 44 | {% if type_obj.type == 'object' %} 45 | class {{ type_obj.id }}(ChromeTypeBase): 46 | def __init__(self 47 | {%- if type_obj.properties is undefined -%} 48 | ): 49 | {%- else -%}, 50 | {% for prop in type_obj.properties | selectattr('optional', 'undefined') %} 51 | {{ prop.name }}: Union['{{ get_py_type(prop) }}'], 52 | {% endfor %} 53 | {% for prop in type_obj.properties | selectattr('optional', 'equalto', False) %} 54 | {{ prop.name }}: Union['{{ get_py_type(prop) }}'], 55 | {% endfor %} 56 | {% for prop in type_obj.properties | selectattr('optional', 'equalto', True) %} 57 | {{ prop.name }}: Optional['{{ get_py_type(prop) }}'] = None, 58 | {% endfor %} 59 | ): 60 | {% endif %} 61 | 62 | {% if type_obj.properties is undefined %} 63 | pass 64 | {% endif %} 65 | {% for prop in type_obj.properties %} 66 | self.{{ prop.name }} = {{ prop.name }} 67 | {% endfor %} 68 | 69 | {% elif type_obj.type == 'py_chrome_identifier' %} 70 | class {{ type_obj.id }}(str): 71 | def __eq__(self, other): 72 | print('type of other = %s ' % type(other)) 73 | if not isinstance(other, {{ type_obj.id[:-2] }}): 74 | return super().__eq__(other) 75 | return other.id_ == self.__str__() 76 | {% else %} 77 | {{ type_obj.id }} = {{ type_obj.type }} 78 | {% endif %} 79 | 80 | {% endfor %} 81 | class {{ domain.domain }}(PayloadMixin): 82 | """ {{ domain.description }} 83 | """ 84 | {% if domain.commands is undefined -%} 85 | pass 86 | {% else %} 87 | {% for c in domain.commands %} 88 | @classmethod 89 | def {{ c.name }}(cls 90 | {%- set indent_len = c.name | length + 1-%} 91 | {%- if c.parameters is undefined -%} 92 | ): 93 | {%- else -%}, 94 | {% for p in c.parameters | selectattr('optional', 'undefined') %} 95 | {{ p.name | indent(indent_len, True) }}: Union['{{ get_py_type(p) }}'], 96 | {% endfor %} 97 | {% for p in c.parameters | selectattr('optional', 'equalto', False) %} 98 | {{ p.name | indent(indent_len, True) }}: Union['{{ get_py_type(p) }}'], 99 | {% endfor %} 100 | {% for p in c.parameters | selectattr('optional', 'equalto', True) %} 101 | {{ p.name | indent(indent_len, True) }}: Optional['{{ get_py_type(p) }}'] = None, 102 | {% endfor %} 103 | {{ "):" | indent(indent_len, True) }} 104 | {%- endif %} 105 | 106 | """{{ c.description }} 107 | {% for p in c.parameters %} 108 | :param {{ p.name }}: {{ p.description }} 109 | :type {{ p.name }}: {{ get_py_type(p) }} 110 | {% endfor %} 111 | """ 112 | return ( 113 | cls.build_send_payload("{{ c.name }}", { 114 | {# as there are no null types, send_payload will remove any null values #} 115 | {% for p in c.parameters %} 116 | "{{ p.name }}": {{ p.name }}, 117 | {% endfor %} 118 | }), 119 | {% if c.returns is defined %} 120 | cls.convert_payload({ 121 | {% for r in c.returns %} 122 | "{{ r.name }}": { 123 | "class": {{ get_py_type(r) }}, 124 | "optional": {{ r.optional is defined and r.optional }} 125 | }, 126 | {% endfor %} 127 | }) 128 | {% else %} 129 | None 130 | {% endif %} 131 | ) 132 | 133 | {% endfor %} 134 | {% endif %} 135 | {% for event in domain.events %} 136 | 137 | 138 | class {{ event.py_class_name }}(BaseEvent): 139 | 140 | js_name = '{{ domain.domain | capitalize }}.{{ event.name }}' 141 | hashable = {{ event.hashable }} 142 | is_hashable = {{ event.is_hashable }} 143 | 144 | def __init__(self 145 | {%- if event.parameters is undefined -%} 146 | ): 147 | {%- else -%}, 148 | {% for prop in event.parameters | selectattr('optional', 'undefined') %} 149 | {{ prop.name }}: Union['{{ get_py_type(prop) }}', dict], 150 | {% endfor %} 151 | {% for prop in event.parameters | selectattr('optional', 'equalto', False) %} 152 | {{ prop.name }}: Union['{{ get_py_type(prop) }}', dict], 153 | {% endfor %} 154 | {% for prop in event.parameters | selectattr('optional', 'equalto', True) %} 155 | {{ prop.name }}: Union['{{ get_py_type(prop) }}', dict, None] = None, 156 | {% endfor %} 157 | ): 158 | {%- endif %} 159 | 160 | {% if event.parameters is undefined %} 161 | pass 162 | {% endif %} 163 | {% for prop in event.parameters %} 164 | if isinstance({{ prop.name }}, dict): 165 | {{ prop.name }} = {{ get_py_type(prop) }}(**{{ prop.name }}) 166 | self.{{ prop.name }} = {{ prop.name }} 167 | {% endfor %} 168 | 169 | {% if event.is_hashable %} 170 | @classmethod 171 | def build_hash(cls, {{ ", ".join(event.hashable) }}): 172 | kwargs = locals() 173 | kwargs.pop('cls') 174 | serialized_id_params = ','.join(['='.join([p, str(v)]) for p, v in kwargs.items()]) 175 | h = '{}:{}'.format(cls.js_name, serialized_id_params) 176 | log.debug('generated hash = %s' % h) 177 | return h 178 | {% else %} 179 | @classmethod 180 | def build_hash(cls): 181 | raise ValueError('Unable to build hash for non-hashable type') 182 | {% endif %} 183 | {% endfor %} 184 | -------------------------------------------------------------------------------- /dev_requirements.txt: -------------------------------------------------------------------------------- 1 | Jinja2==2.10.1 2 | jsonpatch==1.23 3 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Minimal makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = python -msphinx 7 | SPHINXPROJ = chromewhip 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) -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | # 4 | # chromewhip documentation build configuration file, created by 5 | # sphinx-quickstart on Tue Jul 18 21:13:28 2017. 6 | # 7 | # This file is execfile()d with the current directory set to its 8 | # containing dir. 9 | # 10 | # Note that not all possible configuration values are present in this 11 | # autogenerated file. 12 | # 13 | # All configuration values have a default; values that are commented out 14 | # serve to show the default. 15 | 16 | # If extensions (or modules to document with autodoc) are in another directory, 17 | # add these directories to sys.path here. If the directory is relative to the 18 | # documentation root, use os.path.abspath to make it absolute, like shown here. 19 | # 20 | # import os 21 | # import sys 22 | # sys.path.insert(0, os.path.abspath('.')) 23 | 24 | 25 | # -- General configuration ------------------------------------------------ 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | # 29 | # needs_sphinx = '1.0' 30 | 31 | # Add any Sphinx extension module names here, as strings. They can be 32 | # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom 33 | # ones. 34 | extensions = ['sphinx.ext.autodoc', 35 | 'sphinx.ext.doctest', 36 | 'sphinx.ext.todo', 37 | 'sphinx.ext.coverage', 38 | 'sphinx.ext.viewcode', 39 | 'sphinx.ext.githubpages'] 40 | 41 | # Add any paths that contain templates here, relative to this directory. 42 | templates_path = ['_templates'] 43 | 44 | # The suffix(es) of source filenames. 45 | # You can specify multiple suffix as a list of string: 46 | # 47 | # source_suffix = ['.rst', '.md'] 48 | source_suffix = '.rst' 49 | 50 | # The master toctree document. 51 | master_doc = 'index' 52 | 53 | # General information about the project. 54 | project = 'chromewhip' 55 | copyright = '2017, Charlie Smith' 56 | author = 'Charlie Smith' 57 | 58 | # The version info for the project you're documenting, acts as replacement for 59 | # |version| and |release|, also used in various other places throughout the 60 | # built documents. 61 | # 62 | # The short X.Y version. 63 | version = '0.1.0' 64 | # The full version, including alpha/beta/rc tags. 65 | release = '0.1.0' 66 | 67 | # The language for content autogenerated by Sphinx. Refer to documentation 68 | # for a list of supported languages. 69 | # 70 | # This is also used if you do content translation via gettext catalogs. 71 | # Usually you set "language" from the command line for these cases. 72 | language = None 73 | 74 | # List of patterns, relative to source directory, that match files and 75 | # directories to ignore when looking for source files. 76 | # This patterns also effect to html_static_path and html_extra_path 77 | exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] 78 | 79 | # The name of the Pygments (syntax highlighting) style to use. 80 | pygments_style = 'sphinx' 81 | 82 | # If true, `todo` and `todoList` produce output, else they produce nothing. 83 | todo_include_todos = True 84 | 85 | 86 | # -- Options for HTML output ---------------------------------------------- 87 | 88 | # The theme to use for HTML and HTML Help pages. See the documentation for 89 | # a list of builtin themes. 90 | # 91 | html_theme = 'alabaster' 92 | 93 | # Theme options are theme-specific and customize the look and feel of a theme 94 | # further. For a list of options available for each theme, see the 95 | # documentation. 96 | # 97 | # html_theme_options = {} 98 | 99 | # Add any paths that contain custom static files (such as style sheets) here, 100 | # relative to this directory. They are copied after the builtin static files, 101 | # so a file named "default.css" will overwrite the builtin "default.css". 102 | html_static_path = ['_static'] 103 | 104 | # Custom sidebar templates, must be a dictionary that maps document names 105 | # to template names. 106 | # 107 | # This is required for the alabaster theme 108 | # refs: http://alabaster.readthedocs.io/en/latest/installation.html#sidebars 109 | html_sidebars = { 110 | '**': [ 111 | 'about.html', 112 | 'navigation.html', 113 | 'relations.html', # needs 'show_related': True theme option to display 114 | 'searchbox.html', 115 | 'donate.html', 116 | ] 117 | } 118 | 119 | 120 | # -- Options for HTMLHelp output ------------------------------------------ 121 | 122 | # Output file base name for HTML help builder. 123 | htmlhelp_basename = 'chromewhipdoc' 124 | 125 | 126 | # -- Options for LaTeX output --------------------------------------------- 127 | 128 | latex_elements = { 129 | # The paper size ('letterpaper' or 'a4paper'). 130 | # 131 | # 'papersize': 'letterpaper', 132 | 133 | # The font size ('10pt', '11pt' or '12pt'). 134 | # 135 | # 'pointsize': '10pt', 136 | 137 | # Additional stuff for the LaTeX preamble. 138 | # 139 | # 'preamble': '', 140 | 141 | # Latex figure (float) alignment 142 | # 143 | # 'figure_align': 'htbp', 144 | } 145 | 146 | # Grouping the document tree into LaTeX files. List of tuples 147 | # (source start file, target name, title, 148 | # author, documentclass [howto, manual, or own class]). 149 | latex_documents = [ 150 | (master_doc, 'chromewhip.tex', 'chromewhip Documentation', 151 | 'Charlie Smith', 'manual'), 152 | ] 153 | 154 | 155 | # -- Options for manual page output --------------------------------------- 156 | 157 | # One entry per manual page. List of tuples 158 | # (source start file, name, description, authors, manual section). 159 | man_pages = [ 160 | (master_doc, 'chromewhip', 'chromewhip Documentation', 161 | [author], 1) 162 | ] 163 | 164 | 165 | # -- Options for Texinfo output ------------------------------------------- 166 | 167 | # Grouping the document tree into Texinfo files. List of tuples 168 | # (source start file, target name, title, author, 169 | # dir menu entry, description, category) 170 | texinfo_documents = [ 171 | (master_doc, 'chromewhip', 'chromewhip Documentation', 172 | author, 'chromewhip', 'One line description of project.', 173 | 'Miscellaneous'), 174 | ] 175 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. chromewhip documentation master file, created by 2 | sphinx-quickstart on Tue Jul 18 21:13:28 2017. 3 | You can adapt this file completely to your liking, but it should at least 4 | contain the root `toctree` directive. 5 | 6 | Chromewhip - Google Chrome™ as a web service 7 | ============================================ 8 | 9 | Chromewhip is an easily deployable service that runs headless Chrome process 10 | wrapped with an HTTP API. Inspired by the [`splash`](https://github.com/scrapinghub/splash) 11 | project, we aim to provide a drop-in replacement for the `splash` service by adhering to their documented API. 12 | 13 | .. toctree:: 14 | :maxdepth: 2 15 | :caption: Contents: 16 | 17 | install 18 | api 19 | 20 | 21 | 22 | Indices and tables 23 | ================== 24 | 25 | * :ref:`genindex` 26 | * :ref:`modindex` 27 | * :ref:`search` 28 | -------------------------------------------------------------------------------- /docs/install.rst: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chazkii/chromewhip/f0014b1afe077588f77649bf31d1c41071d835a7/docs/install.rst -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | aiohttp==3.6.2 2 | pytest-asyncio==0.10.0 3 | websockets==7.0 4 | beautifulsoup4==4.7.1 5 | lxml==4.6.2 6 | pyyaml==5.1 7 | Pillow==7.1.0 8 | -------------------------------------------------------------------------------- /run_docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker build . -t chromewhip 4 | #docker run --init -it --rm --name chromewhip --shm-size=1024m --cap-add=SYS_ADMIN -p=127.0.0.1:80:33233 chromewhip 5 | docker run --init -it --rm --name chromewhip --shm-size=1024m --cap-add=SYS_ADMIN -p=33233:8080 -p 5900:5900 chromewhip 6 | -------------------------------------------------------------------------------- /scripts/check_generation.py: -------------------------------------------------------------------------------- 1 | import sys 2 | sys.path.insert(0, "../") 3 | import chromewhip.protocol.console 4 | import chromewhip.protocol.debugger 5 | import chromewhip.protocol.heapprofiler 6 | import chromewhip.protocol.profiler 7 | import chromewhip.protocol.runtime 8 | import chromewhip.protocol.schema 9 | import chromewhip.protocol.accessibility 10 | import chromewhip.protocol.animation 11 | import chromewhip.protocol.applicationcache 12 | import chromewhip.protocol.audits 13 | import chromewhip.protocol.backgroundservice 14 | import chromewhip.protocol.browser 15 | import chromewhip.protocol.css 16 | import chromewhip.protocol.cachestorage 17 | import chromewhip.protocol.cast 18 | import chromewhip.protocol.dom 19 | import chromewhip.protocol.domdebugger 20 | import chromewhip.protocol.domsnapshot 21 | import chromewhip.protocol.domstorage 22 | import chromewhip.protocol.database 23 | import chromewhip.protocol.deviceorientation 24 | import chromewhip.protocol.emulation 25 | import chromewhip.protocol.headlessexperimental 26 | import chromewhip.protocol.io 27 | import chromewhip.protocol.indexeddb 28 | import chromewhip.protocol.input 29 | import chromewhip.protocol.inspector 30 | import chromewhip.protocol.layertree 31 | import chromewhip.protocol.log 32 | import chromewhip.protocol.memory 33 | import chromewhip.protocol.network 34 | import chromewhip.protocol.overlay 35 | import chromewhip.protocol.page 36 | import chromewhip.protocol.performance 37 | import chromewhip.protocol.security 38 | import chromewhip.protocol.serviceworker 39 | import chromewhip.protocol.storage 40 | import chromewhip.protocol.systeminfo 41 | import chromewhip.protocol.target 42 | import chromewhip.protocol.tethering 43 | import chromewhip.protocol.tracing 44 | import chromewhip.protocol.fetch 45 | import chromewhip.protocol.webaudio 46 | import chromewhip.protocol.webauthn 47 | import chromewhip.protocol.media 48 | -------------------------------------------------------------------------------- /scripts/generate_protocol.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os.path 3 | import re 4 | import sys 5 | 6 | from jinja2 import Template 7 | 8 | # TODO: circular dependency below 9 | # PACKAGE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 10 | # 11 | # sys.path.insert(0, PACKAGE_DIR) 12 | # 13 | # from chromewhip.helpers import camelize 14 | 15 | FULL_CAP_WORDS = ['url', 'dom', 'css', 'html'] 16 | 17 | 18 | def camelize(string): 19 | words = string.split('_') 20 | result = words[0] 21 | for w in words[1:]: 22 | w = w.upper() if w in FULL_CAP_WORDS else w.title() 23 | result += w 24 | return result 25 | 26 | camel_pat = re.compile(r'([A-Z]+)') 27 | 28 | template_fp = os.path.abspath(os.path.join(os.path.dirname(__file__), '../data/protocol.py.j2')) 29 | output_dir_fp = os.path.abspath(os.path.join(os.path.dirname(__file__), '../chromewhip/protocol/')) 30 | browser_json_fp = ('browser', os.path.abspath(os.path.join(os.path.dirname(__file__), '../data/browser_protocol.json'))) 31 | js_json_fp = ('js', os.path.abspath(os.path.join(os.path.dirname(__file__), '../data/js_protocol.json'))) 32 | test_script_fp = os.path.abspath(os.path.join(os.path.dirname(__file__), './check_generation.py')) 33 | 34 | 35 | # https://stackoverflow.com/questions/17156078/converting-identifier-naming-between-camelcase-and-underscores-during-json-seria 36 | 37 | 38 | 39 | JS_PYTHON_TYPES = { 40 | 'string': 'str', 41 | 'number': 'float', 42 | 'integer': 'int', 43 | } 44 | 45 | 46 | def set_py_type(type_obj, type_ids_set): 47 | type_ = type_obj['type'] 48 | id_ = type_obj['id'] 49 | if re.match(r'.*Id', id_) and id_[:2] in type_ids_set: 50 | new_type_ = 'py_chrome_identifier' 51 | elif type_ == 'array': 52 | item_type = type_obj['items'].get('$ref') or type_obj['items'].get('type') 53 | try: 54 | new_type_ = '[%s]' % JS_PYTHON_TYPES[item_type] 55 | except KeyError: 56 | new_type_ = '[%s]' % item_type 57 | elif type_ == 'object': 58 | if not type_obj.get('properties'): 59 | new_type_ = 'dict' 60 | else: 61 | new_type_ = 'object' 62 | elif type_ in JS_PYTHON_TYPES.keys(): 63 | new_type_ = JS_PYTHON_TYPES[type_] 64 | else: 65 | raise ValueError('type "%s" is not recognised, check type data = %s' % (type_, type_obj)) 66 | type_obj['type'] = new_type_ 67 | 68 | # import autopep8 69 | processed_data = {} 70 | hashable_objs_per_prot = {} 71 | for fpd in [js_json_fp, browser_json_fp]: 72 | pname, fp = fpd 73 | data = json.load(open(fp)) 74 | 75 | # first run 76 | hashable_objs = set() 77 | for domain in data['domains']: 78 | 79 | for event in domain.get('events', []): 80 | event['py_class_name'] = event['name'][0].upper() + camelize(event['name'][1:]) + 'Event' 81 | 82 | # 12 Jul 9:37am - wont need this for now as have enhanced Id type 83 | # for cmd in domain.get('commands', []): 84 | # for r in cmd.get('returns', []): 85 | # # convert to non id 86 | # if r.get('$ref', ''): 87 | # r['$ref'] = re.sub(r'Id$', '', r['$ref']) 88 | 89 | for type_obj in domain.get('types', []): 90 | # we assume all type ids that contain `Id` are an alias for a built in type 91 | if any(filter(lambda p: p['name'] == 'id', type_obj.get('properties', []))): 92 | hashable_objs.add(type_obj['id']) 93 | 94 | 95 | # shorten references to drop domain if part of same module 96 | # for command in domain.get('commands', []): 97 | # for parameter in command.get('parameters', []): 98 | # ref = parameter.get('$ref') 99 | # if ref and ref.split('.')[0] == domain['name']: 100 | # print('modifying command "%s"' % '.'.join([domain['name'], command['name']])) 101 | # ref 102 | 103 | hashable_objs_per_prot[pname] = hashable_objs 104 | processed_data[pname] = data 105 | 106 | # second run 107 | for k, v in processed_data.items(): 108 | hashable_objs = hashable_objs_per_prot[k] 109 | for domain in v['domains']: 110 | for type_obj in domain.get('types', []): 111 | # convert to richer, Python compatible types 112 | set_py_type(type_obj, hashable_objs) 113 | 114 | for event in domain.get('events', []): 115 | p_names = [p['name'] for p in event.get('parameters', [])] 116 | p_refs = [(p['name'], p['$ref']) for p in event.get('parameters', []) if p.get('$ref')] 117 | h_names = set(filter(lambda n: 'id' in n or 'Id' in n, p_names)) 118 | for pn, pr in p_refs: 119 | if pr in hashable_objs: 120 | h_names.add(pn + 'Id') 121 | event['hashable'] = list(h_names) 122 | event['is_hashable'] = len(event['hashable']) > 0 123 | 124 | # finally write to file 125 | t = Template(open(template_fp).read(), trim_blocks=True, lstrip_blocks=True) 126 | test_f = open(test_script_fp, 'w') 127 | test_f.write('''import sys 128 | sys.path.insert(0, "../") 129 | ''') 130 | for prot in processed_data.values(): 131 | for domain in prot['domains']: 132 | name = domain['domain'].lower() 133 | with open(os.path.join(output_dir_fp, "%s.py" % name), 'w') as f: 134 | output = t.render(domain=domain) 135 | f.write(output) 136 | test_f.write('import chromewhip.protocol.%s\n' % name) 137 | 138 | test_f.close() 139 | -------------------------------------------------------------------------------- /scripts/get_latest_chrome.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # delete previously versions 4 | \rm -rf dl.google.com opt 5 | 6 | # download latest unstable, beta, & stable chrome versions 7 | wget -mS https://dl.google.com/linux/direct/google-chrome-{unstable,beta,stable}_current_amd64.deb 8 | 9 | # on Debian/Ubuntu based systems, `dpkg --fsys-tarfile` can also be used to extract deb file contents, 10 | # but ar is more generic, and availalbe on all Linux variants 11 | ar p dl.google.com/linux/direct/google-chrome-unstable_current_amd64.deb data.tar.xz | tar --xz -xvv ./opt/google/chrome-unstable/ 12 | ar p dl.google.com/linux/direct/google-chrome-beta_current_amd64.deb data.tar.xz | tar --xz -xvv ./opt/google/chrome-beta/ 13 | ar p dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb data.tar.xz | tar --xz -xvv ./opt/google/chrome/ -------------------------------------------------------------------------------- /scripts/regenerate_protocol.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | rm -rf devtools-protocol 4 | git clone --depth=1 https://github.com/ChromeDevTools/devtools-protocol.git 5 | cd devtools-protocol 6 | LATEST_GIT_MSG=$(git log --oneline --no-color) 7 | CHROMEWHIP_GIT_MSG=$(cat ../../data/devtools_protocol_msg) 8 | echo $LATEST_GIT_MSG 9 | echo $CHROMEWHIP_GIT_MSG 10 | if [ "$LATEST_GIT_MSG" != "$CHROMEWHIP_GIT_MSG" ] 11 | then 12 | echo "devtools-protocol has been updated. Regenerating chromewhip protocol files." 13 | cd ../.. 14 | jsonpatch --indent 4 scripts/devtools-protocol/json/browser_protocol.json data/browser_protocol_patch.json > data/browser_protocol.json 15 | jsonpatch --indent 4 scripts/devtools-protocol/json/js_protocol.json data/js_protocol_patch.json > data/js_protocol.json 16 | rm -rf scripts/devtools-protocol 17 | echo "$LATEST_GIT_MSG" > data/devtools_protocol_msg 18 | cd scripts 19 | if $(python generate_protocol.py); then 20 | echo "Regeneration complete!" 21 | else 22 | echo "Regeneration failed! Exiting" 23 | exit 1 24 | fi 25 | if $(python check_generation.py); then 26 | echo "Sanity check passed!" 27 | else 28 | echo "Sanity check failed! Please manually check the generated protocol files" 29 | exit 1 30 | fi 31 | # add all newly generated modules 32 | git add ../chromewhip/protocol 33 | git commit -a -m "$LATEST_GIT_MSG" 34 | # TODO: fix me so i don't push if the the variables below are not set 35 | # git push https://$CHROMEWHIP_BOT_USERNAME:$CHROMEWHIP_BOT_PASSWORD@github.com/chuckus/chromewhip.git 36 | else 37 | echo "no changes found!" 38 | rm -rf scripts/devtools-protocol 39 | fi 40 | -------------------------------------------------------------------------------- /scripts/run_chromewhip_linux.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | echo "Starting window manager..." 4 | fluxbox -display $DISPLAY & 5 | 6 | echo "Starting VNC server..." 7 | x11vnc -forever -shared -rfbport 5900 -display $DISPLAY & 8 | 9 | echo "Starting Chromewhip..." 10 | python3.7 -m chromewhip.__init__ --js-profiles-path /usr/jsprofiles 11 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bumpversion] 2 | current_version = 0.3.4 3 | parse = (?P\d+)\.(?P\d+)\.(?P\d+) 4 | commit = True 5 | tag = True 6 | serialize = 7 | {major}.{minor}.{patch} 8 | 9 | [bumpversion:file:setup.py] 10 | 11 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | # Always prefer setuptools over distutils 2 | from setuptools import setup, find_packages 3 | 4 | from os import path 5 | 6 | here = path.abspath(path.dirname(__file__)) 7 | 8 | 9 | def readme(): 10 | try: 11 | with open('README.rst') as f: 12 | return f.read() 13 | except FileNotFoundError: 14 | return "" 15 | 16 | setup( 17 | name='chromewhip', 18 | 19 | version='0.3.4', 20 | 21 | description='asyncio driver + HTTP server for Chrome devtools protocol', 22 | long_description=readme(), 23 | # The project's main homepage. 24 | url='https://github.com/chuckus/chromewhip', 25 | download_url='https://github.com/chuckus/chromewhip/archive/v0.3.4.tar.gz', 26 | 27 | # Author details 28 | author='Charlie Smith', 29 | author_email='charlie@chuckus.nz', 30 | 31 | # Choose your license 32 | license='MIT', 33 | 34 | # See https://pypi.python.org/pypi?%3Aaction=list_classifiers 35 | classifiers=[ 36 | 'Development Status :: 3 - Alpha', 37 | 38 | 'Intended Audience :: Developers', 39 | 'Topic :: Software Development :: Build Tools', 40 | 41 | 'License :: OSI Approved :: MIT License', 42 | 43 | 'Programming Language :: Python :: 3.7', 44 | ], 45 | 46 | # What does your project relate to? 47 | keywords='scraping chrome scraper browser automation', 48 | 49 | # You can just specify the packages manually here if your project is 50 | # simple. Or you can use find_packages(). 51 | packages=find_packages(exclude=['contrib', 'docs', 'tests']), 52 | 53 | # Alternatively, if you want to distribute just a my_module.py, uncomment 54 | # this: 55 | # py_modules=["my_module"], 56 | 57 | # List run-time dependencies here. These will be installed by pip when 58 | # your project is installed. For an analysis of "install_requires" vs pip's 59 | # requirements files see: 60 | # https://packaging.python.org/en/latest/requirements.html 61 | install_requires=[ 62 | 'aiohttp==3.6.2', 'websockets==7.0', 'beautifulsoup4==4.7.1', 'lxml==4.6.2', 63 | 'pyyaml==5.1', 'Pillow==7.1.0' 64 | ], 65 | 66 | # List additional groups of dependencies here (e.g. development 67 | # dependencies). You can install these using the following syntax, 68 | # for example: 69 | # $ pip install -e .[dev,test] 70 | extras_require={ 71 | 'dev': ['Jinja2==2.10.1', 'jsonpatch==1.23'], 72 | 'test': ['pytest-asyncio==0.10.0'], 73 | }, 74 | 75 | # To provide executable scripts, use entry points in preference to the 76 | # "scripts" keyword. Entry points provide cross-platform support and allow 77 | # pip to create the appropriate form of executable for the target platform. 78 | entry_points={ 79 | 'console_scripts': [ 80 | 'chromewhip=chromewhip:main', 81 | ], 82 | }, 83 | ) 84 | -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | # https://github.com/pytest-dev/pytest-asyncio/issues/52 2 | # pytest_plugins = 'aiohttp.pytest_plugin' 3 | 4 | import pytest 5 | import os 6 | import sys 7 | 8 | PACKAGE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..')) 9 | 10 | sys.path.append(PACKAGE_DIR) 11 | 12 | -------------------------------------------------------------------------------- /tests/resources/js/profiles/httpbin-org-html/001_change_p.js: -------------------------------------------------------------------------------- 1 | document.getElementsByTagName('p')[0].innerText = 'Chromewhip'; 2 | var element = document.createElement('h3'); 3 | element.innerText = 'First profile ran only!'; 4 | document.body.appendChild(element); -------------------------------------------------------------------------------- /tests/resources/js/profiles/httpbin-org-html/002_change_h3.js: -------------------------------------------------------------------------------- 1 | document.getElementsByTagName('h3')[0].innerText = 'All profiles ran!'; -------------------------------------------------------------------------------- /tests/resources/responses/httpbin.org.html.after_profile.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Herman Melville - Moby-Dick

5 | 6 |
7 |

8 | Chromewhip 9 |

10 |
11 |

All profiles ran!

12 | 13 | -------------------------------------------------------------------------------- /tests/resources/responses/httpbin.org.html.txt: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Herman Melville - Moby-Dick

5 | 6 |
7 |

8 | Availing himself of the mild, summer-cool weather that now reigned in these latitudes, and in preparation for the peculiarly active pursuits shortly to be anticipated, Perth, the begrimed, blistered old blacksmith, had not removed his portable forge to the hold again, after concluding his contributory work for Ahab's leg, but still retained it on deck, fast lashed to ringbolts by the foremast; being now almost incessantly invoked by the headsmen, and harpooneers, and bowsmen to do some little job for them; altering, or repairing, or new shaping their various weapons and boat furniture. Often he would be surrounded by an eager circle, all waiting to be served; holding boat-spades, pike-heads, harpoons, and lances, and jealously watching his every sooty movement, as he toiled. Nevertheless, this old man's was a patient hammer wielded by a patient arm. No murmur, no impatience, no petulance did come from him. Silent, slow, and solemn; bowing over still further his chronically broken back, he toiled away, as if toil were life itself, and the heavy beating of his hammer the heavy beating of his heart. And so it was.—Most miserable! A peculiar walk in this old man, a certain slight but painful appearing yawing in his gait, had at an early period of the voyage excited the curiosity of the mariners. And to the importunity of their persisted questionings he had finally given in; and so it came to pass that every one now knew the shameful story of his wretched fate. Belated, and not innocently, one bitter winter's midnight, on the road running between two country towns, the blacksmith half-stupidly felt the deadly numbness stealing over him, and sought refuge in a leaning, dilapidated barn. The issue was, the loss of the extremities of both feet. Out of this revelation, part by part, at last came out the four acts of the gladness, and the one long, and as yet uncatastrophied fifth act of the grief of his life's drama. He was an old man, who, at the age of nearly sixty, had postponedly encountered that thing in sorrow's technicals called ruin. He had been an artisan of famed excellence, and with plenty to do; owned a house and garden; embraced a youthful, daughter-like, loving wife, and three blithe, ruddy children; every Sunday went to a cheerful-looking church, planted in a grove. But one night, under cover of darkness, and further concealed in a most cunning disguisement, a desperate burglar slid into his happy home, and robbed them all of everything. And darker yet to tell, the blacksmith himself did ignorantly conduct this burglar into his family's heart. It was the Bottle Conjuror! Upon the opening of that fatal cork, forth flew the fiend, and shrivelled up his home. Now, for prudent, most wise, and economic reasons, the blacksmith's shop was in the basement of his dwelling, but with a separate entrance to it; so that always had the young and loving healthy wife listened with no unhappy nervousness, but with vigorous pleasure, to the stout ringing of her young-armed old husband's hammer; whose reverberations, muffled by passing through the floors and walls, came up to her, not unsweetly, in her nursery; and so, to stout Labor's iron lullaby, the blacksmith's infants were rocked to slumber. Oh, woe on woe! Oh, Death, why canst thou not sometimes be timely? Hadst thou taken this old blacksmith to thyself ere his full ruin came upon him, then had the young widow had a delicious grief, and her orphans a truly venerable, legendary sire to dream of in their after years; and all of them a care-killing competency. 9 |

10 |
11 | 12 | -------------------------------------------------------------------------------- /tests/test_chrome.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import copy 3 | import json 4 | import logging 5 | import queue 6 | 7 | import pytest 8 | import websockets 9 | from websockets.exceptions import ConnectionClosed 10 | 11 | 12 | from chromewhip import chrome, helpers 13 | from chromewhip.protocol import page, network 14 | 15 | TEST_HOST = 'localhost' 16 | TEST_PORT = 32322 17 | 18 | logging.basicConfig(level=logging.DEBUG) 19 | log = logging.getLogger(__name__) 20 | 21 | 22 | class ChromeMock: 23 | 24 | def __init__(self, host, port): 25 | self._tabs = [] 26 | 27 | async def connect(self): 28 | tab = chrome.ChromeTab('test', 'about:blank', f'ws://{TEST_HOST}:{TEST_PORT}', '123') 29 | self._tabs = [tab] 30 | 31 | @property 32 | def tabs(self): 33 | return self._tabs 34 | 35 | 36 | @pytest.fixture 37 | async def chrome_tab(): 38 | """Ensure Chrome is running 39 | """ 40 | browser = ChromeMock(host=TEST_HOST, port=TEST_PORT) 41 | await browser.connect() 42 | chrome_tab = browser.tabs[0] 43 | yield chrome_tab 44 | print("gracefully disconnecting chrome tab...") 45 | try: 46 | await chrome_tab.disconnect() 47 | except ConnectionClosed: 48 | pass 49 | 50 | delay_s = float 51 | 52 | 53 | def init_test_server(triggers: dict, initial_msgs: [dict] = None, expected: queue.Queue = None): 54 | """ 55 | :param initial_msgs: 56 | :param triggers: 57 | :param expected: ordered sequence of messages expected to be sent by chromewhip 58 | :return: 59 | """ 60 | async def test_server(websocket, path): 61 | """ 62 | :param websocket: 63 | :param path: 64 | :return: 65 | """ 66 | log.info('Client connected! Starting handler!') 67 | if initial_msgs: 68 | for m in initial_msgs: 69 | await websocket.send(json.dumps(m, cls=helpers.ChromewhipJSONEncoder)) 70 | 71 | c = 0 72 | 73 | try: 74 | while True: 75 | msg = await websocket.recv() 76 | log.info('Test server received message!') 77 | c += 1 78 | obj = json.loads(msg) 79 | 80 | if expected: 81 | try: 82 | exp = expected.get(block=False) 83 | except queue.Empty: 84 | pytest.fail('more messages received that expected') 85 | 86 | assert exp == obj, 'message number %s does not match, exp %s != recv %s' % (c, exp, obj) 87 | 88 | # either id or method 89 | is_method = False 90 | id_ = obj.get('id') 91 | 92 | if not id_: 93 | id_ = obj.get('method') 94 | if not id_: 95 | pytest.fail('received invalid message, no id or method - %s ' % msg) 96 | is_method = True 97 | 98 | response_stream = triggers.get(id_) 99 | 100 | if not response_stream: 101 | pytest.fail('received unexpected message of %s = "%s"' 102 | % ('method' if is_method else 'id', id_)) 103 | 104 | if not len(response_stream): 105 | log.debug('expected message but no expected response, continue') 106 | 107 | log.debug('replying with payload "%s"' % response_stream) 108 | for r in response_stream: 109 | if isinstance(r, int): 110 | await asyncio.sleep(r) 111 | else: 112 | await websocket.send(json.dumps(r, cls=helpers.ChromewhipJSONEncoder)) 113 | except asyncio.CancelledError as e: 114 | # TODO: look at failure logic here, why cancelled error? why empty? empty could mean it is working properly 115 | # if expected.empty(): 116 | # pytest.fail('less messages received that expected') 117 | raise e 118 | return test_server 119 | 120 | 121 | @pytest.mark.asyncio 122 | async def test_send_command_can_trigger_on_event_prior_to_commmand_containing_event_id(event_loop, chrome_tab): 123 | 124 | msg_id = 4 125 | frame_id = '3228.1' 126 | url = 'http://example.com' 127 | 128 | chrome_tab._message_id = msg_id - 1 129 | f = page.Frame(frame_id, 'test', url, 'test', 'text/html') 130 | p = page.Page.navigate(url) 131 | fe = page.FrameNavigatedEvent(f) 132 | 133 | ack = {'id': msg_id, 'result': {'frameId': frame_id}} 134 | triggers = { 135 | msg_id: [ack] 136 | } 137 | 138 | end_msg = copy.copy(p[0]) 139 | end_msg['id'] = msg_id 140 | q = queue.Queue() 141 | q.put(end_msg) 142 | 143 | initial_msgs = [fe] 144 | 145 | test_server = init_test_server(triggers, initial_msgs=initial_msgs, expected=q) 146 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT) 147 | server = await start_server 148 | await chrome_tab.connect() 149 | 150 | log.info('Sending command and awaiting...') 151 | result = await chrome_tab.send_command(p, await_on_event_type=page.FrameNavigatedEvent) 152 | assert result.get('ack') is not None 153 | assert result.get('event') is not None 154 | event = result.get('event') 155 | assert isinstance(event, page.FrameNavigatedEvent) 156 | assert event.frame.id == f.id 157 | assert event.frame.url == f.url 158 | 159 | server.close() 160 | await server.wait_closed() 161 | 162 | @pytest.mark.asyncio 163 | async def test_send_command_can_trigger_on_event_after_commmand_containing_event_id(event_loop, chrome_tab): 164 | msg_id = 4 165 | frame_id = '3228.1' 166 | url = 'http://example.com' 167 | 168 | chrome_tab._message_id = msg_id - 1 169 | f = page.Frame(frame_id, 'test', url, 'test', 'text/html') 170 | p = page.Page.navigate(url) 171 | fe = page.FrameNavigatedEvent(f) 172 | 173 | ack = {'id': msg_id, 'result': {'frameId': frame_id}} 174 | triggers = { 175 | msg_id: [ack, delay_s(1), fe] 176 | } 177 | 178 | end_msg = copy.copy(p[0]) 179 | end_msg['id'] = msg_id 180 | q = queue.Queue() 181 | q.put(end_msg) 182 | q.put(copy.copy(end_msg)) 183 | 184 | test_server = init_test_server(triggers, expected=q) 185 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT) 186 | server = await start_server 187 | await chrome_tab.connect() 188 | 189 | log.info('Sending command and awaiting...') 190 | result = await chrome_tab.send_command(p, await_on_event_type=page.FrameNavigatedEvent) 191 | assert result.get('ack') is not None 192 | assert result.get('event') is not None 193 | event = result.get('event') 194 | assert isinstance(event, page.FrameNavigatedEvent) 195 | assert event.frame.id == f.id 196 | assert event.frame.url == f.url 197 | 198 | server.close() 199 | await server.wait_closed() 200 | 201 | @pytest.mark.asyncio 202 | async def test_send_command_can_trigger_on_event_with_input_event(event_loop, chrome_tab): 203 | """test_send_command_can_trigger_on_event_with_input_event 204 | Below is test case that will workaround this issue 205 | https://github.com/chuckus/chromewhip/issues/2 206 | """ 207 | msg_id = 4 208 | old_frame_id = '2000.1' 209 | frame_id = '3228.1' 210 | url = 'http://example.com' 211 | 212 | chrome_tab._message_id = msg_id - 1 213 | f = page.Frame(frame_id, 'test', url, 'test', 'text/html') 214 | p = page.Page.navigate(url) 215 | fe = page.FrameNavigatedEvent(f) 216 | fsle = page.FrameStoppedLoadingEvent(frame_id) 217 | 218 | # command ack is not related to proceeding events 219 | ack = {'id': msg_id, 'result': {'frameId': old_frame_id}} 220 | triggers = { 221 | msg_id: [ack, delay_s(1), fe, fsle] 222 | } 223 | 224 | end_msg = copy.copy(p[0]) 225 | end_msg['id'] = msg_id 226 | q = queue.Queue() 227 | q.put(end_msg) 228 | 229 | test_server = init_test_server(triggers, expected=q) 230 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT) 231 | server = await start_server 232 | await chrome_tab.connect() 233 | 234 | log.info('Sending command and awaiting...') 235 | result = await chrome_tab.send_command(p, 236 | input_event_type=page.FrameNavigatedEvent, 237 | await_on_event_type=page.FrameStoppedLoadingEvent) 238 | assert result.get('ack') is not None 239 | assert result.get('event') is not None 240 | event = result.get('event') 241 | assert isinstance(event, page.FrameStoppedLoadingEvent) 242 | assert event.frameId == f.id 243 | 244 | server.close() 245 | await server.wait_closed() 246 | 247 | @pytest.mark.asyncio 248 | async def xtest_can_register_callback_on_devtools_event(event_loop, chrome_tab): 249 | # TODO: double check this part of the api is implemented 250 | interception_id = '3424.1' 251 | msg_id = 7 252 | chrome_tab._message_id = msg_id - 1 253 | fake_request = network.Request(url='http://httplib.org', 254 | method='POST', 255 | headers={}, 256 | initialPriority='superlow', 257 | referrerPolicy='origin') 258 | msgs = [ 259 | network.RequestInterceptedEvent(interceptionId=interception_id, 260 | request=fake_request, 261 | resourceType="Document", 262 | isNavigationRequest=False) 263 | 264 | ] 265 | 266 | enable = network.Network.setRequestInterceptionEnabled(enabled=True) 267 | 268 | # once emable command comes, send flurry in intercept events 269 | triggers = { 270 | msg_id: msgs 271 | } 272 | 273 | expected = queue.Queue() 274 | e0 = copy.copy(enable[0]) 275 | e0['id'] = msg_id 276 | expected.put(e0) 277 | e1 = network.Network.continueInterceptedRequest(interceptionId=interception_id) 278 | expected.put(e1) 279 | 280 | test_server = init_test_server(triggers, expected=expected) 281 | start_server = websockets.serve(test_server, TEST_HOST, TEST_PORT) 282 | server = await start_server 283 | await chrome_tab.connect() 284 | 285 | log.info('Sending command and awaiting...') 286 | # TODO: registration api 287 | 288 | # no point returning data as nothing to do with it. 289 | # but how would i go about storing all the events being collected? 290 | # - this is not the api for it, just add an api for storing events in a queue 291 | # TODO: how do declare return type of method? 292 | async def cb_coro(event: network.RequestInterceptedEvent): 293 | return network.Network.continueInterceptedRequest(interceptionId=event.interceptionId) 294 | 295 | with chrome_tab.schedule_coro_on_event(coro=cb_coro, 296 | event=network.RequestInterceptedEvent): 297 | await chrome_tab.send_command(enable) 298 | 299 | server.close() 300 | await server.wait_closed() 301 | -------------------------------------------------------------------------------- /tests/test_chromewhip.py: -------------------------------------------------------------------------------- 1 | from urllib.parse import quote 2 | import os 3 | import sys 4 | 5 | import pytest 6 | 7 | PROJECT_ROOT = os.path.abspath(os.path.join(__file__, '../..')) 8 | sys.path.insert(0, PROJECT_ROOT) 9 | 10 | from chromewhip import setup_app 11 | from chromewhip.views import BS 12 | from aiohttp.test_utils import TestClient as tc 13 | HTTPBIN_HOST = 'http://httpbin.org' 14 | 15 | RESPONSES_DIR = os.path.join(os.path.dirname(__file__), 'resources/responses') 16 | 17 | # TODO: start chrome process in here 18 | # https://docs.pytest.org/en/3.1.3/xunit_setup.html#module-level-setup-teardown 19 | 20 | @pytest.mark.asyncio 21 | async def xtest_render_html_basic(event_loop): 22 | expected = BS(open(os.path.join(RESPONSES_DIR, 'httpbin.org.html.txt')).read()).prettify() 23 | client = tc(setup_app(loop=event_loop), loop=event_loop) 24 | await client.start_server() 25 | resp = await client.get('/render.html?url={}'.format(quote('{}/html'.format(HTTPBIN_HOST)))) 26 | assert resp.status == 200 27 | text = await resp.text() 28 | assert expected == text 29 | 30 | @pytest.mark.asyncio 31 | async def xtest_render_html_with_js_profile(event_loop): 32 | expected = BS(open(os.path.join(RESPONSES_DIR, 'httpbin.org.html.after_profile.txt')).read()).prettify() 33 | profile_name = 'httpbin-org-html' 34 | profile_path = os.path.join(PROJECT_ROOT, 'tests/resources/js/profiles/{}'.format(profile_name)) 35 | client = tc(setup_app(loop=event_loop, js_profiles_path=profile_path), loop=event_loop) 36 | await client.start_server() 37 | resp = await client.get('/render.html?url={}&js={}'.format( 38 | quote('{}/html'.format(HTTPBIN_HOST)), 39 | profile_name)) 40 | assert resp.status == 200 41 | text = await resp.text() 42 | assert expected == text 43 | -------------------------------------------------------------------------------- /tests/test_helpers.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import pytest 4 | 5 | from chromewhip import helpers 6 | from chromewhip.protocol import page 7 | 8 | 9 | def test_valid_json_to_event(): 10 | valid_payload = {"method": "Page.frameNavigated", "params": { 11 | "frame": {"id": "3635.1", "loaderId": "3635.1", "url": "http://httpbin.org/html", 12 | "securityOrigin": "http://httpbin.org", "mimeType": "text/html"}}} 13 | 14 | event = helpers.json_to_event(valid_payload) 15 | assert event.__class__ == page.FrameNavigatedEvent 16 | assert event.frame.loaderId == "3635.1" 17 | 18 | 19 | def test_invalid_method_json_to_event(): 20 | valid_payload = {"method": "Page.invalidEvent", "params": { 21 | "frame": {"id": "3635.1", "loaderId": "3635.1", "url": "http://httpbin.org/html", 22 | "securityOrigin": "http://httpbin.org", "mimeType": "text/html"}}} 23 | with pytest.raises(AttributeError) as exc_info: 24 | helpers.json_to_event(valid_payload) 25 | 26 | 27 | def test_json_encoder_event(): 28 | f = page.Frame(1, 'test', 'http://example.com', 'test', 'text/html') 29 | fe = page.FrameNavigatedEvent(f) 30 | payload = json.dumps(fe, cls=helpers.ChromewhipJSONEncoder) 31 | assert payload.count('"method":') == 1 32 | assert payload.count('"params":') == 1 33 | 34 | 35 | def test_json_encoder_type(): 36 | f = page.Frame(1, 'test', 'http://example.com', 'test', 'text/html') 37 | payload = json.dumps(f, cls=helpers.ChromewhipJSONEncoder) 38 | assert payload.count('"id": 1') == 1 39 | assert payload.count('"url": "http://example.com"') == 1 40 | 41 | 42 | def test_hash_from_concrete_event(): 43 | f = page.Frame(3, 'test', 'http://example.com', 'test', 'text/html') 44 | fe = page.FrameNavigatedEvent(f) 45 | assert fe.hash_() == "Page.frameNavigated:frameId=3" 46 | 47 | 48 | def test_build_hash_from_event_cls(): 49 | hash = page.FrameNavigatedEvent.build_hash(frameId=3) 50 | assert hash == "Page.frameNavigated:frameId=3" 51 | 52 | 53 | --------------------------------------------------------------------------------